--- title: Amulet updates date: August 11, 2018 maths: true --- Jesus, it's been a while. Though my last post was almost 6 months ago (give or take a few), I've been busy working on [Amulet](https://github.com/tmpim/amulet), which continues to grow, almost an eldritch abomination you try desperately, but fail, to kill. Since my last post, Amulet has changed a ton, in noticeable and not-so-noticeable ways. Here are the major changes to the compiler since then. Parser improvements =================== No language is good to use if it's inconvenient. So, in an effort to make writing code more convenient, we've removed the need for `;;` after top-level declarations, and added a _bunch_ of indentation sensitivity, thus making several keywords optional: `begin`{.ocaml} and `end`{.ocaml} are implicit in the body of a `fun`{.ocaml}, `match`{.ocaml}, or `let`{.ocaml}, which has made those keywords almost entirely obsolete. The body of a `let`{.ocaml} also need not be preceded by `in`{.ocaml} if meaning is clear from indentation. To demonstrate, where you would have ```ocaml let foo = let bar = fun x -> begin a; b; c end in begin bar (); bar 1; end ;; ``` One can now write ```ocaml let foo = let bar = fun x -> a b c bar () bar 1 ``` Moreover, we've added shorthand syntax for building and destructuring records: `{ x, y, z }`{.ocaml} is equivalent to `{ x = x, y = y, z = z }`{.ocaml} in both pattern and expression position. Changes to record typing ======================== Whereas `{ x with a = b }` would extend the record `x` to contain a new field `a` (with value `b`), it's now _monomorphic update_ of the record `x`. That is: `x` must _already_ contain a field called `a`, with the same type as `b`. This lets you write a function for updating a field in a record, such as the one below, which would previously be impossible. Supporting polymorphic update is not a priority, but it'd be nice to have. The way PureScript, another language with row-polymorphic records, implements polymorphic update does not fit in with our constraint based type system. A new type of constraint would have to be introduced specifically for this, which while not impossible, is certainly annoying. ```ocaml let foo : forall 'row. { 'row | x : int } -> { 'row | x : int } = fun r -> { r with x = r.x + 1 } ``` The impossibility of supporting polymorphic update with regular subsumption constraints $a \le b$ stems from the fact that, when faced with such a constraint, the compiler must produce a coercion function that turns _any_ $a$ into a $b$ _given the types alone_. This is possible for, say, field narrowing---just pick out the fields you want out of a bigger record---but not for update, since the compiler has no way of turning, for instance, an `int`{.ocaml} into a `string`{.ocaml}. Stronger support for Rank-N Types ================================= Changes to how we handle subsumption have made it possible to store polymorphic values in not only tuples, but also general records. For instance: ```ocaml let foo = { apply : forall 'a 'b. ('a -> 'b) -> 'a -> 'b = fun x -> x } (* : { apply : forall 'a 'b. ('a -> 'b) -> 'a -> 'b } *) ``` `foo`{.ocaml} is a record containing a single polymorphic application function. It can be used like so: ```ocaml let () = let { apply } = foo apply (+ 1) 1 apply (fun x -> x) () ``` Pattern-matching Let ==================== A feature I've desired for a while now, `let` expressions (and declarations!) can have a pattern as their left-hand sides, as demonstrated above. These can be used for any ol' type, including for cases where pattern matching would be refutable. I haven't gotten around to actually implementing this yet, but in the future, pattern matching in `let`s will be restricted to (arbitrary) product types only. ```ocaml type option 'a = Some of 'a | None type foo = Foo of { x : int } let _ = let Some x = ... (* forbidden *) let Foo { x } = ... (* allowed *) ``` Even more "in-the-future", if we ever get around to adding attributes like OCaml's, the check for this could be bypassed by annotating the declaration with (say) a `[@partial]`{.ocaml} attribute. Unfortunately, since Amulet _is_ a strict language, these are a bit limited: They can not be recursive in _any_ way, neither directly nor indirectly. ```ocaml (* type error *) let (a, b) = (b, a) (* similarly *) let (a, b) = x and x = (a, b) ``` Cycle detection and warnings ============================ A verification pass is run over the AST if type-checking succeeds, to forbid illegal uses of recursion (strict language) and, as an additional convenience, warn when local variables go unused. For instance, this is [forbidden](/static/verify_error.png): ```ocaml let f = (fun x -> x) g and g = (fun x -> x) f ``` And this gives a [warning](/static/verify_warn.png): ```ocaml let _ = let { a, b } = { a = 1, b = 2 } () ``` Plans for this include termination (and/or productivity) (as a warning) and exhaustiveness checks (as an error). No more `main` ============ Since pattern-matching `let`{.ocaml}s are allowed at top-level, there's no more need for `main`. Instead of ```ocaml let main () = ... ``` Just match on `()`{.ocaml} at top-level: ```ocaml let () = ... ``` This gets rid of the (not so) subtle unsoundness introduced by the code generator having to figure out how to invoke `main`, and the type checker not checking that `main` has type `unit -> 'a`{.ocaml}, and also allows us to remove much of the silly special-casing around variables called `main`. ```ocaml let main x = x + 1 (* attempt to perform arithmetic on a nil value *) ``` Implicit Parameters =================== A bit like Scala's, these allow marking a function's parameter as implicit and having the type checker find out what argument you meant automatically. Their design is based on a bit of reading other compiler code, and also the paper on modular implicits for OCaml. However, we do not have a ML-style module system at all (much to my dismay, but it's being worked on), much less first class modules. Implicit parameters allow ad-hoc overloading based on dictionary passing (like type classes, but with less inference). ```ocaml type show 'a = Show of 'a -> string let show ?(Show f) = f let implicit show_string = Show (fun x -> x) let "foo" = show "foo" ``` Here, unification makes it known that `show` is looking for an implicit argument of type `show string`{.ocaml}, and the only possibility is `show_string`{.ocaml}, which is what gets used. Implicit laziness ================= There is a built-in type `lazy : type -> type`{.ocaml}, a function `force : forall 'a. lazy 'a -> 'a` for turning a thunk back into a value, and a keyword `lazy` that makes a thunk out of any expression. `lazy 'a`{.ocaml} and `'a` are mutual subtypes of eachother, and the compiler inserts thunks/`force`{.ocaml}s where appropriate to make code type-check. ```ocaml let x && y = if x then force y else false let () = false && launch_the_missiles () (* no missiles were launched in the execution of this program *) ``` General refactoring =================== - Literal patterns are allowed for all types, and they're tested of using `(==)`. - Amulet only has one type constructor in its AST for all its kinds of functions: `forall 'a. 'a -> int`{.ocaml}, `int -> string`{.ocaml} and `show string => unit`{.ocaml} are all represented the same internally and disambiguated by dependency/visibility flags. - Polymorphic recursion is checked using "outline types", computed before fully-fledged inference kicks in based solely on the shape of values. This lets us handle the function below without an annotation on its return type by computing that `{ count = 1 }` _must_ have type `{ count : int }` beforehand. Combined with the annotation on `x`, this gives us a "full" type signature, which lets us use checking for `size`, allowing polymorphic recursion to happen. ~~~{.ocaml} type nested 'a = Nested of nested ('a * 'a) * nested ('a * 'a) | One of 'a let size (x : nested 'a) = match x with | One _ -> { count = 1 } | Nested (a, _) -> { count = 2 * (size a).count } ~~~ - The newtype elimination pass was rewritten once and, unfortunately, disabled, since it was broken with some touchy code. - Operator declarations like `let 2 + 2 = 5 in 2 + 2` are admissible. - Sanity of optimisations is checked at runtime by running a type checker over the intermediate language programs after all optimisations - Local opens are allowed, with two syntaxes: Either `M.( x + y)`{.ocaml} (or `M.{ a, b }`{.ocaml}) or `let open M in x + y`{.ocaml}. - Amulet is inconsistent in some more ways, such as `type : type` holding. - There are no more kinds. Conclusion ========== This post was a bit short, and also a bit hurried. Some of the features here deserve better explanations, but I felt like giving them an _outline_ (haha) would be better than leaving the blag to rot (yet again). Watch out for a blog post regarding (at _least_) implicit parameters, which will definitely involve the changes to subtyping involving records/tuples.