Between my job and my own projects I've spent the last couple of months switching between several languages on an almost daily basis. I've spent a not insignificant amount of time winnowing down my development environment to Sublime Text and the standard tools available for the language and platform I'm working on (where possible) to simplify switching and make my core workflow more enjoyable. Given that personal goal I've been contemplating the concept of enjoyable when it comes to programming software.

Syntax

The first thing you really encounter when talking about programming is the syntax of the language being used, the actual written code and what it means. In the best scenarios, syntax is the expression of the thought process behind the language.

#include <stdio.h>
#include <stdlib.h>

int main()
{
    char *file_name = "test.txt";
    char *line;
    size_t len;
    FILE *fp;
 
    fp = fopen(file_name,"r");
 
    if( fp == NULL )
    {
        perror("Error while opening the file.\n");
        exit(EXIT_FAILURE);
    }
 
    while(line = fgetln(fp, &len)) 
    {
        printf("%s", line);
    }
 
    fclose(fp);
    return 0;
}

Even if the nuance of the code is unclear, each step of the operation is ordered as you would expect. If you don't know what FILE *fp is, you can at least see from context that it's used to reference the file data. C code is meant for readability and primarily procedural code, it's almost possible to see how the steps above would translate to CPU instructions. A lot of the look of C has flowed into many popular subsequent languages, like Java, C#, Rust, and Swift.

import java.io.*;

public class PrintFile {
    public static void main(String [] args) {
        String fileName = "temp.txt";
        String line = null

        try {
            FileReader fileReader = new FileReader(fileName);

            BufferedReader bufferedReader = new BufferedReader(fileReader);

            while((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }   

            bufferedReader.close();         
        }
        catch(FileNotFoundException ex) {
            System.out.println("Unable to open file '" + fileName + "'");                
        }
        catch(IOException ex) {
            System.out.println("Error reading file '" + fileName + "'");                  
        }
    }
}

The C structural lineage is clear in this Java code, but there are several more constructs which may be unfamiliar on first approach. This code focuses on using these libraries like FileReader and BufferedReader. Without too much of a stretch, it's easy to see that theres more of an emphasis on object oriented programming, constructing functionality modularly to build complicated and robust systems.

def main():
  file_name = "test.txt"
  with open(file_name) as f:
    for line in f:
      print line
defmodule PrintFile do
  def main(args) do
    file_name = "test.txt"
    file_name
    |> File.stream!
    |> Enum.each(&IO.write/1)
  end
end

Syntax ends up playing a major part in the usage of a language, but it can also add an aesthetic element. Python and Elixir both allow for a high level of expressiveness. The code snippets above are simple, readable, and efficient. Compare this to the Java snippet above, and you begin to see how many fewer lines of code it requires to accomplish essentially the same task.

Toolchain

When approaching a language for the first time you will undoubtedly ask yourself "Ok, how do I get started?" Sometimes you may need to hunt down every last part of your toolchain. C and Java development, depending on your environment, can be as simple as opening up a good Integrated Development Environment and moving forward from there.

Sometimes the IDE is too limited in its scope or output, forcing you to find your own compilers and debuggers which can be daunting if you don't know where to begin. Several modern languages include Read-Eval-Print-Loop tools, or REPLs, to allow experimentation and one-off commands within a runtime and even within an existing codebase. Elixir comes with an entire application development pipeline, including project generation, a REPL, library management, compilation, and a simple testing harness, built into the core tooling of the language.

Testing can be key when building high quality software. On one hand, a good testing tool allows you to create simple ways of verifying your own assumptions about your code, but in a language where testing is core to the ecosystem, testing can become an excellent way to learn how to write idiomatic code that is readable and performant.

Framework

Currently, many language runtimes consist of an interpreter and a virtual machine to allow for true portability across a multitude of platforms. While this can make your job as a programmer very easy, it must still be an important consideration when deciding how to run scalable production applications. In many cases the onus will actually fall on the programmer to find the good parts of the language framework and program defensively around the rougher parts.

Java, for instance, runs in a virtual machine with a garbage collector. Instead of managing memory manually like C and C++, the JVM will keep track of allocated objects and when they are no longer in use the garbage collector will go through and clear the memory for reuse. If many objects are released from use in a single garbage collection cycle, slowdown may occur which could impact the user's experience of your application, and so veteran Java programmers know to manage objects in ways that result in less impactful garbage collection.

It's easy to imagine that runtime interpreters can result in slower execution, but good interpreters are usually built so that idiomatic code will result in better performance. For instance, when performing computationally intensive tasks statistics show that Python code will perform at least 2 times slower than the same operation in natively compiled C. Contrast this with a Phoenix web server handling nearly 2 million simultaneous web socket connections.
~2 million!
Since Phoenix is a very idiomatic piece of Elixir software running on the Erlang VM, which is purpose made to handle as many simultaneous tasks as you can throw at it, it performs almost magically well. This shows how important picking the right tool for the job can be.

Perhaps a lesser considered aspect of framework, the community surrounding a technology greatly affects how you feel about it. When the people surrounding a language are knowledgable, passionate, and supportive it can be infectious, and when it's hard to find enthusiasm or even answers the language quality suffers.

Conclusion

It seems that enjoyable is not really a clearly definable term when it comes to a particular programming workflow. Languages come along with intentions and tools and frameworks which could completely alter how you use them, and your level of enjoyment with a language would likely stem from a combination of factors. First of all, are you attracted to the syntax? How close is your intention to the main intention of the language? How often will you need to fight against the syntax or framework to accomplish what you want? More so, when you do need to bend a language to your will, do you feel clever or as though you just built an unmaintainable rat's nest?

The tools, of course, play a major part since without the compiler all you're doing is writing a highly organized text file. At a deeper level, how well do your tools fit together and help you along your development process? How intuitive is it to invent your own tools and release your final product to the world?

I have no doubt I will continue analyzing and maybe come up with some better metrics for my own experiences, but I'd also be interested in learning about other takes on how the language ecosystem affects peoples enjoyment of the actual work.