Skip to content

Learn Lambda

Alberto La Rocca edited this page Aug 9, 2015 · 88 revisions

Tutorial

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.

Contents

Introduction

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.

Set up

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.

Comments

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

Data

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.

Operators

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 !

Choices

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

Functions

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.

Let and objects

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.

Methods and this

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

Exceptions

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.

Arrays and higher order functions

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.

Types

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:

  1. An object type B is a sub-type of an object type A if:
    • B contains at least all the fields A contains, and
    • each field of A is a super-type of the corresponding field in B.
  2. A function type A' => B' throws C' is a sub-type of a function type A => B throws C if:
    • A' is a sub-type of A (covariance of the returned type),
    • B' is a super-type of B (contravariance of the argument type),
    • C' is a sub-type of C (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.

unknown

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.

Doing many things

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.

Clone this wiki locally