Skip to content

Latest commit

 

History

History

elab-record-patching

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Elaboration with Record Patching and Singleton Types

This is an implementation of a dependently typed language with dependent record types, with some additional features intended to make it more convenient to use records as first-class modules. It was originally ported from a gist by mb64, which was inspired by the language feature implemented in CoolTT.

The type system is implemented in terms of an ‘elaborator’, which type checks and tanslates a user-friendly surface language into a simpler and more explicit core language that is more closely connected to type theory.

This was originally posted at record-patching.ml.

Record patching

Record patching is a way to constrain the values of fields in a record type. Given a record type R, a record patch can be applied using the syntax R [ l := t; ... ]. For example:

let Monoid := {
  T : Type;
  empty : T;
  append : T -> T -> T;
};

let string-monoid : Monoid [ T := String ] := {
  empty := "";
  append := string-append;
};

This is like Standard ML’s where type syntax for type realisation, OCaml’s with operator for constraining module types, and Rust’s Iterator<Item = T> shorthand syntax for equality constraints in type parameter bounds.

Elaboration of patches to singleton types

Patches only exist as a feature of the surface language and are removed during elaboration. The expression Monoid [ T := String ] in the example above elaborates to a new record type, where the type of the T field is constrained to String through the use of a singleton type.

We also derive the definitions of missing fields in record literals from singletons in the expected type. This works nicely in combination with record patching. Note in the previous example how we don't need to define the field T in string-monoid.

With that in mind, the definition of string-monoid is elaborated to:

let string-monoid : {
  T : Type [= String]; -- singleton type patched here
  empty : T;
  append : T -> T -> T;
} := {
  T := String; -- definition taken from the singleton
  empty := "";
  append := string-append;
};

Future work

Total space conversion

CoolTT implements ‘total space conversion’ which automatically converts functions in the form F : { l : T; ... } -> Type to the record type { l : T; ..., fibre : F { l := l; ... } } where necessary. Apparently this could help address the ‘bundling problem’, and reduce the need to implement implicit function parameters.

Opaque ascription

Adding a ‘sealing operator’ e :> T to the surface language would allow us to opaquely ascribe a type T to an expression e. This would prevent the contents of the expression from reducing definitionally, allowing us to define abstract data types.

Opaque ascription is sometimes modelled using effects, as seen in the language 1ML. The paper “Logical Relations as Types: Proof-Relevant Parametricity for Program Modules” describes an effectful approach based on call-by-push-value that could be useful in the context of dependent types.

Apparently effects are only needed in the presence of mutable references, however. If we didn’t need these, we might be able implement sealing by hiding definitions behind function parameters. For example:

  • ... (e :> T) ... elaborates to ... (fun (x : T) := x ...) e
  • ... let x :> T := e; ... elaborates to ... ((fun (x : T) := ...) e)

Metavariables and unification

Implicit function types and unification could be convenient. This could be challenging to implement in the presence coercive subtyping, however. Apparently total space conversion addresses some of the same pain points as implicit parameters, but I'm still somewhat skeptical of this!

Patches elaborate to large, unfolded terms

Each patch currently elaborates to a copy of the original record type. This a problem for error messages, where the type ends up fully unfolded and to understand, and it could become a performance issue down the line when elaborating and compiling larger programs.

A distiller could attempt to convert singletons back to patches for better error messages, but to really address the usability and performance issues we might ultimately need to add patches to the core language and control the level of unfolding with glued evaluation.

Use patch syntax for record literal updates

The same syntax used by patches could be used as a way to update the fields of record literals.

Related work

This implementation is heavily based on mb64’s sketch implementation in Haskell but contains various bug fixes, alterations, and extensions.

Record patching was originally proposed and implemented for CoolTT in the setting of cubical type theory:

Reed Mullanix's presentation from WITS’22, Setting the Record Straight with Singletons (slides) provides a good description of the approach taken in CoolTT, which continues to be developed and improved.

Elaborating record patches to singleton types is similar to approaches developed for formalising and implementing type realisation in Standard ML, for example in “Extensional equivalence and singleton types”. Unlike this work, we avoid defining singletons in terms of extensional equality, which makes it much easier to maintain decideable type checking.