-
Notifications
You must be signed in to change notification settings - Fork 0
Tutorial
If you are new to Lomda, you should read over this guide (or at the very least skim over it). In this tutorial, I will demonstrate how to use the various features of Lomda.
If you have not installed Lomda, see the readme for more information on how to install the interpreter. To use the interpreter, one can either provide a source file to run code from (ex: ./lomda
), or none if one wishes to directly access the interpreter (ex: ./lomda source.lom
). Source files should have the .lom
extension.
Lomda has several flags available that can be specified from the command line. These are:
Flag | Description |
---|---|
-O/--optimize | Optimization option. When enabled, this will attempt to improve the efficiency of the program. |
-t | Runs the test cases for the language. Program will terminate following execution of the test cases. |
-v/--verbose | Verbose mode. This is for development of the language, and will display various debug info. |
--version | Displays the version and exists. |
--werror | Interprets warnings as errors. For instance, a warning is thrown when a program's syntax tree is ambiguous. |
On the command line, these can be used to adjust some of the functionality of the interpreter. For instance, for a restrictive setup that forces proper usage of the grammar, one can run one of the following:
$ ./lomda --werror
$ ./lomda --werror source.lom
Performing simple mathematical operations can be done exactly as one would expect.
> 1 + 2
3
> 3 * 5
15
> 3 * 4 - 7
5
One can also define and use variables in expressions. In order to use variables, one must declare them:
> let x = 2; x + 3
5
Notice the presence of a single semicolon. In Lomda, programs do not consist of semicolon-terminated statements, but are instead built from semicolon-separated statements.
In Lomda, we can define multiple variables on different lines or on the same line. Thus, the following are identical programs:
> let x = 2; let y = 3; x + y
> let x = 2, y = 3; x + y
Lomda allows for boolean conditionals and branching using any of the C-style operators (==, !=, >, etc.), as well as some pythonic operators (is, equals, not, etc.). Some examples include:
> 6 >= 2
true
> true and not false
true
> true is not true
false
> 6 is 6
true
We can also perform branching operations using if statements:
> if 6 is 7 then 1 else 0
0
> if true is not false and 7 < 13 then 17 else 3
17
Furthermore, we can create while loops that continue to evaluate until the loop condition evaluates to false:
source.lom
==========
let x = 2,
y = 0;
while x > 0 {
y = x + y;
x = x - 1
};
y
Output
======
3
For the purpose of storage, Lomda allows for the creation of lists, dictionaries, tuples, and ADTs.
Creation and access of lists operates very similarly to most languages:
> [1,2,3]
[1, 2, 3]
> let x = [1,2,3,4,5]; x[3]
4
> [0,1,2,3,4][1:3]
[1, 2]
To insert into a list, one can use the insert or removal syntax:
> let x = [0,2,3]; insert 1 into x at 1; x
[0, 1, 2, 3]
> let x = [0,1,1,2]; remove 2 from x; x
[0, 1, 2]
Finally, we can iterate over a list using a for loop. We can do this like so:
source.lom
==========
let sum = lambda(x) {
let s = 0;
for i in x
s = s + i;
s
};
sum([1,2,3])
Output
======
6
Lists can also be utilized as linear algebraic structures such as vectors and matrices. Some examples:
> [1, 2] + [3, 4]
[4, 6]
> [[1, 2], [-1, 3]] * [[1], [2]]
[[5], [5]]
Dictionaries are defined explicitly, and are accessed via the dot operator:
> let D = {x : 1, y : 2}; D
{x : 1, y : 2}
> let A = {x : 123, y : "abc"}; A.x
123
> let X = {t : true, f : false}; X.t
true
They can be given any number of initial values (including zero), and can be given new values dynamically:
> let A = {}; A.x = 42; A
{x : 42}
Note: this is not expected to be type safe. As of 2018-07-23, this example will not pass the type checker.
We can also define tuples in the language, which store exactly two values. We can define a tuple like so:
> let T = (1, 2); T
(1, 2)
> left of (1, 2)
1
> right of (false, true)
true
Algebraic Datatypes, or ADTs, are a special type that can be recursively defined. When declared, a dictionary is created that allows a user to generate instances of the type:
> type Num = Int(Z) | Real(R); switch Num.Int(2) in Int(z) -> z + 1 | Real(r) -> r - 1
3
Functions in Lomda are designed with the ability to be recursive depending on the definition context. One can define a function as an inline lambda or as a function in a let expression. The difference between the two is that a function is given recursive properties on definition. Consider the following:
> lambda (x) x
λx.x | {}
The notation that is outputted is designed to reflect the connection between the language and the lambda calculus. We can see that the lambda has a set of arguments (only 'x'), a body (compute 'x'), and a set of variables that can be accessed from the lambda's scope (its "environment"). However, we can also define lambdas that take multiple arguments:
> lambda (x, y) x + y
λx,y.x + y | {}
We can also define a lambda with a multiple line body:
> lambda (x) { let y = x + 1; x * y }
λx.let y = x + 1; x * y | {}
If one does not wish to write out the 'lambda' keyword, we can also use an abbreviated syntax. Consider the following identical lambdas:
> lambda (x, y) x + y
> (x, y) -> x + y
While the second version is clearly smaller, one may wish to use the keyword syntax to make it clear that a lambda is in use. Hence, the keyword syntax will be used throughout the wiki.
Should we wish to define a function, we can make use of a let to do this:
> let f = lambda (x) x; f
λx.x | {}
> let inc = lambda (x) x+1; inc(2)
3
Notice that we can call the lambda by the given name instead of by the raw lambda. This allows us to define repeated values more quickly and pass them around.
It it very useful to have a notion of recursion available. The syntax for defining recursive functions is as follows:
> let fib(x) = if x < 2 then x else fib(x-1) + fib(x-2); fib(5)
5
> let f(x) = x, g = lambda (x) 2*x; f
λx.x | {g := λ, f := λ}
Notice that in this example, there exist a larger number of variables that are visible to the lambdas. Hence, we can define a recursive function such as the Fibonacci function. Furthermore, we could define mutually recursive functions that call one another if we so wish.
If one wishes to do so (for some horrible reason), one can also use the lambda symbol in place of "lambda". This would be for the purpose of brevity or notation, but otherwise serves little purpose. Hence, the following are equivalent:
> lambda (x) x
> λ(x) x
Lomda provides the higher order map and fold as expressions in the language. It also provides the thunk operation, which saves a calculation until a later time.
Map is an operation that takes a list of values and converts them to different values. Consider the following example:
source.lom
==========
let L = [1, 2, 3];
let square = lambda (x) x*x;
map square over L
Output
======
[1, 4, 9]
Here, we can see the use of a map to convert a list of integers into the squares of the original list. Thus, the expression "maps" a list of numbers to their squares.
A fold is a function that is used to calculate over the set of lists. Consider the following example:
> fold [1, 2, 3] into lambda (x,y) x+y from 0
6
As we can see, this use of a fold computed the sum of the given list of numbers; 1 + 2 + 3 = 6. It is important to note that the current value is stored in y, while the accumulated value is given as x. The use of the variables is illustrated as follows:
> fold [1, 2, 3] into lambda (x, y) x from 0
0
> fold [1, 2, 3] into lambda (x, y) y from 0
3
In the first line, passing x simply carries the original accumulator value through the expression, resulting in zero. On the second line, the use of y passes the last seen value in the list, resulting in the last element of the list being returned.
Although they are not technically a higher-order function, thunks are special expressions that do not evaluate until they are needed. Thunking is the basis of so-called "lazy evaluation", in which unnecessary values are not computed until they become necessary. This is ordinarily implicitly handled, but in Lomda, it is done explicitly. Consider the following example:
> let f(x) = f(x); let x = f(2); true
...
In this case, the program will infinitely loop until it inevitably encounters a stack overflow. It is also clear that this infinite calculation serves no purpose in the final evaluation of the expression. Hence, we do not need to compute it. If we are not sure of whether or not an expression is necessary, we can choose to thunk it:
> let f(x) = f(x); let x = thunk { f(2) }; true
true
Now, x will be defined as a thunk, which will never be evaluated. In this specific example, the true answer would be to eliminate the definition of x. However, it may be the case in practice that the infinite call is a necessary component. Furthermore, it could have intentional side effects that are vital to the program. Hence, 'thunk' should be used with care.
One potential appication of 'thunk' would be in deep recursive functions. Consider Ackermann's function:
> let A(x, y) = {if x is 0 then y+1 else if y is 0 then A(x-1, 1) else A(x-1, A(x, y-1))}; A(5, 5)
stack overflow
If a thunk is used in place, the stack depth is limited to no more than a few frames:
> let A(x, y) = thunk {if x is 0 then y+1 else if y is 0 then A(x-1, 1) else A(x-1, A(x, y-1))}; A(5, 5)
...
One of the more powerful applications of Lomda is the built-in capability to differentiate programs. Using this, we can compute the derivative of a section of code, including functions. The syntax for differentiation uses the Leibniz notation. An annoying side effect of this is that 'd' is considered to be a keyword, and hence cannot be used as a variable name. The following are sample usages:
> d/dx 6
0
> let x = 2; d/dx x
1
> let x = 7; d/dx x*x
14
> d/dx lambda (x) x
λx.d/dx x | {}
In some cases, it may be quite cumbersome to put all of the source code into a single source file. Hence, Lomda grants the ability to split up code into modules. Consider the following example:
square.lom
==========
lambda (x) x * x
main.lom
========
import square;
square(4)
Output
======
16
In the example, the program defines square to be the result of evaluating the module square
in square.lom
. In the example, this is simply a lambda that multiplies a given value by itself. Then, the main program uses this value to evaluate the square of 4.
absurdlylongnameforamodule.lom
==============================
"Hello"
main.lom
========
import absurdlylongnameforamodule as hello;
print hello + " world"
Output
======
Hello world
In the second example, we see how we can given different names to our modules. Although the module's name is absurdly long, one does not need to use it in the program, since they can opt to use a different name instead.
We can also define a program that propagates a module. Consider the following:
> let import_mod = () -> { import module }; import_mod
λ.import module; module | {import_mod := λ}
Based on this, we can theoretically utilize functions to conditionally import a module.
Lomda also comes with a collection of standard libraries. These functions provide access to system utilities, and implement operations that most users would prefer to not handle. In the import statement, they take precedence over existing libraries. The provided modules are as follows:
Module | Description |
---|---|
fs | File system accessors and constants. |
linalg | Linear algebra operations. |
math | Mathematical operations. |
random | Random number generation utilities. |
sort | Sorting functions for real numbers. |
string | String manipulation utilities (note: x as S converts x to a string, regardless of type). |
sys | System utilities and constants. |