← Blog ♘

Reflection on a few programming languages

For the past many years I have been teaching myself new programming languages fairly frequently. This blog post is meant as a reflection on a few of my favorite languages and what they have taught me about programming in general.

A lot of it comes from accepting the language as it is, and not get tied to a certain way of doing software. To understand a language, which really is an expression of how the creators believe programming should be done, it has been important that I let my own ideas rest for a bit and just go with whatever was being presented to me. Accept it (for now) and really, honestly, try to follow the patterns and the ideas. I have become a better programmer by doing this, and I believe everyone would be wise to adopt the same attitude. You will see contradictions below, and that is exactly the point of it all.

JavaScript

function divide(a, b) {
    if (!b) throw "Divide by zero"
    return a / b
}

Even though I work almost exclusively in languages that consider JavaScript a compile target, I still really enjoy going back to basics and write some raw JavaScript. No compilation, just :w alt+Tab ctrl+R.

JavaScript was my first real language and I honestly believe it was a good one. It has zero restrictions on how to write code. It doesn't force you into writing OOP or functional or anything. So it is perfect for exploring areas of programming without committing too hard to any particular style.

What matters, in terms of JavaScript, and where I think most people go wrong with the language, is that you need to keep whatever it is you are doing small. Preferably less than 1000 lines. Or one file. JavaScript should live in the browser, and the code should be understandable by one person in an hour. If it is bigger than that, then in my opinion you got the wrong language. TypeScript, Elm, and ReansonML are all better suited for larger code bases.

When you have the whole thing to yourself, and everything will be cleaned up in two minutes when the user is done with your page, then the game is a different, and JavaScript makes sense. Global variables by default, coercion everywhere, limited set af data structures, no integers. It all makes sense, because you should not be doing things that need to be correct nor correct anyway.

Rust

fn divide(a: f64, b: f64) -> Result<f64, &'static str> {
    if b == 0.0 {
        return Err("Divide by zero");
    }
    Ok(a / b)
}

Rust wasn't my second language. Nor was it my third or fifth. It also took me three attempts to even get started. What I love about Rust isn't so much the lack of garbage collector, but the team's and the community's relentless focus on correctness.

As an example, in JavaScript all you have are floats, in Rust, the default .sort() won't sort arrays of floats because NAN < 0 == false and NAN >= 0 == false, so we can't talk about a total order of floats.

I love this mindset (some call it a religion) about Rust because it forces me to learn about subjects I did not know existed. It shows me where the rabbit holes are. Usually I can work around them when I run into them and then get back to them at a later point. Every time I do, I feel I come away as a more knowledgeable person. One I answered yesterday, was why are we using something called a usize for indexing instead of, say u32? Another one I dug into the other day, is about why paths in Rust are an actual type, distinct from String (hint: They aren't strings).

I write all this, because I find it fascinating that what I on some level find a detriment to the language (that things aren't simple) actually is a win for me as a student of computers. I studied many of these complexities, and I have learned a lot.

Another aspect of Rust that isn't something I discovered with Rust per se; I already knew it from Haskell and OCaml, is that a very strong type system with type inference and without null is desirable. This was confirmed in working with Rust. I have never been angry at the type checker for yelling at me. Why wouldn't I want a friendly helper pointing out my mistakes? If I was building anything critical, Rust would be the weapon I'd pick.

What is interesting here, is not so much how great (or not great) Rust is. What is interesting, at least to me, is that I find whatever I am saying here, to be true for all programming languages. Yet it contradicts so much of what I love about JavaScript.

Go

func Divide(a, b float64) (float64, error) {
	if b == 0 {
		return 0, errors.New("Divide by zero")
	}
	return a / b, nil
}

Speaking of contradicting. Go broke all the rules I thought I knew, and it didn't even apologize. It did not occur to me that a language could lack generics. Now I hope they never put them in.

I still remember my first program written in it. A friend needed a program that would take some input text files and produce a third. I had been reading about and playing with Haskell a lot, and this sounded like a task that would be perfectly suited for a pure language. Data in, data out. This was going to be my very first real program written in Haskell!

After about an hour of trying to get something meaningful going, I gave up and wrote my very first Go program instead. It took me about half an hour. Compiled it. Shipped it. The few extra features I had to add a year later was a breeze. That, to me, is what Go is all about.

I love Go because it doesn't care about your fancy type theory. It doesn't want you abstract data types or your monoids. It just wants if/else , while loops and functions. Oh and first class support for concurrency. It aims to get you those basic building blocks, and then omit the rest. You can't do fancy stuff, because there is nothing to do fancy stuff with. Just build what you need to build, and move on.

It was with Go, that I first realized that a language really is just an interface to the internals of the machine. I build interfaces to modules every day, so I know first hand how important it is to keep the interface small. But if languages are just interfaces, then why aren't all languages small? And why aren't they formally defined before they are implemented? At least for me, defining the interface is the first thing I do.

I used to have many ideas about what Go should include in the language, but this language is built by people better at this than I ever will be, so at some point I just sat down and started listening instead.

Elixir

def divide(_, 0), do: {:error, "Divide by zero"}
def divide(a, b), do: {:ok, a / b}

Elixir (and Erlang) turns everything up side down once more. Suddenly we aren't talking about creating correct code anymore. We are talking about knowing there will be bugs, and handling them gracefully. It's about introspection into running systems in production and fixing it while it is running.

It's a completely different mindset that doesn't get the credit it should. When reviewing what we know about building resilient systems (I recommend this talk), then, at least as far as I can tell, Elixir gets it exactly right. You need it to be introspective and open to change. It needs to help you understand what is going on, rather than try to hide the details and discourage you from tinkering. Because things will go wrong, and things will need tinkering. This is troublesome, because very few programming languages work that way. I don't even know how you'd begin interfering with running Go/Rust/JavaScript code.

There is also a notion that less code is good. Small code is good. The less code, the less can go wrong, of course, but also to my earlier point in JavaScript, less code is just easier to read and understand. Because of this, you see extreme examples of generalizing code and concepts. For instance, the awesome GenServer that helps you keep state manageable in a different process (green thread).

Of course, to make small code very generalized, you end up having to expose the internals so that they can be changed as needed. Even to the point of modules not being modules, rather than just "good ideas" on how to do something. Again, this is completely opposite how Rust does it, and how Go does it.

My last point on Elixir, is that I have this pet project that I build in languages that I am learning. It is essentially the backend for an RSS reader. It's a good task because it needs a web server with authentication and it needs to, periodically, go and check websites for new posts, parsing an obscure old format with all the errors that can come with that. This project is very real for me, because I use and abuse this system daily. Despite Elixir being the language I have the least experience with of the ones mentioned so far, this is the language that the current version running in production is built in. It has never been down (even for deploys), and I am never afraid to open up the code and add new features. I love that project and I love Elixir.

Conclusion

I hope I managed to convey how these languages are different and have their differences, without being necessarily wrong. They each bring something valuable to the table, and they well worth learning and understanding in their own right.