Hello and welcome. This series is meant as an introduction to the Amulet programming language. It assumes some previous programming experience, but no experience with functional languages. If you’re already familiar with ML-family languages, feel free to skip this one.
Like all languages in the ML family, Amulet is a strict strongly-typed language with pervasive type inference; The compiler can check your code for correctness even if you omit most (if not all) type annotations. As a simple example, the compiler can easily figure out the function
magnitude below works on floating-point values:
Unlike most other major languages in the ML tradition (OCaml and Standard ML, for instance), Amulet is able to infer a principal type for the
magnitude function even though the set of fields in the record
r isn’t statically known.
A note on presentation: Code blocks presented in block quotes (such as this one) are not executable, and may not even parse (here, for instance, Amulet has no
The principal type for the
magnitude function indicates that it can take a record with any number of fields, as long as the
y fields are both present and are
While Amulet does not have a module system as complex as other MLs’, it has a more expressive type system than most, with support for higher-rank polymorphism, kind polymorphism, GADTs, type classes with associated types, closed type functions, functional dependencies and more1. As of recently, Amulet can even be used to prove simple properties of functions on the type level!
Amulet has two built-in number types,
float; These are the types of literals, such as
0.5. Note that since Amulet has no overloading for numeric literals or subtyping,
123 : float is a type error. Even further, Amulet does not overload numeric operators, so we have two sets: The familiar
+ - * /, which work on
+. -. *. /., which work on
floating point numbers. There are also exponentiation operators
We can do simple arithmetic in the REPL:
Lines starting with a
>are user input. The rest is printed by the compiler.
Definitions can be made using the
let ... = ... form:
Amulet will prevent us from adding a
floating-point number to an
It was shown in the first section that functions can be defined with
let function_name argument = body .... However, this is slightly misleading, as it (almost) implies that functions can only be defined by using
This is not the case: Functions, like almost everything in Amulet, are expressions, and there is an associated literal notation for expressing functional values:
fun argument -> body. Notice that
argument here is singular.
This isn’t a typo: Amulet functions can only have one parameter.
“What?!”, I hear you say. This is not as limiting as you can think (in fact, it is not limiting at all): Since functions are first-class values, we can return functions from functions. This lets us define functions of multiple arguments by currying: For each parameter, there’s an intervening function.
Concretely, the function
-> associate to the right: That is, the type
A -> B -> C is to be read as
A -> (B -> C), never
(A -> B) -> C: The latter is a function of a single parameter (that is also a function). Since writing
fun x -> fun y -> fun z -> ... is boring, you can write multiple arguments in a single
fun x y z -> .... This is directly equivalent to the longer formulation.
This might sound like it leads to a terrible performance cost, what with allocating all those closures, but the Amulet compiler aggressively optimises these away.
One might wonder if these curried functions are truly equivalent to the familiar functions of multiple arguments in other programming languages. We can verify this using amc-prove, by asking for an isomorphism between
(A -> B -> C) and
A * B -> C.
*?”, you ask. This leads us nicely to our next topic.
Tuples and Records
Amulet has two primitive, non-extensible3 heterogeneous collections: tuples (in reality, pairs) and records.
A pair is just that: Two values, associated. Pairs are written like
(x, y), and their type reflects the types of both components. For instance,
(1, 2) has type
int * int. Why are pair types written with a
*? This notation, traditional to the ML family, is because pairs are like Cartesian products of types. Moreover, if the type
A has \(a\) elements and the type
B has \(b\) elements, the type
A * B has \(ab\) elements.
Concretely, let us take pairs of booleans as an example. How many elements does
bool * bool have? Well,
bool has 2 elements, so it stands to reason that
bool * bool has 2 * 2 = 4 elements. Indeed:
bool * bool.
(a, b, c) is shorthand for
(a, (b, c)).
Destructuring pairs can be done with pattern matching. Here, we use the wildcard pattern
_ to ignore the second component of the pair, or Amulet will complain that we bound a variable and did not use it.
Records, unlike pairs, can contain many values (as opposed to just two), and the positions are labelled:
Record accesses are written postfix:
r.x. Unlike in OCaml and Standard ML, this does not fix the type of the record, as mentioned in the introduction; It merely constrains
r to have the field
x. This lets us have almost dynamic-language levels of flexibility when accessing records, since unused fields are simply ignored!
You can never have a type error from having too much in a record, just from having not enough.
Using the Compiler
The Amulet compiler,
amc, supports both batch compilation (where a file is compiled into Lua) and interactive execution, where you load a file and can interact with it from the REPL.
There is as of writing no mechanism for compiling multiple files other than listing them all in the
amc invocation, in dependency order. For instance, if
main depends on
mod1, which depends on
mod2, you’d have to compile them as such (calling the output file
If you want to enter the REPL with files loaded, replace the
compile command with
Note that, in the top-level of a file, only declarations can be present. To evaluate an expression for its side effects, bind it to the wildcard pattern
_. When the expression is of type
unit, you may also use
() for greater clarity:
Foreign functions can be defined using the
external val construct. The text in quotes should be a Lua expression (It will be syntax-checked and pretty-printed by the Amulet compiler). To use multiple lines in the definition, escape the newline character by placing a backslash (
\\) right before the line break.