finos/morphir-elm/10.0.0

NOTE: the latest version is 22.0.2

Formerly Morgan-Stanley/morphir-elm. Search finds this package by either name.

Morphir.IR.Type

Like any other programming languages Morphir has a type system as well. This module defines the building blocks of that type system. If you want to learn more about type systems check out Wikipedia: Type system.

Morphir's type system is heavily inspired by Elm's type system so the best way to understand the building blocks here is through some Elm examples. Let's take this bit of Elm code as a starting point:

type alias MyInteger =
    Int


type alias MyRecord a =
    { foo : List a
    }


type Foo a
    = Bar a
    | Baz

These would translate to type definitions in Morphir which is represented by the Definition type. Definitions themselves don't have a name. It's the Module that contains that information in the types dictionary as a key. The type parameters and the right-hand side of the declaration is contained in the Definition type itself. This is how the above would translate to the IR (some parts are omitted to reduce noise):

{ types =
    Dict.fromList
        [ ( [ "my", "integer" ], TypeAliasDefinition [] (...) )
        , ( [ "my", "record" ], TypeAliasDefinition [ [ "a" ] ] (...) )
        , ( [ "foo" ], CustomTypeDefinition [ [ "a" ], [ "b" ] ] (...) )
        ]
, values =
    Dict.empty
}

Type aliases simply assign a new name to a type expression. This type expression can be a reference to another type or a record type or any other type expression. Custom types are defined by a list of constructors. Each of those constructors have a list of arguments. Each argument is a type expression.

Here is the full definition for reference:

{ types =
    Dict.fromList
        [ ( [ "my", "integer" ]
          , TypeAliasDefinition []
                (Reference (fqn "Morphir.SDK" "Basics" "Int") [])
          )
        , ( [ "my", "record" ]
          , TypeAliasDefinition [ [ "a" ] ]
                (Record ()
                    [ Field [ "foo" ] (Reference () (fqn "Morphir.SDK" "List" "List") [ Variable () [ "a" ] ])
                    ]
                )
          )
        , ( [ "foo" ]
          , CustomTypeDefinition [ [ "a" ], [ "b" ] ]
                (AccessControlled.public
                    [ Constructor [ "bar" ] [ ( [ "arg", "1" ], Variable () [ "a" ] ) ]
                    , Constructor [ "baz" ] [ ( [ "arg", "1" ], Variable () [ "b" ] ) ]
                    ]
                )
          )
        ]
, values =
    Dict.empty
}

Type Expression

type Type a
= Variable a Name
| Reference a FQName (List (Type a))
| Tuple a (List (Type a))
| Record a (List (Field a))
| ExtensibleRecord a Name (List (Field a))
| Function a (Type a) (Type a)
| Unit a

Represents a type expression that can appear in various places within the IR. It can be the right-hand-side of a type alias declaration, input and output types of a function or as an annotation on values after type inference is done.

Type are modeled as expression trees: a recursive data structure with various node types. The type argument a allows us to assign some additional attributes to each node in the tree. Here are some details on each node type in the tree:

  • Variable
  • Reference
    • A fully-qualified reference to some other type or type alias within the package or one of its dependencies.
    • References to built-in types (like Int, String, ...) don't have an associated definition.
    • creation, matching
  • Tuple
    • A tuple is a composition of other types (potentially other tuples).
    • The order of types is relevant so the easiest way to think about it is as a list of types.
    • A tuple can have any number of elements and there is no restriction on the element types.
    • A tuple with zero elements is equivalent with Unit.
    • A tuple with a single element is equivalent to the element type itself.
    • Wikipedia: Tuple
    • creation, matching
  • Record
    • A record is a composition of other types like tuples but types are identified by a field name instead of an index.
    • The best way to think of a record is as a dictionary of types.
    • Our representation captures the order of fields for convenience but the order should not be considered for type equivalence.
    • [Wikipedia: Record](https://en.wikipedia.org/wiki/Record_(computer_science))
    • Elm-lang: Records
    • Utilities: creation, matching
  • ExtensibleRecord
    • Similar to records but while record types declare that the underlying object has "exactly these fields" an extensible record declares that the object has "at least these fields".
    • Besides the list of fields you need to specify a variable name that will be used to abstract over the type that's being extended.
    • Elm: Extensible records
    • creation, matching
  • Function
    • Represents the type of a function. The two arguments are the argument and the return type of the function.
    • Multi-argument functions are represented by composing functions:
      • a -> b -> c is represented as a -> (b -> c)
    • Wikipedia: Function type
    • creation, matching
  • Unit
    • Unit type is used as a placeholder in situations where a type is required but the corresponding value is unused.
    • Semantically the unit type represents a set that has exactly one value which is often called unit.
    • Unit corresponds to void in some other programming languages.
    • Wikipedia: Unit type
    • creation, matching

Creation

variable : a -> Name -> Type a

Creates a type variable.

toIR a == variable [ "a" ] ()


toIR fooBar == variable [ "foo", "bar" ] ()
reference : a -> FQName -> List (Type a) -> Type a

Creates a fully-qualified reference to a type.

toIR (List Int)
    == reference SDK.List.listType [ intType ]


toIR Foo.Bar
    == reference
        ( [ [ "my" ], [ "lib" ] ], [ [ "foo" ] ], [ "bar" ] )
        []
tuple : a -> List (Type a) -> Type a

Creates a tuple type.

toIR ( Int, Bool )
    == tuple [ basic intType, basic boolType ]
record : a -> List (Field a) -> Type a

Creates a record type.

toIR {} == record []


toIR { foo = Int }
    == record
        [ field [ "foo" ] SDK.Basics.intType
        ]


toIR { foo = Int, bar = Bool }
    == record
        [ field [ "foo" ] SDK.Basics.intType
        , field [ "bar" ] SDK.Basics.boolType
        ]
extensibleRecord : a -> Name -> List (Field a) -> Type a

Creates an extensible record type.

toIR { e | foo = Int }
    == extensibleRecord (variable [ "e" ])
        [ field [ "foo" ] intType
        ]


toIR { f | foo = Int, bar = Bool }
    == extensibleRecord (variable [ "f" ])
        [ field [ "foo" ] intType
        , field [ "bar" ] boolType
        ]
function : a -> Type a -> Type a -> Type a

Creates a function type. Use currying to create functions with more than one argument.

toIR (Int -> Bool) ==
    function
        SDK.Basics.intType
        SDK.Basics.boolType


toIR (Int -> Bool -> Char) ==
    function
        intType
        (function
            SDK.Basics.boolType
            SDK.Basics.charType
        )
unit : a -> Type a

Creates a unit type.

toIR () == unit

Record Field

type alias Field a =
{ name : Name
, tpe : Type a
}

An opaque representation of a field. It's made up of a name and a type.

mapFieldName : (Name -> Name) -> Field a -> Field a

Map the name of the field to get a new field.

mapFieldType : (Type a -> Type b) -> Field a -> Field b

Map the type of the field to get a new field.

Specification

= TypeAliasSpecification (List Name) (Type a)
| OpaqueTypeSpecification (List Name)
| CustomTypeSpecification (List Name) (Constructors a)
customTypeSpecification : List Name -> Constructors a -> Specification a

Definition

type Definition a
= TypeAliasDefinition (List Name) (Type a)
| CustomTypeDefinition
(List Name)
(AccessControlled (Constructors a))

This syntax represents a type definition. For example:

  • type alias Foo a = {bar : Maybe a, qux : Int}
  • type MyList a = End | Cons a (MyList a)

In the definition, the List Name refers to type parameters on the LHS and Type extra refers to the RHS

typeAliasDefinition : List Name -> Type a -> Definition a
customTypeDefinition : List Name -> AccessControlled (Constructors a) -> Definition a

Constructors

type alias Constructors a =
Dict Name (List ( Name, Type a ))

Constructors in a dictionary keyed by their name. The values are the argument types for each constructor.

Mapping

mapTypeAttributes : (a -> b) -> Type a -> Type b
(Type a -> Result e (Type b))
-> Result (List e) (Definition b)

#Utilities

collectVariables : Type ta -> Set Name

Collect all variables in a type recursively.

substituteTypeVariables : Dict Name (Type ta) -> Type ta -> Type ta

Substitute type variables recursively.