Skip to content

Learn Lambda

Alberto La Rocca edited this page Sep 24, 2016 · 88 revisions

Warning - This is outdated.

Update work is in progress...

Tutorial

Lambda is a simple, functional, statically typed programming language that compiles to JavaScript.

You can use it to develop for Node, 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

Previous development experience is needed to read this tutorial. Knowledge of functional languages is beneficial. Knowledge of JavaScript is too.

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

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:

> 1 + 2 + 3
6
> 

The lambda executable reads from the standard input and writes to the standard output. You can run program source files like this:

$ lambda < foo.lambda

To compile a Lambda file into a JavaScript file, specify the -c command line flag and redirect the standard output:

$ 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

Lambda has single-line comments only, and they start with the hash symbol (#) like in Python.

Example:

# this is a comment
console.log {'hello, world!'} # this is another one
# yet another comment

Data

Lambda uses the following data types (T indicates a generic type):

Type Name Description
undefined undefined The value undefined
booleans boolean true and false
complex numbers complex Complex numbers
real numbers real Real numbers
Integers integer Integer numbers
Naturals natural Natural numbers
Strings string Sequences of Unicode (UTF-16) characters
Lists T* Sequences of many elements of the same type
Closures T => T Function references

A closure is a function reference. For those who don't know, this means you can pass functions as parameters to other functions and return a function as the result of another function.

For example, this is what you do in JavaScript when you register a jQuery event handler:

$("button#my-button").on("click", function (event) {
	window.alert("Clicked!");
});

The second argument to the on method is a closure, or function reference.

The equivalent Lambda code would be:

($ {'button#my-button'}).on 'click' {fn event ->
	window.alert {'Clicked!'}}

See Functions for more information about functions.

Operators

Now that you know your data you can perform operations on it.

Lambda's operators are mostly the same as JavaScript.

3 + 2 * 5  # yields 13

Full table of Lambda operators:

Operator Description JavaScript equivalent
+ Binary plus +
- Binary minus -
* Multiplication *
/ Division /
** Power Math.pow
% Modulus %
= 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 Boolean AND &&
or Boolean OR `
xor Boolean XOR -
not Boolean NOT !
typeof String representation of type typeof

Comparisons can be chained:

1 < 2 < 3          # true
2.72 <= 3.14 < 10  # true
3 != 4 = 4         # true
7 = 7 <= 20 < 100  # true

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:

fn 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.

The previous translates to JavaScript as follows:

function (x) {
	return x;
}

To invoke a one-argument function, simply write the function and the argument value next to it:

# this returns 5
(fn x -> x) 5

This is called an "application", as in "applying a function to an argument".

To implement a function that receives more than one argument you need to define a function returning a function. For example, this is how you define a two-argument function that computes the sum of two numbers:

fn x -> fn y -> x + y

Fortunately Lambda provides syntactic sugar for that. The previous can be written as:

fn x, y -> x + y

And you can invoke it as usual:

# adding up 4 and 5 yields 9
(fn x, y -> x + y) 4 5

The syntax to invoke native functions provided by the JavaScript environment is slightly different. Those functions don't have a fixed arity and they may receive any number of arguments, including 0. For this reason Lambda is unable to unmarshal them to Lambda functions directly, and arguments must be specified in a list.

For example:

console.log {}
console.log {'hello, world'}
console.log {'hello', 'world'}

The above examples are equivalent to the following JavaScript, respectively:

console.log();
console.log('hello, world');
console.log('hello', 'world');

let and objects

The let statement allows you to create variables and assign values to them.

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 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 = fn this, message -> console.log {message} in
object.method 'hello!'

The first argument of the function is automatically bound to the object instance, so it's common convention to call it this (but this is not mandatory).

The example above prints hello!.

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 recommended because it will give you extra information such as the stack trace to the point the exception was thrown:

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-may-throw>
catch console.log {error}

You might also want to add a finally clause:

try <expression-that-might-throw>
catch console.log {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.'}

Just like in JavaScript, catch expressions cannot differentiate among different types of exceptions. A catch expression catches anything the corresponding try expression may throw.

Lists and higher order functions

List literals in Lambda are defined including a list of expressions in curly braces:

{ 1, 2, 3 }

A major difference from JavaScript arrays is that Lambda doesn't allow heterogeneous lists. This expression will produce a type error:

{ false, 0, 'bogus', 3.14 }

Lambda lists have methods similar to the modern JavaScript Array prototype: slice, concat, join, sort, each, filter, map, reverse, reduce, 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 = fn x, y -> x + y in
console.log {sum 13 45} # prints 58

That's great, but what if we need to implement a recursive function, i.e. 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 = fn 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 not let you 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 fn f, n ->
	if < n 1
	then 1
	else * n (f (- n 1))

Now we can write this:

let factorial = fix fn 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 a functional programming enthusiast.

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 on 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:

fn 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:

fn x: real, y: real -> + x y

In this way you put a restriction, the two arguments must be floating point numbers (or a sub-type: integer or unknown).

The polymorphic version is more flexible because it also allows 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 everything 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');

While this can be achieved in several ways in Lambda, the recommended one is using a basic monad.

Our monad will be a function called main that lets us apply an indefinite number of arguments. Each argument can be the result of an expression that we compute in place, so that we can write programs that compute any number of expressions instead of just one.

In code:

let main = fix fn f, x -> f in

main
	(console.log "hello 1")
	(console.log "hello 2")
	(console.log "hello 3")

Left associativity guarantees the expressions are evaluated in order.

When run in the interpreter, the program will output:

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