Everything You Need To Know To Write Amulet

This is part of a series of examples of the Amulet programming language. If you do not yet have an Amulet compiler, please refer to the installation instructions.

open import "foo.ml" imports foo from the library path.

open import "prelude.ml"

Let bindings introduce variables. These variables can not be mutated.

let foo = 1
let bar = ""

Type inference can figure out the type of a variable without the programmer having to supply it.

let foo x = x + x

(+) operates on ints, (+.) on floats.

let bar x = x +. x

The type construct can be used to define a new data type. Types are defined by listing their constructors

type foo =
  | Foo_str of string
  | Foo_int of int

Values of algebraic data types can be taken apart with pattern matching.

let get_foo_str x =
  match x with
  | Foo_str x -> x
  | Foo_int _ -> "it wasn't a string!"

The function keyword abbreviates a pattern matching function.

let get_foo_str' = function
  | Foo_str x -> x
  | Foo_int _ -> "not a string again?"

Pattern matching on bool can be done with if .. then .. else ...

let is_gte x y =
  if x >= y then
    "it is"
  else
    "it's not"

User-defined data types can be recursive.

type chain =
  | Chain_end
  | Chain_add of string * chain

Recursive functions need a rec keyword, as in OCaml.

let rec str_of_chain = function
  | Chain_end -> "end"

Concatenation is done using the (^) function.

  | Chain_add (x, rest) ->
      x ^ " + " ^ str_of_chain rest

Types can have parameters. These stand for another type, and can be inferred.

type li 'a =
  | Nil_
  | Cons_ of 'a * li 'a
val map : ('a -> 'b) -> li 'a -> li 'b
let rec map f = function
  | Nil_ -> Nil_
  | Cons_ (x, xs) -> Cons_ (f x, map f xs)

Amulet has a built-in type of lists, list 'a.

let foo : list int = [1, 2, 3, 4, 5]

Lists can be manipulated using list comprehensions.

let greater_than_3 =
  [ x
  | with x <- foo
  , x > 3
  ]

List literal syntax can also be used in patterns.

let [a, b, c] = [1, 2, 3]
let _ = a + b + c

Type classes are used for principled overloading.

class to_string 'a begin
  val to_str : 'a -> string
end

instance to_string int begin
  let to_str _ = "int"
end

instance to_string bool begin
  let to_str _ = "bool"
end

Methods can be used with any type that has a corresponding instance.

let "int" = to_str 123
let "bool" = to_str true

Type class methods can be polymorphic in their return types.

type a_box 'a = Box of 'a

The class into_box is parametrised by a type constructor.

class into_box 'f begin
  val into : 'a -> 'f 'a
end

instance into_box list begin
  let into x = [x]
end

instance into_box a_box begin
  let into = Box
end

Amulet selects the instance to use automatically, based on the context into is used in.

let Box 123 = into 123
let [123] = into 123

into_box is almost the standard class applicative. Amulet has special syntax for using applicative functors, idiom brackets:

let cartesian (xs : list _) ys =
  (| (,) xs ys |)

Idiom brackets desugar into uses of pure and <*>. These definitions of cartesian are identical.

let cartesian (xs : list _) ys =
  pure (,) <*> xs <*> ys

Amulet has special syntax for use of the monad class.

let might_fail x =
  if x >= 10 then
    None
  else
    Some x

The monad option instance aborts the computation when it encounters a None.

let None =
  let! x = might_fail 1
  let! y = might_fail 11
  pure (x + y)

Computations without None are simply chained.

let Some 2 =
  let! x = might_fail 1
  let! y = might_fail 1
  pure (x + y)