Road to Elm - 'let' and 'in'

Road to Elm - Table of Contents

If you haven't programmed in an ML-inspired language before, let and in are probably new to you.

In an imperative language you can get away with sprinkling your variables all over the place. Nothing is enforcing their placement in a particular place in the code.

In many functional languages the variables you need for a specific function are defined in a let, and then they are in scope only for the code between the let and the end of the function.

Also the in part must evaluate to one expression, you can't just write line after line in it.

Comparative let example

An example in Elm of how let ... in ... works: we're adding the areas of two rectangles. The rectangles width and height are represented by (Int, Int) tuples.

addAreas : (Int, Int) -> (Int, Int) -> Int
addAreas rect1 rect2 = 
  let
    (w1, h1) = rect1 -- using destructuring
    (w2, h2) = rect2
    area w h = w * h -- we can declare a function
  in 
    (area w1 h1) + (area w2 h2)

The same example in Javascript:

var r1 = { x : 9, y : 8 };
var r2 = { x : 7, y : 5 };

function addAreasNice (rect1, rect2){
  var w1 = rect1.x;
  var h1 = rect1.y;
  var w2 = rect2.x;
  var h2 = rect2.y;
  var area = function (w, h){ return (w * h) };

  return area(w1, h1) + area(w2,h2);
}

> addAreasNice(r1, r2);
107

that was a virtuous example, that declares those variables inside the function that uses them, and doesn't depend on variables not passed as arguments.

When things go stealthily wrong

But since in Javascript you are not restricted from declaring variables pretty much anywhere, what you may actually end up doing is:

var r1 = { x : 9, y : 8 };
var rect2 = { x : 7, y : 5 };

function addAreasEvil (rect1){
  var w1 = rect1.x;
  var h1 = rect1.y;
  var area = function (w, h){ return (w * h) };
  var a1 = area(w1, h1);
  var a2 = area(w2,h2)
  var w2 = rect2.x;
  var h2 = rect2.y;

  return a1 + a2;
}

> addAreasEvil(r1);

which is way harder to read. And possibly a source of bugs.

Let's try that out:

> addAreasEvil({x :6, y:8});

NaN

Fun. It is quite an evil function.

You might say "I'd never do that!", but this is a very simple and obvious function. You can see at a glance everything that you need for it.

Consider the case where 5 developers are working on the same code. You don't know exactly what the others have done, and you're trying to extract some functionality out of their code without reading it line by line.

In that case it's pretty easy to not realise that the variable you're using from outside the function should be passed in, instead of relied on because it's in scope, anyway.

I suggest Kris Jenkis's great article on this matter.

Before encountering let I didn't think twice about whether I declared/calculated the variable I need in a function, inside that same function.

But from the comparison I can easily see that it makes it easier for me to keep the code complexity from spiralling out of control while I'm not particularly paying attention to it.

Which is any time you need to ship something fast (= most of the time).

Won't compile example

As I mentioned earlier you can't just write your statements in sequence inside an in.

addAreas : (Int, Int) -> (Int, Int) -> Int
addAreas rect1 rect2 = 
  let
    (w1, h1) = rect1 -- using destructuring
    (w2, h2) = rect2
    area w h = w * h -- we can declare a function
  in 
    a1 = (area w1 h1)
    a2 = (area w2 h2)
    a1 + a2 -- THIS WON'T COMPILE

If we try to compile that we'll get:

-- SYNTAX PROBLEM --------------------------------------------
I ran into something unexpected when parsing your code!

7│     a1 = (area w1 h1)
          ^
I am looking for one of the following things:

    end of input
    whitespace

Yep, the compiler doesn't buy it.

This example is easily solved by moving the a1 and a2 declaration into the let:

addAreas : (Int, Int) -> (Int, Int) -> Int
addAreas rect1 rect2 = 
  let
    (w1, h1) = rect1 -- using destructuring
    (w2, h2) = rect2
    area w h = w * h -- we can declare a function
    a1 = (area w1 h1)
    a2 = (area w2 h2)
  in 
    a1 + a2 -- THIS COMPILES

which also demonstrates that the definitions in the let are also available within it, as well as the within the in.

Examples of things you can have in an in are:

  • case <something> of
  • multiple functions nested using round parenthesis, or connected by pipes (|>) or other ways of composing functions
  • tuples or records (they can contain more than one value)

That's all for let and in, see you next time.

References

Scope in Elm