Skip to content

Learn Lambda

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

Warning - This is somewhat 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:

# 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

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:

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.

Using this syntax you can extend any value by adding fields to it:

let x = 5,
    x.double = 10,
    x.triple = 15,
    x.is_even = false
in

{x, x.double, x.triple, x.is_even}  # {5, 10, 15, false}

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 a good convention to call it this (but this is not mandatory).

The example above prints hello!.

Classes?

Much like in JavaScript, a function can be intended as a constructor in Lambda. In that case it's a good convention to have the first argument called this, while the constructor function is intended to extend this with the object's properties and return this.

The new function can then be used to bind this to a new object and invoke the constructor.

For example:

let Class = fn this, arg1, arg2 ->
  let this.field1 = arg1,
      this.field2 = arg2,
      this.add = fn this -> this.field1 + this.field2
  in this
in

let object = new Class 3 4 in

console.log {object.field1 + '+' + object.field2 + '=' + object.add}  # prints 3+4=7

Exceptions

Any value may be thrown (and possibly caught) as an exception.

The following throws a string:

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 new Error {'The error xyz happened.'}

You can then refer to a thrown value inside a catch expression using the error keyword:

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.'}

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

To achieve iteration, Lambda provides a fixed point combinator called fix.

The following example is a recursive factorial:

fix fn f, n ->
	if n < 1
	then 1
	else n * f (n - 1)

While fix is very easy to implement as a Z combinator, the type system would not be able to give it a type. For this reason fix is provided as a predefined term whose type is arbitrarily defined as the most generic type that satisfies the fix-point equation: ∀T . (T → T) → T.

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.

Let's say you need to run subsequent statements that cause side effects and you want to be able to rely on the correct order of the side effects, as in the following JavaScript example:

console.log('hello 1');
console.log('hello 2');
console.log('hello 3');

In Lambda this can be achieved using the predefined seq term:

seq
  (console.log {'hello 1'})
  (console.log {'hello 2'})
  (console.log {'hello 3'})

seq has the following definition:

fix fn f, x -> f

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 seq produces to work correctly.

This extra closure print will not appear when the program is compiled and then run in a JavaScript interpreter.