docs/syntax/types.md
Enso is a statically typed language, meaning that every variable is associated with information about the possible values it can take. In Enso, the type language is the same as the term language, with no artificial separation. For more information on the type system, please see the types design document.
This section will refer to terminology that has not been defined in this document. This is as this document is a specification rather than a guide, and it is expected that you have read the above-linked document on the type-system design as well.
Additionally, this document will colloquially refer to the left and right hand
sides of the type ascription operator : as the 'term' and 'type' levels,
respectively. In reality, there is no separation between the two in Enso, but it
is a useful way of thinking about things when discussing type signatures.
Enso allows users to provide explicit type signatures for values through use of
the type ascription operator :. The expression a : b says that the value a
has the type b attributed to it.
foo : (m : Monoid) -> m.this
Type signatures in Enso have some special syntax:
The reserved name in is used to specify the monadic context in which a value
resides. The Enso expression a in IO is equivalent to the Haskell
MonadIO a.
foo : Int -> Int in IO
The operator ! is used to specify the potential for an error value in a
type. The expression a ! E says that the type is either an a or an error
value of type E.
/ : Number -> Number -> Number ! ArithError
Please note that :, in, and ! all behave as standard operators in Enso.
This means that you can section them, which is incredibly useful for programming
with types. In addition, Enso supports a number of additional operators for
working with types. These are listed below.
| Operator | Precedence Relations | Level | Assoc. | Description |
|---|---|---|---|---|
: | > = | 0 | Left | Ascribes the type (the right operand) to the value of the left operand. |
in | > :, > ! | 3 | Left | Ascribes the context (the right operand) to the value of the left operand. |
! | > :, > -> | 2 | Left | Combines the left operand with the right operand as an error value. |
-> | > : | 1 | Left | Represents a mapping from the left operand to the right operand (function). |
<: | > !, < |, > in | 4 | Left | Asserts that the left operand is structurally subsumed by the right. |
~ | == <: | 4 | Left | Asserts that the left and right operands are structurally equal. |
; | < :, > = | -2 | Left | Concatenates the left and right operand typesets to create a new typeset. |
| | > <:, > !, > in, > : | 5 | Left | Computes the union of the left and right operand typesets. |
& | > | | 6 | Left | Computes the intersection of the left and right operand typesets. |
:= | < :, > =, > ; | -1 | Left | Creates a typeset member by assigning a value to a label. |
Solving this set of inequalities produces the relative precedence levels for
these operators shown in the table above. In order to check this, you can use
the following formula as an input to an SMTLib compatible solver. For reference,
bind (=) has a relative level of -3 in this ordering.
(declare-fun ascrip () Int) ; `:`
(declare-fun bind () Int) ; `=`
(declare-fun in () Int) ; `in`
(declare-fun err () Int) ; `!`
(declare-fun fn () Int) ; `->`
(declare-fun sub () Int) ; `<:`
(declare-fun eq () Int) ; `~`
(declare-fun tsConcat () Int) ; `;`
(declare-fun tsUnion () Int) ; `|`
(declare-fun tsInter () Int) ; `&`
(declare-fun tsMember () Int) ; `:=`
(assert (> ascrip bind))
(assert (> in ascrip))
(assert (> in err))
(assert (> err ascrip))
(assert (> err fn))
(assert (> fn ascrip))
(assert (> sub err))
(assert (< sub tsUnion))
(assert (> sub in))
(assert (= eq sub))
(assert (< tsConcat ascrip))
(assert (> tsConcat bind))
(assert (> tsUnion sub))
(assert (> tsUnion err))
(assert (> tsUnion in))
(assert (> tsUnion ascrip))
(assert (> tsInter tsUnion))
(assert (> minus tsInter))
(assert (< tsMember ascrip))
(assert (> tsMember bind))
(assert (> tsMember tsConcat))
(check-sat)
(get-model)
(exit)
A permalink to the program using an online Z3 console can be found here.
The actionables for this section are:
- Decide which of these should be exposed in the surface syntax.
Sometimes it is useful or necessary to write a typeset literal in your code. These work as follows.
Typeset Member: Syntax for typeset members have three components:
This looks like the following:
label : Type := value
Please note that the right-hand-side of the := operator is not a pattern
context.
Member Concatenation: Members can be combined into a typeset using the
concatenation operator ;.
x ; y
Typeset Literals: A typeset literal consists of zero or more typeset
member definitions concatenated while surrounded by curly braces {}. The
braces are necessary as they delimit a pattern context to allow the
introduction of new identifiers.
{ x: T ; y: Q }
Typeset literals are considered to be a pattern context, and hence the standard rules apply.
When ascribing a type to a value, there are two main ways in which it can be
done. Both of these ways are semantically equivalent, and ascribe the type
given by the signature (to the right of the :) to the expression to the left
of the :.
Inline Ascription: Using the type ascription operator to associate a type signature with an arbitrary expression.
my_expr : Type
Freestanding Ascription: Using the type ascription operator to associate a type with a name. The name must be defined on the line below the ascription.
a : Type
a = ...
Binding Ascription: Using the type ascription operator to associate a type with a binding at the binding site.
a : Type = ... # this is equivalent to the above example
(a : Type) -> ... # use in a lambda
The actionables for this section are:
- In the future do we want to support freestanding ascription that isn't directly adjacent to the ascribed value?
In Enso, a type signature operates to constrain the values that a given variable can hold. Type signatures are always checked, but Enso may maintain more specific information in the type inference and checking engines about the type of a variable. This means that:
: in any scope in
which the identifier is visible.From a syntactic perspective, the type ascription operator : has the following
properties:
: introduces a new scope on its RHS.b:A only
introduces constraints to b.The actionables for this section are:
- How do signatures interact with function bodies in regards to scoping?
- Does this differ for root and non-root definitions?
Enso also provides a set of rich operations on its underlying type-system notion of typesets. Their syntax is as follows:
|: The resultant typeset may contain a value of the union of its
arguments.&: The resultant typeset may contain values that are
members of both its arguments.The actionables for this section are:
- Unify this with the types document at some point. The types document supersedes this section while this actionable exists.
Types in Enso are defined by using the type reserved name. This works in a
context-dependent manner that is discussed properly in the
type system design document, but is summarised briefly
below.
Body with Atom Definitions: If you provide a body with atom definitions, this defines a smart constructor that defines the atoms and related functions by returning a typeset.
type Maybe
Nothing
Just (value : Integer)
is_just self = case self of
Maybe.Nothing -> False
Maybe.Just _ -> True
nothing self = self.is_just.not
To reference atoms use type name followed by the name of the atom. E.g.
Maybe.Nothing or Maybe.Just 2. Atom constructors act like functions and
fully support currying - e.g. one can create fn = Maybe.Just and later apply
two to it (fn 2) to obtain new atom.
Constructor arguments may be defined on the same line as above (similarly to the syntax for defining functions); alternatively, each argument may be defined on its own line in an indented block following the constructor name. This can make complex constructor definitions easier to read:
type Maybe
Nothing
Just
value : Integer
Autoscoped Constructors: Referencing constructors via their type name may
lead to long and boilerplate code. To simplify referencing constructors when
the context is known a special .. syntax is supported. Should there be a
method describe:
describe (m : Maybe) -> Text = if m.is_just then m.value.to_text else "Empty"
one may invoke it as describe (Maybe.Just 5) - e.g. the regular way. Or one
may use autoscoped constructors and call
describe (..Just 5)
the argument (..Just 5) is recorded but not executed until it is send to
the describe method. The argument of the describe method is known to be of
type Maybe and have Just constructor. The scope is now known and the so
far deferred .. value gets evaluated. Maybe.Just 5 atom is constructed and
execution of describe method continues with such atom.
Body Without Atom Definitions: If you provide a body and do not define any atoms within it, this creates an interface that asserts no atoms as part of it.
type HasName
name: String
In addition, users may write explicit this constraints in a type definition,
using the standard type-ascription syntax:
type Semigroup
<> : this -> this
type Monoid
this : Semigroup
use Nothing
When defining an atom in the body of a type (as described above), there are two ways in which you can define an atom:
type keyword inside the body of a type
will define a new atom with the fields you specify. The syntax for doing
this is the same as that of a bare atom.type Just value
Nothing
While we don't usually like making things private in a programming language, it sometimes the case that it is necessary to indicate that certain fields should not be touched (as this might break invariants and such like). To this end, we propose an explicit mechanism for access modification that works as follows:
We have a set of access modifiers, namely private and unsafe.
We can place these modifiers before a top-level definition.
type MyAtomType
type MyAtom a
is_foo : Boolean
is_foo = ...
private private_method a b = ...
unsafe unsafe_method a b = ...
By default, accessing any member under an access modifier is an error when performed from another module.
To use members protected by an access modifier, you must import that access modifier from the file in which you want to access those elements.
import private Base.Data.Vector
import unsafe Base.Data.Atom
These modified imports are available in all scopes, so it is possible to limit the scope in which you have access to the modified definitions.
function_using_modifiers v x =
import private Base.Data.Vector
import unsafe Base.Data.Atom
v.mutate_at_index 0 (_ -> x)
x = MyAtom.mutate_field name="sum" (with = x -> x + 20)
x + 20
While private works as you might expect, coming from other languages, the
unsafe annotation has additional restrictions:
Base.Unsafe.unsafe, you must write a documentation comment on its usage
that contains a section Safety that describes why this usage of unsafe is
valid.