3.8 KiB
title | date | tags | draft | |||
---|---|---|---|---|---|---|
Compiling a Functional Language Using C++, Part 12 - Let/In and Lambdas | 2020-04-20T20:15:16-07:00 |
|
true |
Now that our language's type system is more fleshed out and pleasant to use,
it's time to shift our focus to the ergonomics of the language itself. I've been
mentioning let/in
expressions and lambda expressions for a while now.
The former will let us create names for expressions that are limited to
a certain scope (without having to create global variable bindings), while
the latter will allow us to create functions without giving them any name at
all.
Let's take a look at let/in
expressions first, to make sure we're all on
the same page about what it is we're trying to implement. Let's
start with some rather basic examples, and then move on to more
complex examples. The most basic use of a let/in
expression is, in Haskell:
let x = 5 in x + x
In the above example, we bind the variable x
to the value 5
, and then
refer to x
twice in the expression after the in
. The whole snippet is one
expression, evaluating to what the in
part evaluates to. Additionally,
the variable x
does not escape the expression -
{{< sidenote "right" "used-note" "it cannot be used anywhere else." >}}
Unless, of course, you bind it elsewhere; naturally, using x
here does not forbid you from re-using the variable.
{{< /sidenote >}}
Now, consider a slightly more complicated example:
let sum xs = foldl (+) 0 xs in sum [1,2,3]
Here, we're defining a function sum
,
{{< sidenote "right" "eta-note" "which takes a single argument:" >}}
Those who favor the
point-free
programming style may be slightly twitching right now, the words
eta reduction swirling in their mind. What do you know,
fold
-based sum
is even one of the examples
on the Wikipedia page! I assure you, I left the code as you see it
deliberately, to demonstrate a principle.
{{< /sidenote >}} the list to be summed. We will want this to be valid
in our language, as well. We will soon see how this particular feature
is related to lambda functions, and why I'm covering these two features
in the same post.
Let's step up the difficulty a bit more, with an example that,
{{< sidenote "left" "translate-note" "though it does not immediately translate to our language," >}}
The part that doesn't translate well is the whole deal with patterns in
function arguments, as well as the notion of having more than one equation
for a single function, as is the case with safeTail
.
It's not that these things are impossible to translate; it's just
that translating them may be worthy of a post in and of itself, and would only
serve to bloat and complicate this part. What can be implemented with
pattern arguments can just as well be implemented using regular case expressions;
I dare say most "big" functional languages actually just convert from the
former to the latter as part of the compillation process.
{{< /sidenote >}} illustrates another important principle:
let
safeTail [] = Nothing
safeTail [x] = Just x
safeTail (_:xs) = safeTail xs
myTail = safeTail [1,2,3,4]
in
myTail
The principle here is that definitions in let/in
can be recursive and
polymorphic. Remember the note in
[part 10]({{< relref "10_compiler_polymorphism.md" >}}) about
let-polymorphism? This is it: we're allowing polymorphic variable bindings,
but only when they're bound in a let/in
expression (or at the top level).
The principles demonstrated by the last two snippets mean that compiling let/in
expressions, at least with the power we want to give them