Beginner tutorial - Pauan/nulan GitHub Wiki
In Nulan, everything is an expression. An expression is a chunk of code that when evaluated returns a value. Let's see an example. First off, make sure you're in the REPL, then type 1
and press Enter
. You should see this:
=> 1
1
Here's what happened. You entered the expression 1
. Then, when you hit Enter
, Nulan evaluated the expression. Numbers evaluate to themself, so Nulan simply returned 1
, which was then printed on the line after =>
Because numbers evaluate to themself, they're not very interesting. Let's look at a more interesting expression:
=> (add 1 2 3)
6
What happened? The expression (add 1 2 3)
is known as a "list". A list is a data structure that can hold multiple items in it. In this particular case, the list holds 4 items: the symbol add
, the number 1
, the number 2
, and the number 3
When Nulan sees a list, it assumes that it's a call to a vau, with the first element being the vau, and every element after the first being the arguments to the vau.
So, Nulan first looked for the vau add
, which just so happens to be built-in to Nulan. Nulan then passed the numbers 1
, 2
, and 3
to the add
vau. The add
vau then evaluated all of its arguments, added them together, and returned 6
Whew, that's a lot of work! But just what is a vau, anyways? A vau is a special data structure that accepts arguments as inputs and returns values as outputs. Very simple, no? But, how do you create your own vaus?
It just so happens that there's a $vau
vau which can create vaus:
=> ($vau Env [X] (add X 1))
(&vau)
Woah! What's going on here? First off, $vau
doesn't evaluate its arguments. If it did, then (add X 1)
would have thrown an error because the symbol X
doesn't exist! By convention, vaus that don't evaluate all of their arguments are prefixed with $
and vaus that evaluate all their arguments are not prefixed with $
. That's why it's called $vau
rather than vau
, and add
rather than $add
Okay, and what's with that weird (&vau)
bit? Well, unlike numbers, Nulan can't print out vaus in a sensible way, so it instead just prints (&vau)
Alright, so, we're passing the Env
, [X]
, and (add X 1)
arguments to the $vau
vau. You can ignore the Env
part for now, but we'll get back to it later. The [X]
part is the argument list. It specifies how many arguments the vau accepts and also what to name them. In this case, the vau only accepts a single argument, which is called X
Everything after [X]
is the body of the vau. What happens is that when a vau is called, it will evaluate every expression in the vau's body, left-to-right, and whatever expression is last will be returned. In this case, there's only one expression (add X 1)
, so that's what will be returned when the vau is called.
Okay, but, what does the above vau do? Well, let's try calling it!
=> (($vau Env [X] (add X 1)) 2)
3
What the...?! Well, we use (...)
to call a vau, right? And the first element of the list is assumed to be a vau, right? Well, the first element doesn't have to be a symbol, it can be any expression! So we used ($vau Env [X] (add X 1))
to create a vau, and then immediately called the vau with the argument 2
It might help if you think about it as replacing expressions with simpler expressions:
(($vau Env [X] (add X 1)) 2) # Replace every X inside the body with 2
($vau Env [X] (add 2 1)) # Replace (add 2 1) by calling it
($vau Env [X] 3) # Replace the vau with its body
3
Thus, you could say that what Nulan does is keep replacing more complicated expressions with simpler expressions until it can't replace them anymore. This way of thinking is incorrect, but it will do for now. Later on, once we reach more advanced concepts like closures, you'll understand why it's incorrect.
Therefore, ($vau Env [X] (add X 1))
is a vau that accepts a single argument X
and returns the result of adding 1
to its argument.
Personally, I think it's getting rather tiring having to type out the full vau every time. What we really want to do is give the vau a name so we can easily call it multiple times. To do that, we use the $set!
vau:
=> ($set! $add1 ($vau Env [X] (add X 1)))
(&vau)
As the name suggests, $set!
doesn't evaluate all of its arguments. What $set!
does is, it evaluates the second argument, assigns it to the first argument, then returns the second argument. In this particular case, that means assigning the vau ($vau Env [X] (add X 1))
to the symbol $add1
, then returning the vau.
What does "assign" mean? It means that every time we evaluate the symbol $add1
, it will now return the vau that we have assigned to it. Let's try it out:
=> $add1
(&vau)
Great! Now we can just use $add1
rather than having to type out the full vau every time! Let's try calling it:
=> ($add1 10)
11
If we view this as substitution, then...
($add1 10) # Replace the symbol $add1 with whatever it's assigned to
(($vau Env [X] (add X 1)) 10) # Replace the symbol X inside the vau's body with 10
($vau Env [X] (add 10 1)) # Replace (add 10 1) by calling it
($vau Env [X] 11) # Replace the vau with its body
11
Let's try it again, this time with a different argument:
=> ($add1 50)
51
I'm sure you can go through the above substitution steps yourself. The only difference is that X
is replaced with 50
rather than 10
.
I'm also rather tired of having to type out numbers all the time, so let's assign a number to a symbol:
=> ($set! its-over 9000)
9000
Now, evaluating its-over
will return 9000
. Let's try calling our $add1
vau on the symbol its-over
:
=> ($add1 its-over)
%f
Woah, what happened?! Well, let's go through the substitution steps:
($add1 its-over) # Replace the symbol $add1 with whatever it's assigned to
(($vau Env [X] (add X 1)) its-over) # Replace every X inside the body with its-over
($vau Env [X] (add its-over 1)) # Replace (add its-over 1) by calling it
($vau Env [X] %f) # ???
%f
You see, the add
vau can only add numbers, it doesn't know how to add the symbol its-over
to the number 1
! Most things, when they fail, won't throw an error. Instead, they'll just return %f
, which is used to mean fail/false in Nulan.
The problem here is that vaus by default don't evaluate their arguments, which means the argument its-over
wasn't evaluated. That's why we called the vau $add1
rather than add1
. What if we want to evaluate the vau's arguments? There's two ways:
-
We can call the
eval
vau to explicitly evaluate things:($set! add1 ($vau Env [X] (add (eval Env X) 1)))
Now, when the vau
add1
is called, it will evaluate the argumentX
in the environmentEnv
. Aha! So that's what theEnv
argument to$vau
is for: we pass it toeval
when evaluating things.But does it work? Let's try calling the above vau on the symbol
its-over
:=> (add1 its-over) 9001
Much better. Let's go through the substitution steps:
(add1 its-over) # Replace the symbol add1 with whatever it's assigned to (($vau Env [X] (add (eval Env X) 1)) its-over) # Replace every X inside the body with its-over ($vau Env [X] (add (eval Env its-over) 1)) # Replace (eval Env its-over) by calling it ($vau Env [X] (add 9000 1)) # Replace (add 9000 1) by calling it ($vau Env [X] 9001) # Replace the vau with its body 9001
But having to explicitly evaluate every argument is a pain... So instead you can...
-
Call the
wrap
vau on any vau. Whatwrap
does is it returns a special vau which will evaluate all of its arguments before passing them to the vau. So, we can also writeadd1
like this:=> ($set! add1 (wrap ($vau Env [X] (add X 1)))) (&fn)
What's with the
(&fn)
bit? Well, the special vaus thatwrap
returns are called "functions" because they evaluate all of their arguments. But the word "function" is rather long, so we shorten it to "fn". It just so happens that there's already a$fn
vau which returns fns. We can use that rather thanwrap
+$vau
:=> ($set! add1 ($fn [X] (add X 1))) (&fn)
Notice that
$fn
looks an awful lot like$vau
, except it doesn't have theEnv
argument. That's becausewrap
already evaluates the arguments, so there's no need to calleval
explicitly, so theEnv
argument isn't needed except in very rare circumstances, thus it's more convenient to leave it off.
From this point forward, I will use the word "vau" to refer only to vaus that might not evaluate all of their arguments (these are prefixed with $
), and I'll use the word "fn" to refer to functions, which are vaus that always evaluate all of their arguments.