top of page

On compilers & their psychological superpowers

23 sep 2019

5 min. leestijd

1

7



I often think about the idea that there is only so much you can care about. Some people describe this as the amount of fucks they have to give, which is usually a finite amount. As the years in my (short) career go on, I’ve increasingly felt the relevance of this concept in software development.

While they are only part of the puzzle, I believe that compilers and type systems can offload a significant amount of concerns that take up valuable time and energy. Instead of having to think about everything that can go wrong, they let you focus on building features and adding value to the product.

To make this theory more concrete, I’ll be talking about my favorite programming language: Elm. It is by no means the only language that can illustrate my point, but I am comfortable with it and believe that it will aptly show what can be gained.



Building blocks


Somewhat surprisingly, the possible reduction in mental strain can already be seen in Elm’s most basic building blocks.

The first thing you have to do when writing a new function is coming up with its type signature.

multiply : Int -> Int -> Int  
multiply a b = a * b  

This type signature says that multiply takes two integers and returns an integer. Thanks to the compiler, you can be completely sure that when this function is called, that’s exactly what will happen. If anyone tries to misuse it, like giving it a float instead of an integer, it will be caught at compile time.

— TYPE MISMATCH —  
The 1st argument to `multiply` is not what I expect:  
7 | multiply 2.5 2  
This argument is a float of type: **Float**  
But `multiply` needs the 1st argument to be: **Int**  

Note: Read this to learn why Elm does not implicitly convert Ints to Floats. Use toFloat and round to do explicit conversions.

Not only will it be caught, the compiler will tell us what is wrong, what we can do to fix it and (sometimes) even why it behaves that way, all in a friendly and clear message.

By front loading important questions and having the type system and compiler to back up your decisions, you don’t have to think about a whole category of possible problems afterwards. No more “What if one of the arguments is null?”, “What happens when George accidentally sends it an array?”, “What is the type of the output”?, not even the whimsical “Will it always have the same type of output?”.



A case for iteration


One part of software development where compilers and type systems can really shine is during refactoring. I think most people would agree that being able to iterate often and without fear can play a big part in successful development cycles.

Let’s say that we have a simple custom type called Vegetable that can be either a Carrot or a Cucumber and a transformer function that uses a case statement to turn a Vegetable into a String.

type Vegetable  
              =  Carrot  
               |  Cucumber  

vegetableToString : Vegetable -> String  
vegetableToString vegetable =  
                        case vegetable of  
                                   Carrot -> "Carrot"  
                                   Cucumber -> "Cucumber"  

We notice, however, that our Vegetable type is quite limited, so we quickly add another one to it.

type Vegetable  
            =   Carrot  
            |    Cucumber  
            |    Cabbage

When we try to compile our code, the compiler will not be happy because we haven’t added a branch for Cabbage to our vegetableToString function. Thankfully, it’ll guide us towards fool proofing our program once again.

— MISSING PATTERNS —  
This `case` does not have branches for all possibilities:  

13|  case vegetable of  
14|  Carrot -> "Carrot"  
15|  Cucumber -> "Cucumber"  

Missing possibilities include: **Cabbage**  
I would have to crash if I saw one of those. Add branches for them!  

Hint: If you want to write the code for each branch later, use Debug.todo as a placeholder. Read this for more guidance on this workflow.


Safeguards like these can take away a lot of uncertainties that come with refactoring and allow you to make sweeping changes without batting an eye. Sometimes it may feel like you are fighting the compiler, but if you look at it from a different perspective it’s more akin to a designated driver: a shining force of reason guiding you through the chaos.



Opening the door to strangers


Taking it one step further, let’s imagine that our vegetable comes as the result of an HTTP call to a server. We know what the response should look like, but since we don’t know when that might change and keeping in mind that our request can always fail, we’re not guaranteed to get what we asked for.

In a lot of languages this is handled by the null concept, which thankfully does not exist in Elm. Instead, it has the Maybe type (a.k.a Option(al)), which forces you to always consider that there might be nothing to work with.

vegetableToString : Maybe Vegetable -> String  
vegetableToString vegetable =  
             case vegetable of  
                          Just v ->  
                          case v of  
                                       Carrot -> "Carrot"  
                                       Cucumber -> "Cucumber"  
                                       Cabbage -> "Cabbage"  
                          Nothing ->  
                                        "Unknown"  

By changing our vegetable’s type to Maybe Vegetable, we must deal with the fact that it might not be there. Once we do, it’s business as usual. Of course, it’s not advisable to use Maybe all over the place, but when it’s necessary it can be a powerful ally.

You’ll probably run into Maybe quite early in your Elm experience, as it is used to solve problems such as using an index to retrieve an item from an array that may or may not exist. It may frustrate you, it adds boilerplate and it feels weird at first, but it frees your mind of an enormous burden. Let’s appreciate that for a moment: a world without null and undefined!



Nobody cares


Of course, not everyone will care. In fact, looking at what’s currently popular and what I’ve personally experienced, most people don’t.

Some simply don’t think about software development that way. They never experience all the uncertainty, unknowns and what-ifs eating away their productive thoughts. It’s as if someone in a meeting room shouts “Isn’t that noise driving you crazy?!” and they respond with “What noise?”.

Others might argue that the problems and concerns eliminated by type systems and compilers are not significant and that they could either just fix those problems when they occur, or proactively defend against them.

However envious I may sometimes feel of these types of people; this post is obviously not for them. If you’re anything like me, you do often experience those worries and fret about everything that could go wrong.

Whatever the case may be, I sincerely hope more people will start seeing the value of clear and friendly error messages, great type systems and fantastic compilers. I truly believe we could be building better software and having less frustrating experiences as developers, even though there is currently little financial incentive to do so.

So, should you ever feel like you’re running out of fucks, just know that there are tools out there to help you recover some. Even if you don’t, you might be surprised by the different perspective they can offer and the shift in focus they might provide.

While most people don’t care, it can still be worthwhile to explore what’s out there. Be warned though, it may be hard to go back.

23 sep 2019

5 min. leestijd

1

7

bottom of page