Lambda To The Rescue: Syntax

This is the fifth post in a series about using Functional Programming concepts to make your Object Oriented code more comprehensible. Start here if you want to read the whole thing.

✨ Syntactic Sugar

When I was first reading Learn You A Haskell For Great Good, I came across the term Syntactic Sugar and I was confused... My background in OOP languages made me cringe a little when I read about how you can create your own syntax in Haskell using infix operators and DSLs. It even goes so far that some functional languages have almost no syntax at all! Let's look at an example of "sugar":

map reverse
    (filter (\x -> length x < 5)
            ["foo", "bar", "baz", "qux", "ramsam"])

We're taking the list ["foo", "bar", "baz", "qux", "ramsam"], filtering out items that are 5 characters or longer, and reversing them all, one by one. The result is ["oof", "rab", "zab", "xuq"].

We can make this a little bit more readable by using Haskell's $ infix operator, which allows us to remove some of the parentheses by evaluating the expression on its righthand side first:

map reverse $ filter (\x -> length x < 5) $ ["foo", "bar", "baz", "qux", "ramsam"]

Although the parentheses are gone, we still have to read from right to left if we want to know what exactly the result of this will be. We basically pipe the result of the expression on the right of the $ to the expression on the left side of it. Some languages like F# have the |> operator which allows you to pipe from left to right, so the exact opposite of the $ operator. It's not idiomatic Haskell, but in some cases we might want to use it to create some clarity.

Since we know |> is the exact opposite of $, let's just try to write it ourselves. In Haskell, infix operators are just normal functions with two parameters. Here's an example:

*repl > 2 + 3
5

*repl > (+) 2 3
5

The function (+) can be used as a normal Haskell function, writing the function first and its arguments after it, or as an infix operator, without the parentheses. So we know ($) is a function taking two parameters, and we have flip which takes a function with two parameters, and flips the order of them!

(|>) = flip ($)

Easy! Let's now write the original example using (|>):

["foo", "bar", "baz", "qux", "ramsam"]
  |> filter (\x -> length x < 5)
  |> map reverse

We created our own syntactic sugar! We can now read this left to right, or top to bottom as you will. The result of the first expression you read will be "piped" to the next as the last parameter! We get the exact same result ["oof", "rab", "zab", "xuq"].

💬 Domain Language

As you saw in the last example, we can create our own infix functions in Haskell. This can be very useful to create more readable code, using the language of the domain. Let's see a typical example of a function that doesn't do that:

changeAddress :: Client -> Address -> Client

Now when we use this function in the classical way, it looks like this:

let abbeyRoad3 = "3 Abbey Road, London NW8 9AY, UK"
let updatedClient = changeAddress client abbeyRoad3

which doesn't read very well... What if we could use an infix here?

let updatedClient = client `changedAddressTo` abbeyRoad3

Using backticks we can use a function as an infix operator. The only thing we needed to change was the name of the function, and now this looks like a sentence!

client changedAddressTo abbeyRoad3

This is now the function type:

changedAddressTo :: Client -> Address -> Client

So a simple changed lead to a better understanding of the code.

📉 The Absolute Minimum

In the previous examples, you could see how we can mold the existing syntax to our likings, and use that to create very understandable code. Now let's see what happens when you take a language that has almost no syntax at all, like scheme. Literally, the only syntax in scheme is ( and ). The rest are functions. Let's check out some code:

(define sum
  (lambda (list)
    (cond
      ((null? list) 0)
      (else (+ (car list) (sum (cdr list)))))))

As you can see, there are lots of (s and )s. Basically, every () combo is a function call.

  • (define name body) defines a variable. In this case, it's called sum.
  • (lambda (arguments) body) defines a function. In this case, it has an argument called list.
  • (cond (condition result1) (else result2)) creates a conditional. If the condition is #t (true), we return result1, else result2.
  • (null? list) checks if a list is empty.
  • (+ a b) returns the sum of two numbers.
  • (car list) returns the first item of a list (the head of the list).
  • (sum list) is the recursive call to our own defined function.
  • (cdr list) returns a list without its head (the tail of the list).

Almost all of the above functions can be defined in scheme itself. That's why you'll find a lot of scheme parsers or environments written in scheme. Even the datatype of a list can be written as a function! And it doesn't end there! Scheme has support for macros which will let you manipulate your own code!

🤷‍♂️ What's the point?

My point isn't that you should try to minimize the amount of syntax to have a good language, but I'm always looking for better ways to write stuff. This means that languages where you can create your own syntax have a high appeal to me. I love to read code that tells a story, so that my brain can try and understand the problem & the solution at hand, instead of the code. I think a lot of functional programming languages have strong support for writing code like this, compared to most OOP languages.

That doesn't mean we can't apply a lot of this in our day-to-day OOP codebases. See what you can do to make your code more meaningful. Can you give your classes, functions and variables better names? How can you lower the cognitive overhead needed to understand the code? How can we bring developer time down? Experimenting with this is something we can do in all languages, but it gets easier if we try and look around to the other languages and ecosystems around us! Don't get stuck in the idioms of the language you're spending the most time with, try stuff, look around, conquer the world!

It's also worth mentioning that there are whole communities around "designing software according to the domain" and "using the language of the domain". If you want to learn more, check out Domain Driven Design! I've been going to the DDD Europe Conference and DDD Belgium meetups for a few years now, and I must say, a lof of DDD people are trying functional languages!

That concludes this post. See you in the next Lambda To The Rescue post! Happy programming y'aλλ! 🖖

Categories: Functional Programming