Semantics, Not Syntax; Developer empowerment using functional-first programming
Tags: fsharp, functional, typescript, rust
Published: 2021-11-29 16:35:42


**_"It's not about syntax; it's about semantics."_ - [Richard Feldman](https://twitter.com/rtfeldman)**

This article is just a collection of my thoughts concerning my favourite languages and why I enjoy them. For the most part, I think software developers operate like artists; our attachment or reluctance to different technologies is heavily influenced by recency, emotional connection, and personal association. We like the things we like, not necessarily the things that are "correct" if there is even some correct to be.

However, in recent times I've seen a few languages spark joy for myself and other developers, and I have spent some time contemplating why that is the case; what makes these (seemingly disparate and unrelated) languages all seem to inspire the same type of zeal and interest in their users.

That seeming disparity is essential. Rust, Elixir, f#, and Go could never be mistaken for each other, yet their advocates' _emotional_ response feels familiar. And in between the various quirks of function definition, platforms, object definitions, etc., there seems to be some more fantastic design ethic that draws people in.

So I'd like to surface some of the ones that I've noticed and maybe explain a bit of _why_ I think they matter to us.

Note: I'll be using examples from a tiny [implementation](https://github.com/tkshill/FSnake/blob/master/src/Snake.fs) of the Snake Game I wrote in [F#](https://fsharp.org/) here because the language exemplifies pretty much all the things I'll be speaking about today. Also, I like it.

## Immutability as Default

![gif about hating changes](https://media.giphy.com/media/JSjiaBMFpyrQ66VfdP/giphy.gif)

If I had to put forth the single most powerful of these language semantics to influence and improve the type of programs we write, it would have to be the *decision* to make variables **not** alterable by default after initialisation. The concept of explicit mutation seems so fundamentally baked into what programming means that the idea of working without it seems inconceivable. What does it mean to program with it? 

To the computer, not much. Under the hood, languages that leverage immutability are the same variables and spaces in memory that we're used to. But to the developer, it's a **big deal** to be able to make guarantees that data can only ever be what you defined it to be the _first_ time. And if you want something new, you can use the first thing as a template for the new thing, but they are not the same.

Let's look at an example.

```fsharp

[<StructAttribute>]
type Game = { Food: Food; Snake: Snake; Size: int; Status: Status }

let advance game =
    match game with
    | AlreadyOver -> game
    ...
    | EatsFood ->
      let newSnake = updateSnake game.Snake true
      let newFood = createFood game.Size (newSnake.Head :: newSnake.Tail)

      { game with Snake = newSnake; Food = newFood }
```

Here we have the core data type for our Snake implementation, the type `Game`, a record/object with fields Food of a kind `Food`, Snake of type `Snake`, Size of type `int` and Status of type `Status`. We'll learn what those types are a little later in the article, but what I want to focus on right now is a snippet of the `advance()` function shown below.

`advance()` is a function that accepts a game and returns a game. I've trimmed away most of the implementation but kept the portion where advance has determined the snake has eaten a piece of food.

Let's look at the order of operations:
- `let newSnake = updateSnake game.Snake true` is used to create a new snake based on the state of the old one.
- `let newFood = createFood game.Size (newSnake.Head:: newSnake.Tail)` creates a _new_ piece of food by passing in the size of the grid and our new snake.
- Finally we return `{ game with Snake = newSnake; Food = newFood }`. Now, this _looks_ very much like a stateful update. It is changing the game fields to these new values. But what it's doing is _making a new record_, using the values from the old game, but with these new changes.

The old game was unmodified. The game returned is an entirely different value. But **the semantics of the language make it cheap, efficient, and sensible to produce new values.** So we don't have to worry about actions later accidentally mutating previous values.

It's essential to think about this last part. It's not that we _can't_ program like this in other languages. It's just that their semantics make it less worthwhile. It's harder to track when you're mutating or not. Idiomatic methods and functions in those languages mutate. There could be a performance overhead for making new values too often. These are all *semantic barriers* to using immutable values and disempowering the developer from this programming style, leading to more precarious code.

In languages like F# and [Rust](https://www.rust-lang.org/), the `mutable` keyword is an intentional indicator to the language that you intend to modify a value. In languages like [Elm](https://elm-lang.org/), you cannot do mutation at all. But either way, it makes the programmer much more _thoughtful_ about how they change states in their code. And in the field of software development, **thoughtfulness matters***.

## No Default Nulls

![gif of being disappointed by nothing](https://media.giphy.com/media/ZCCERtxEH01ksB2tXu/giphy.gif)

I won't spend too much time on this one since many, many people have expounded on the [dangers](https://hinchman-amanda.medium.com/null-pointer-references-the-billion-dollar-mistake-1e616534d485) of [null](https://www.lucidchart.com/techblog/2015/08/31/the-worst-mistake-of-computer-science/) [values](https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/).

Suffice to say, it is difficult to **trust** types, typing, and functions itself in languages that do not or cannot guarantee that the function will return the type you expect, and perhaps more importantly, do not enforce that you write operations that _return_ the types you say they do.

It's OK to have a function that returns `Some value` or `Nothing`. That's a semantically correct logical operation. Sometimes things fail. What's **not** fine is if language _inserts_ a null value because you _forgot_ to return a value at all operation paths in your function. It's hard to write and use code that you cannot trust. It's hard to read and follow docs if every function can _not_ return the value it's supposed to.

```fsharp
  let changeDirection game proposedChange =
    match proposedChange with
    | Perpendicular game.Snake.Direction ->
      { game with Snake = { game.Snake with Direction = proposedChange } }
    | _ -> game
```

This function, `changeDirection`, is responsible for changing the way the snake is moving. It has some guard logic for making sure the snake's direction can only change _perpendicularly_. A snake moving up can either go left or right, but it can't, for example, reverse back into itself.

`| _ -> game` is the default case for our match (switch) statement where we return the game that came in unchanged. And F# will complain if:
- We forget the default (or fail to account for all possible shapes of the input)
- We return anything but a game from this function at any point. It won't compile unless we _tell_ it that this function could return a game or something else. But then, everywhere we call this function, we would have to deal with the fact that it _might_ return a game or it might not. All our inputs have to match our outputs.

And that means if I say a function returns an `int`, the language itself will ensure I'm not lying, and I'd rather not be a liar.

Almost every language I know created in the last decade does not have default nullability on its functions and objects. What was meant as a convenience turned out to be a detriment, and it turns out developers prefer living without it.

## Brevity

![gif of a short person being annoyed](https://media.giphy.com/media/X9jbyxzhco1JmR2AsR/giphy.gif)

I have _many_ thoughts on brevity. So many thoughts. I can't write them all because there's something wrong with not being brief about being brief.

So, briefly,

Programming languages and paradigm popularity ebbs and wanes. But nothing truly goes away. In the 90s and aughts, we saw C#, C++, and Java in maybe their heyday as the language _du jour_ of software development. Many times, it has been posited that the rise of _dynamic_ languages like python, ruby, and javascript was a direct response to developers feeling the friction of the overhead of the enterprise languages.

Some people think that this was a resistance to the rigidity of static typing. Developers wanted more freedom and spent less time "type wrangling", opting for performing actions over defining structures.

I think that's _part_ of it, and not all of it. Specifically, I don't believe the types were necessarily the problem, but more like collateral damage from incredibly verbose language syntax.

Curly braces, accessibility modifiers, semicolon, semicolon, semicolon, and always everywhere type definitions made for an intimidating syntax for new developers and seemed to add to laden the burgeoning developer; the better you understood the language, the more code you seemed to have to write to express yourself.

```fsharp
  let private opposite = // Direction -> Direction
    function
    | Up -> Down
    | Down -> Up
    | Left -> Right
    | Right -> Left
```
Here's a private function in F# that returns the opposite direction. F# does not make explicit return statements in its functions; everything is an expression, so the function's body is a valid return. Indentation handles defining the boundaries of the function body. Newlines define the following case in the switch. Arrows (->) separate cases from results. _Mise en place_.

Languages like python, F#, and Rust, in contrast to the older iterations of the enterprise languages, do their best to eliminate superfluous syntax, verbose symbols and overly elaborate exposition for every construct. They embrace whitespace as syntax; an idea which arguably does not make that much sense for compilation, but makes a massive difference for **human readability**. Code people can read and parse is lexically succinct.

By and large, languages are getting briefer and more expressive, relying more on intuitive whitespace for scoping.

And as for the question of verbose type definitions:

## Type inference

![gif of two men asking if you're psychic](https://media.giphy.com/media/hTerI4FpPtJg4/giphy.gif)

As a direct continuation of the pattern of brevity discussed above, recently we've seen the emergence of **type inference** (the ability for a language compiler or runtime to determine and _enforce_ types based on usage).

All the F# code I've shown you so far has been fully/strongly typed. Every function parameter and function return has been deduced and enforced by the type checker.

Some tools, like the VSCode [Ionide](https://ionide.io/) extension take advantage of this and will display the types **for you**.

![image of the code with type hints](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mzeapno1rna3250q19fs.png)

All the type comments you see here are overlaid on to the code. They're not actually being written in the file.

It's hard for me to return to dynamic languages when I know I can get all the benefits of strong types and compile time guarantees without having to explicitly write all the type information.

Safety meets brevity. Admittedly, type inference isn't perfect, and you lose context if you're reading the code outside of an optimized editor experience, but at that point, it's still no **worse** than if the code was dynamic, and you still have the knowledge that all the logic is type safe and checks out.

I never personally felt the slow down that dynamic programming enthusiasts have mentioned comes with using static types - I think I write **working** code faster with strong typing - but if you are concerned with speed and expressiveness, type inference seems like an excellent way to mitigate it. 

## Abstract Data Types

![gif of being impressed](https://media.giphy.com/media/krtDmVuQGssOtMWfRg/giphy.gif)

We've reached the final pattern I want to discuss here and I feel like I've saved the best for last; at least for me, it is my personal favourite of all the things we've discussed here and the one that has had the greatest impact on my progression as a developer.

[Algebraic Data Types](https://guide.elm-lang.org/types/custom_types.html) aka Custom Types aka Union and Product Types are a relatively straightforward concept with profound applications.

Ultimately, programming is giving instructions to a machine to perform meaningful work. And **modern programming involves _making abstractions_ that produce maintainable code that performs the behaviours we desire.** Values, functions, classes, modules and all these other namespaces allow us to define constructs and ideas that map the *real* domain of our endeavours to a program space of data structures and logic.

Algebraic Data Structures (ADTs) provide a straightforward syntax for expressing the shape of a problem with as little overhead as possible.

Let's see how.

```fsharp
type Game = { Food: Food; Snake: Snake; Size: int; Status: Status }

and Snake = { Head: Head; Tail: Tail; Direction: Direction }

and Head = Position

and Tail = Position list

and Food = Position

and Position = int * int

and Status =
  | Active
  | Won
  | Lost

and Direction =
  | Up
  | Down
  | Left
  | Right
```

These are the types representing the domain of my Snake game. The concepts, so to speak, that are meaningful to the idea of snake. What _is_ a game of snake? 

**What is the minimum amount of information necessary to play a game of snake?**

There are a few interesting things happening here:
- The data structure for the game is composed from smaller structures
- We can easily alias types (give them a more semantically meaningful name that's relevant on the context of our application). Like how `Food` and the snake's `Head` are both just positions, but we can use their aliases throughout our code for more clarity.
- Status and Direction are both [Union types](https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html). They're similar to enums, but they're not integers or strings under the hood. They're fully qualified Values that we can use in our code, like making our own primitives unique to this application.

You might not find this particularly exciting, saying these are _just_ fancy enums and records, but ADTs are fully unencumbered by shape:

```fsharp
type Message =
  | Restart
  | Dir of Direction
  | Tick
```

Here we make a `Message` type that has two simpler values, `Restart` and `Tick` that don't rely on any other data, and one parametrized value, `Dir` that requires a `Direction`. 

```fsharp
let update game msg =
  match msg with
  | Restart -> Game.init 10
  | Dir direction -> Game.changeDirection game direction
  | Tick -> Game.advance game
```

When we consume this data type we can make decisions and access the associated data with each value, but **not** mix them up. We can't use the direction in any case except the `Dir direction` case, because directions only exist on that value.

This allows us to _precisely_ model domains without wastage. It is not a trivial operation to express something like that message type in a language like Java, and requires significantly more code to do so. As a consequence, people rarely do it, opting to use more mutation, and nullable values to handle states where data is lacking, or shouldn't exist.

And that causes more bugs.

**We shouldn't write code that squeezes our real-world domain into the primitive types of our programming languages; our programming languages should provide the tools to represent our domain precisely and without wastage.** The better the representation, the easier it will be to work with the data.

ADTs are now a first-class feature for me in any language I want to use. The more resistance a language gives me to describing what a thing _actually_ is, the less I find myself wanting to use it.

## Final thoughts

If you've made it here, thank you for taking the time to read my little love letter to the patterns I enjoy, and why I think other developers enjoy them as well.

We've gone this whole way without me mentioning functional programming, and that's deliberate. While almost all of these patterns saw their origins and notable iterations in the functional programming space, I've recently found myself moving away from attempting to separate the world into functional or not functional; I'd rather talk about patterns that I like, and tools that implement them.

F# itself has recently done the same with it's updated tagline:

_**F# empowers everyone to write succinct, robust and performant code**_

The goal is not to be functional or object oriented. It's not to be the most popular language or the fastest language.

It's to help people write good code.
It's to help developers express their desires.
It's to avoid bugs, and errors.

Languages that execute well on these ideas, seem to be well received. And it's not just the new languages. All the older tools and frameworks are thinking about developer empowerment. 

**It was never really about semicolons.** The things we've just talked about aren't recommendations or suggestions in style guides. They're not buried in tomes like, "Everything you need to know about X". They're baked into the language ecosystems themselves.

Let me know in the comments what you think, and what language patterns bring you delight.