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
Comments? Give me a shout at @lambda_cat.
To get the latest post updates subscribe to the LambdaCat newsletter.
You can support my writing on LambdaCat's Patreon.