Skip to content

Values and types

The lexical and type surface of the Emet language. For the standard-library functions these types thread through, see the prelude reference. For the four glyph constructors, see the four glyphs.

Literals

KindTypeExamples
IntegerInt0, 42, -7
FloatFloat3.14, 0.0, -2.5
StringString"nginx", "port ${String.fromInt n}"
CharChar'c', '\n', '\u{1F600}'
BoolBoolTrue, False

String interpolation

${expr} inside a string embeds an expression. The embedded expression must have type String.

greeting : String -> String
greeting name = "hello ${name}"

\${ writes a literal ${. Interpolation desugars to String.concat at compile time; the compiled value carries only the final string, never a placeholder.

Char

A Char is exactly one Unicode scalar, written between single quotes. The escapes are \n, \t, \\, \', and \u{...} (one to six hex digits).

newline : Char
newline = '\n'
smile : Char
smile = '\u{1F600}'

Char is comparable and orderable by codepoint. It is authoring-time only and never appears in a glyph field.

String escapes

Inside "...": \n, \t, \", \\, \${, and \u{...}.

Operators

Every operator desugars to a prelude builtin (a + b becomes add a b).

OperatorMeaningPrecedenceAssociativity
^exponent7right
* / //multiply, float divide, integer divide7left
+ -add, subtract6left
++append5right
::cons (prepend to list)5right
== /= < <= > >=equality and comparison4non-associative
&&logical and3right
||logical or2right

/ is float division; // is integer division. Division by zero returns 0.

Level-4 operators are non-associative: a < b < c is a compile error. Add parentheses.

Unary minus negates: -x desugars to negate x.

Constraints

Three built-in bounded type variables restrict which concrete types a polymorphic operator or function admits. They are not user-extensible; there are no user-defined typeclasses.

ConstraintAdmitsUsed by
numberInt, Float+ - * / // ^ and integer literals
comparableInt, Float, String, Char, and tuples of comparables== /= < <= > >=
appendableString, List a++

An integer literal is a number, defaulting to Int if nothing forces it to Float. A float literal is always Float. appendable shares no admissible type with number or comparable, so those constraints never combine.

Type annotations

A signature line binds a name to a type; it may immediately precede a declaration. A signature is optional — an unannotated declaration is inferred and generalized.

port : Int
port = 8080
endpoint : String -> Int -> String
endpoint host p = "${host}:${String.fromInt p}"

Function types associate to the right: A -> B -> C is A -> (B -> C). Signatures may name type variables (a, b), which range over all types:

identity : a -> a
identity x = x

A signature more general than the inferred body is rejected.

Built-in types

Int, Float, String, Bool, Char, Order, Maybe a, and List a. Bool (True / False), Order (LT / EQ / GT), and Maybe (Just a / Nothing) are sum types supplied by the prelude.

The glyph and scroll types — AptPackage, SystemdService, Filesystem, LineInFile, their sum Glyph, the filesystem Entry sum, and Scroll — are documented in the four glyphs.

User types

Sum types

type Role = Web | Db

Each variant is a value constructor. A variant may carry fields, written as type atoms after the constructor name:

type Shape = Circle Float | Rectangle Float Float

An applied constructor as a field must be parenthesized — Node (Tree a) a — because a bare applied head reads as separate fields.

Parameterized types

A type constructor may take parameters, listed as lowercase names before =:

type Tree a = Leaf | Node (Tree a) a (Tree a)

The type constructor Tree has arity 1; each variant’s result type is Tree a. Both nullary and parameterized user types cross module boundaries.

Records

A record type lists its field types; a record literal lists field values. Records are structural — there is no named record declaration.

host : { name : String, port : Int } -> Scroll
host h = scroll { name = h.name, glyphs = [] }

h.field accesses a field. Field access is row-polymorphic: .name constrains its argument to some record that has at least a name field of the right type, not one exact record shape. A function reading h.name accepts any record carrying a matching name. A record literal has a closed row — exactly the fields written, no more.

Record update

{ r | field = value, … } produces a copy of r with the named fields replaced. Everything else is carried through.

defaults : { domain : String, port : Int, tls : Bool }
defaults =
{ domain = "example.org"
, port = 8080
, tls = False
}
secured : { domain : String, port : Int, tls : Bool }
secured = { defaults | port = 443, tls = True }

Update is type-preserving: a new value must have the type the field already has, and the result has the base’s type. An update changes what a record holds, never its shape — a record literal is how the shape changes.

Updating a field the record does not have is always a compile error. How it reads depends on what is known at the update. When the base’s type is already a closed record — an annotated value, or a literal — the error names the offending field, suggests a near spelling, and lists the fields the record does have. When the base is an inferred or row-polymorphic record, as in the setter below, the row absorbs the unknown field at the update and the mismatch surfaces at the call site instead, reading as two record types that differ rather than naming the field.

At least one field is required; { r | } is a parse error.

The base is any expression, not only a variable. It is unified against an open record demanding just the named fields, so an unannotated setter is row-polymorphic in the same way field access is, and one function updates every record shape carrying that field:

withPort p r = { r | port = p }
service = withPort 443 { name = "web", port = 8080 }
listener = withPort 9000 { port = 80, backlog = 128 }

Tuples and unit

A tuple groups a fixed number of values positionally. Emet allows tuples of two or three elements; a tuple of four or more is a compile error — use a record with named fields instead.

point : (Int, Int)
point = (1, 2)
labelled : (String, Int, Bool)
labelled = ("web", 8080, True)

The type (A, B) / (A, B, C) mirrors the value. The parenthesized forms read by element count: (e) is grouping (Emet has no one-tuple), (a, b) and (a, b, c) are tuples, and () is unit — the zero-element tuple, its own type and value.

nothingUseful : ()
nothingUseful = ()

Tuples are comparable when their elements are, compared lexicographically — element by element, left to right — so <, ==, compare, min, and max all work on them. Unit is vacuously comparable and compares equal to itself. Like Char, tuples are authoring-time only and never appear in a glyph field. The Tuple module builds and transforms pairs.

See also