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