DEV Community

Benji Stephenson
Benji Stephenson

Posted on

Make Expressions, not Statements

I was chatting to a colleague about Haskell the other day and some of the qualities it has, such as everything being an expression and how that can encourage the use of smaller functions.
The drive to use expressions over statements is something that I've advocated for for quite a while at work as I think it has a strong set of benefits over merely making statements. Although not all languages make this a first class citizen, it's still usually possible to do.

TL;DR at the bottom

I'll use a couple of languages, but let's start with Typescript. Say we want to get the age bracket of an individual, based on their age range. We might do something like:

var ageBracket: string

if (age <= 12) ageBracket = "junior"
else if (age >= 19) ageBracket = "teen"
else ageBracket = "adult"
Enter fullscreen mode Exit fullscreen mode

So what's could we say is a problem with that? We could wrap the logic up in a function and avoid the mutability, but I think the aspect that we're still dealing with statements remains. A statement is a bit of code that does not return a value and so can't be chained very easily. An expression on the other hand returns some value and can easily be built into a larger expression to achieve something more complex.
Scala is a more expression based language and so allows us to do things like:

val ageBracket = if (age <= 12) "junior"
  else if (age >= 19) "teen"
  else "adult"
Enter fullscreen mode Exit fullscreen mode

One mode of thought is that statements describe how to do things, expressions state what to do. Here's another example:

const nums = [1, 1, 2, 3, 5, 8, 13]
var total = 0
for (var i = 0; i < nums.length; i++)
  total = total + nums[i]
Enter fullscreen mode Exit fullscreen mode

It's quite low level in that is describing how to loop through an array of elements and then compute a running total. The for statement also does not return anything, if we wanted to do something afterwards, we must do so with a new statement. Compare with:

[1, 1, 2, 3, 5, 8, 13].reduce((total, n) => total + n, 0)
Enter fullscreen mode Exit fullscreen mode

In Scala we get something ever more pleasant:

List(1, 1, 2, 3, 4, 5, 8, 13).sum
Enter fullscreen mode Exit fullscreen mode

or

List(1, 1, 2, 3, 4, 5, 8, 13).fold(0)(_ + _)
Enter fullscreen mode Exit fullscreen mode

Great, so what? Let's add a couple more example steps, again in Typescript, well filter for even numbers, double them and then get the word for the number (2 => "two"):

const nums = [1, 1, 2, 3, 5, 8, 13, ...]
var odds = []
for (var i = 0; i < nums.length; i++)
  if (nums[i] % 2 === 0) odds.push(num[i])

var doubles = []
for (var i = 0; i < odds.length; i++)
  doubles.push(odds[i] * 2)

var names = []
for (var i = 0; i < doubles.length; i++)
  names.push(getNumberWord(doubles[i]))
Enter fullscreen mode Exit fullscreen mode

We end up with quite a lot of intermediary variables. If we change the functionality we need to keep track of whether we should get rid of them or not, IDEs help with this of course, but it can still be pretty easy to end up with unused code lying around. Compare with:

[1, 1, 2, 3, 5, 8, 13, ...]
   .filter(n => n % 2 === 0)
   .map(n => n * 2)
   .map(getNumberWord)
Enter fullscreen mode Exit fullscreen mode

I find it to be much more descriptive and declarative, we talk about what it is we want the code to do and don't have to be concerned with the how.

Related to the intermediary variables point, expressions also allow us to more tightly group related bits of functionality or logic together. To go back to the sum example:

var total = 0
for (var i = 0; i < nums.length; i++)
  total = total + nums[i]
Enter fullscreen mode Exit fullscreen mode

Here the initial value for total and indeed the logic to do the addition is free to be moved around, away from the loop implementation. Not implicitly a bad thing but it does make code harder to reason about when it's spread apart. What if we also forgot to initialise total? Depending on the language, we could get an exception or we could get behaviour we didn't expect. Javascript isn't as well behaved, but in the Scala version

List(1, 1, 2, 3, 4, 5, 8, 13).sum
List(1, 1, 2, 3, 4, 5, 8, 13).fold(0)(_ + _)
Enter fullscreen mode Exit fullscreen mode

it's impossible to forget the initial 0 value for the fold as the compiler would yell at us and in the sum version it's implicit.

Obviously these are really contrived examples but generally I think they illustrate the point. When we have statement orientated code we usually end up with less of a set of , declarative building blocks and more of a longer set of lower level instructions that are much harder to compose.

There's a great talk by Paul Louth, the writer of the C# functional programming lib language-ext where he talks about statements vs expressions that's well worth a watch., he talks about much more than I'll go into here. I love his examples (around 25:30) of how statements are much more open the insertion of side-effecting changes in a more uncoordinated way, whereas the expression based approach forces you to be more structured and deliberate.

I'm a big fan of FP and FP languages, but lots of the patterns and practices that I think make them great can be applied to other languages too and this is definitely one of them, I hope you give it a try and see how it feels.

Summary

In my experience, expression leaning code makes for a much nicer experience in writing and reading code. I find it helps to create more maintainable and composable solutions to problems.

Statements

  • describe how to do it
  • leave the code open to modification
  • intermediate variables/state
  • tend towards more mutability
  • easier to rot

Expressions

  • return values
  • describe what to do
  • keeps related logic/functionality contained and together
  • leave the code open for extension
  • composable

Top comments (5)

Collapse
 
sethcalebweeks profile image
Caleb Weeks

Nice post! How would you refactor your initial example in TypeScript?

var ageBracket: string

if (age <= 12) ageBracket = "junior"
else if (age >= 19) ageBracket = "teen"
else ageBracket = "adult"
Enter fullscreen mode Exit fullscreen mode
Collapse
 
benjistephenson profile image
Benji Stephenson

Thanks :)
One option I might use could be

const ageBracket = age <= 12 
    ? "junior"
    : age >= 19
        ? "teen"
        : "adult"
Enter fullscreen mode Exit fullscreen mode

but 2 checks is probably my limit for using a ternary. If you're a Ramda fan you could take a look at the ifElse function, or something like Predicate from Prelude-ts.

Collapse
 
sethcalebweeks profile image
Caleb Weeks

Nice! I sometimes like using cond, as seen in Randa (though I usually roll my own). But yeah, ternary seems good enough for just two conditions. I also like the switch(true) inside a function.

Collapse
 
user64bit profile image
Arth

Expressions over statements, Sounds Interesting...
I need to give it shot.

Collapse
 
benjistephenson profile image
Benji Stephenson

I've definitely found it helps me model what I'm doing a bit more clearly. Hope you have fun with it!