-
Notifications
You must be signed in to change notification settings - Fork 2
Learn Lambda
Lambda is a simple, functional, statically typed programming language that compiles to JavaScript.
You can use it to develop for both Node and the browser, or any other JavaScript environment.
You can also integrate the Lambda interpreter and transpiler in your own JavaScript application: Lambda is written in pure JavaScript and does not have any dependencies.
- Introduction
- Set up
- Comments
- Data
- Operators
- Choices
- Functions
- Let and objects
- Exceptions
- Arrays and higher order functions
- To iterate is human, to recurse is divine
- Types
- Doing many things
Some previous JavaScript development experience is needed to read this tutorial, which is mainly intended for people coming from that language.
The name "Lambda" comes from the lambda calculus, which Lambda is derived from.
Prerequisites: Node.js.
Use the following command to install Lambda:
$ sudo npm i -g lambda
Of course you need not specify sudo
if you are on a Windows command prompt.
After a successful installation you can start the Lambda interpreter using the lambda
command:
$ lambda
>
Type a simple expression to test everything is working correctly:
> console.log "hello, world!"
hello, world!
undefined
>
The first line is the hello, world!
message printed by the console.log
call; the second line reports the return value of the typed expression (the console.log
method returns undefined
).
You can also redirect the standard streams of the lambda
program, like this:
$ lambda < foo.lambda
This allows you to write Lambda source files of more than one line.
To compile a Lambda file into a JavaScript file, specify the -c
option:
$ lambda -c < foo.lambda > foo.js
The above will produce a foo.js
JavaScript file.
If you use Grunt or Gulp you might want to have a look at their respective plugins.
We will be sometimes using comments in the code snippets of this tutorial.
Lambda doesn't have multi-line comments, only single-line ones, and they are started by the hash symbol (#
), like in Python.
This is a commented hello-world program:
# this is a comment
console.log "hello, world!"
# this is another one
The data types that can be handled by a Lambda program are very similar to JavaScript's ones, the reason being that Lambda programs must be able to interact with JavaScript APIs provided by the environment (browser, Node, etc.).
But there are some differences. First difference: all the data in Lambda is immutable. For the rationale behind this and many other design choices, you can read Discourse.
Here's the full table of data types you will be dealing with while developing in Lambda (T indicates a generic type):
Type | Name | Description |
---|---|---|
undefined | undefined |
The value undefined
|
null | null |
The value null
|
booleans | bool |
true and false
|
complex numbers | complex |
Complex numbers |
floating point numbers | float |
Floating point numbers |
Integers | int |
Signed integer numbers |
Strings | string |
Sequences of Unicode characters |
Regular expressions | regex |
Regular expressions implemented by JavaScript |
Objects | Aggregations of other types | |
Arrays | T* |
Sequences of many elements of the same type |
Closures | T => T |
Function references |
A closure is a function reference. The reason closures are listed as a data type is that Lambda, much like JavaScript, is a functional language, and as such it has first-class functions. This means you can pass functions as parameters to other functions or return a function as the result of another function.
This is what you do in JavaScript, for instance, when you register a jQuery event handler:
$("button#my-button").on("click", function (event) {
window.alert("Clicked!");
});
You are passing a function (reference) as the second argument to the on
function of a jQuery object.
You would write that in Lambda in this way:
($ "button#my-button").on "click" event ->
window.alert "Clicked!"
See Functions for more information about functions.
Now that you have data you can perform operations on it.
Lambda mostly provides the same operators as JavaScript, the main difference being that Lambda uses Polish notation.
For example, this is how you write 3 + 2 * 5
in Lambda:
+ 3 (* 2 5)
Here is the full table of Lambda operators:
Operator | Description | JavaScript equivalent |
---|---|---|
+ |
Binary plus | + |
- |
Binary minus | - |
* |
Multiplication | * |
/ |
Division | / |
** |
Power | Math.pow |
% |
Modulus | % |
<< |
Left shift | << |
>> |
Right shift | >> |
>>> |
Unsigned right shift | >>> |
= |
Comparison | === |
!= |
Negated comparison | !== |
< |
Less than | < |
<= |
Less than or equal to | <= |
> |
Greater than | > |
>= |
Greater than or equal to | >= |
& |
Bitwise AND | & |
` | ` | Bitwise OR |
^ |
Bitwise XOR | ^ |
~ |
Bitwise NOT | ~ |
and |
Logical AND | && |
or |
Logical OR | ` |
xor |
Logical XOR | none |
not |
Logical NOT | ! |
Sometimes in our lives we have to make them.
The syntax of the if-then-else statement in Lambda is pretty straightforward:
if <condition>
then <result1>
else <result2>
Example:
if = "secret" (window.prompt "Enter your password here:") # if the entered password is "secret"...
then window.alert "Yup, that's it." # ... then alert this
else window.alert "No way, man." # ... otherwise alert that
This is how you write a simple function in Lambda:
x -> x
That is the identity function, a function that receives one argument and returns it without doing anything. x
is the argument, and what comes after the arrow symbol is the function body.
We could write that in JavaScript like this:
function (x) {
return x;
}
To call a one-argument function simply write the function and the value for the argument next to it:
# this returns 5
(x -> x) 5
This is called an "application", as in "applying an argument to a function".
You can of course do something more useful than using the identity function. For example you can call the log
method of the console
global object:
console.log 5
Or any other API provided by the environment: Node APIs, DOM APIs, jQuery, AngularJS, ... pick your favorite.
If you want to implement a function that receives more than one argument you can define a function returning a function. For example, this is how you define a two-argument function that computes the sum of two numbers:
x -> y -> + x y
Fortunately Lambda provides syntactic sugar for that. You can simply write:
# completely equivalent to the previous
x, y -> + x y
And you can subsequently invoke it like this:
# adding up 4 and 5
(x, y -> + x y) 4 5
You might now understand that operators are nothing but functions with special names like +
, -
and *
, and the reason for the Polish notation is that the syntax to apply operands to operators is the same as applying arguments to functions.
Unary operators (not
and ~
) are one-argument functions, and binary operators (+
, -
, *
, /
, ...) are two-argument functions, meaning they technically are functions returning functions.
The let
statement allows you to assign values to variables before they are used.
Here is an example let
statement:
let s = "hello, world!" in console.log s
In JavaScript, this is equivalent to:
var s = "hello, world!";
console.log(s);
The basic syntax is let <name> = <expression> in <rest-of-the-program>
.
You can also group definitions. For example, the following:
let x = 3 in
let y = 5 in
console.log (+ x y) # prints 8
can be written as:
let x = 3, y = 5 in
console.log (+ x y)
The let
statement can also be used to define objects. If the name of the variable you declare happens to contain one or more dots (.
), then you are defining one or more nested objects containing a field.
Example:
let user.name.first = "John",
user.name.last = "Doe",
user.age = 35,
user.sex = "M" in
console.log user
The above example will print something like { name: { first: 'John', last: 'Doe' }, age: 35, sex: 'M' }
, which is a JavaScript object containing some fields, including a nested object with other fields.
To create a method inside an object just let
a dotted name be a function:
let object.method = message -> console.log message in
object.method "hello!"
The above will print hello!
.
Just like JavaScript, Lambda lets you access other fields of the current object through the this
keyword:
let o.x = 123, o.add = y -> + this.x y in
console.log o.add 321
The above will print 444
because it defines an object with an integer field whose value is 123
and a method to add another value to it; then the value 321
is passed to the method, therefore 123 + 321
is performed and the result is printed.
Particular attention is required when working with this
due to a subtle difference between Lambda and JavaScript.
Like in JavaScript, Lambda's this
refers to the object a method is being invoked from:
function add(y) {
return this.x + y;
}
var o1 = {
x: 123,
add: add
};
var o2 = {
x: 456,
add: add
};
console.log(o2.add(321)); // prints 777
let add = y -> + this.x y in
let o1.x = 123, o1.add = add in
let o2.x = 456, o2.add = add in
console.log (o2.add 321) # prints 777
But Lambda's this
is more "sticky" than JavaScript's. In Lambda, unlike JavaScript, this
will still refer to the original object when a closure is assigned to and invoked from a non-dotted name:
let o.x = 123, o.add = y -> + this.x y in
let add = o.add in
console.log (add 321) # still prints 444
But a closure will definitely get a new this
if you assign it to a new dotted name:
let o1.x = 123, o1.add = y -> + this.x y in
let o2.x = 456, o2.add = o1.add in
console.log (o2.add 321) # prints 777
Any value may be thrown (and possibly caught) as an exception.
Throwing an exception in Lambda is as easy as:
throw "The error xyz happened."
Using JavaScript's Error is definitely suggested because it will give you extra information such as the stack trace to the point the exception was thrown, so you will more likely do this:
throw Error "The error xyz happened."
You can then refer to a thrown value inside a catch
expression using the error
keyword, like this:
try <expression-that-might-throw>
catch console.dir error
You might also want to add a finally
clause:
try <expression-that-might-throw>
catch console.dir error
finally console.log "This is printed anyway."
Or omit the catch
part:
try <expression-that-might-throw>
finally console.log "This is always printed."
Remember that, just like in JavaScript, catch
expressions cannot differentiate among different types of exceptions. A catch
expression catches anything the corresponding try
expression may throw.
Array literals in Lambda are defined including a list of expressions in curly braces:
{ 1, 2, 3 }
A major difference from JavaScript is that Lambda doesn't allow heterogeneous arrays. This expression will produce a type error:
{ false, 0, "bogus", /regex/ }
Lambda arrays have methods similar to the modern JavaScript Array prototype: slice
, concat
, join
, indexOf
, lastIndexOf
, sort
, forEach
, filter
, map
, reverse
, reduce
, reduceRight
, every
, and some
.
TBD
If you look at Functions you will notice we only talked about anonymous functions. Lambda's syntax to define a function simply doesn't have a place to put the name.
We can use the let
statement to assign the function to a variable so that we can later call it by name, like this:
let sum = (x, y -> + x y) in
console.log (sum 13 45) # will print 58
That's great, but what if we need to implement a recursive function, that is a function that calls itself inside its body?
We need to use its name inside its body.
As a basic example we might want to implement a recursive version of the factorial function.
If we wanted to compute and print the factorial of 5 (which is 120) we might think of this:
let factorial = n ->
if < n 1
then 1
else * n (factorial (- n 1)) in
console.log (factorial 5)
But nope, it won't work; the let
statement will definitely not allow you to use the defined name outside of the in
part, so you can't use it in the initialization expression.
The predefined fix
function solves the problem allowing one to build recursive functions. All we need to do is to define a function that receives its... "recursive version" as its first argument. fix
will do the rest and pass the recursive function to our first argument.
Example:
fix factorial, n ->
if < n 1
then 1
else * n (factorial (- n 1))
Now we can write this:
let factorial = fix (f, n ->
if < n 1
then 1
else * n (f (- n 1))) in
console.log (factorial 5)
which will definitely work.
fix
is a so-called "fixed point combinator". If you are curious about how it works you can find more information here.
Wondering what that has to do with the famous startup accelerator in Silicon Valley? As far as I know, Paul Graham, its founder, is passionate about functional programming.
Now for some more advanced stuff.
In Data you were provided an overview about Lambda's data types. But you must also be aware that Lambda has sub-typing rules, summarized by the following diagram:
Technically this is called a partial order relationship among Lambda types, and the above lattice is a Hasse diagram.
Read the diagram top-to-bottom like this: undefined
is the super-type of everything, complex
is the super-type of float
, float
is the super-type of int
, etc.
There are a few rules we cannot represent in the finite space of the diagram:
- An object type
B
is a sub-type of an object typeA
if:-
B
contains at least all the fieldsA
contains, and - each field of
A
is a super-type of the corresponding field inB
.
-
- A function type
A' => B' throws C'
is a sub-type of a function typeA => B throws C
if:-
A'
is a sub-type ofA
(covariance of the returned type), -
B'
is a super-type ofB
(contravariance of the argument type), -
C'
is a sub-type ofC
(covariance of the thrown type).
-
Roughly said, the whole point of having a sub-typing relationship among types is that every function expects a type, and you can only pass either that type or a sub-type.
So far you only learned to define functions without defining any type for their arguments, like this:
x, y -> + x y
These are called polymorphic functions, functions that accept any type and whose type is in turn computed when the function is applied, so that the polymorphic types can be replaced by the types of the applied arguments.
Polymorphic functions are easier to write, but argument types can indeed be specified explicitly. Example:
x: float, y: float -> + x y
In this way you put a restriction, the two arguments of our functions must be floating point numbers (or a sub-type: integer or unknown
).
The polymorphic version was more flexible because it also allowed strings and complex numbers, for example.
You might notice unknown
is the sub-type of everything, and wonder why.
Being a sub-type of everything means you can do everything on unknown
data (use any operators on it, pass it to any function, etc.), effectively disabling the type system.
The need for an unknown
type that allows the developer to do anything is explained by the need for interaction with external APIs provided by the environment.
The JavaScript world is growing at an extremely fast pace today and there are new APIs everyday. Unfortunately, JavaScript is not strictly typed and creating those APIs doesn't require the authors to give them types.
Trying to provide those types ourselves, doing it for every possible reusable piece of JavaScript code in the world, and keeping the types up to date with them would be foolish, and I'm not that fan of Steve Jobs. So the best solution is to allow Lambda developers to do anything with their favorite JavaScript API.
As you might have noticed, each Lambda program may only be made up of exactly one expression. This expression can span whatever number of lines of code and can include several different statements, including let
, if
, or try
, but there can be only one.
So what can you do if, for example, all you want to do is three consecutive console prints, like in the following JavaScript program?
console.log('hello 1');
console.log('hello 2');
console.log('hello 3');
This can be achieved in several ways in Lambda, the suggested one is definitely using a basic monad.
Our monad will be a function called main
that allows us to apply an indefinite number of arguments. Each argument can be the result of an expression that we can compute in place, so that we can compute any number of expressions (instead of just one) in our program.
In code:
let main = fix (f, x -> f) in
main
(console.log "hello 1")
(console.log "hello 2")
(console.log "hello 3")
Yet, if run in the interpreter, the output of this program will be slightly different from what is expected:
hello 1
hello 2
hello 3
closure
The final closure
message is the string representation of the result of the whole program, as it is always printed by the interpreter (see "Set up"). The result is a closure because this is what the main
monad produces to work correctly.
This extra closure
print will not appear when the program is compiled and then run in a JavaScript interpreter.