The FEEL type system (DMN 1.6, section 10.3.2.9).
Types form a lattice with :any on top and :null at the bottom. Apart from
the parameterised families (list, range, context, function), the
lattice is flat: the primitive types do not conform to each other, and in
particular :date does not conform to :date_and_time. There are no union
types, and no common supertype of the two duration types.
t/0 | FEEL |
|---|---|
:any, :null | Any, Null |
:boolean, :number, :string | boolean, number, string |
:date, :time | date, time |
:date_and_time | date and time |
:years_and_months_duration | years and months duration |
:days_and_time_duration | days and time duration |
{:list, type} | list<type> |
{:range, type} | range<type> |
{:context, %{"k" => type}} | context<k: type> |
{:function, [type], result} | function<type> -> result |
{:recursive, name, graph} | guarded recursive item data |
{:named, "Name"} | a model-defined type, by name |
{:non_null, type} | type! (an extension, D-48) |
{:not, type} | Any except type (an extension, D-178) |
{:union, [type, ...]} | type | type (an extension, D-178) |
A context type lists its members by name; the order of members does not matter. Parameter names are not part of a function type.
The last three go past the lattice: T! says that a name is never null, Any except T that it is not
a T, and T | U that it is one or the other. They are for what a host declares (Feel.Scope, the result type of an
expression, a type of the scope that an expression names), and parse/2 reads them; the types in an expression (instance of, the
parameters of a function) are those of the specification and T!. See Feel.Type.Composite for what the last two mean.
Types and values
This module describes types as data (t/0), for checking and inference. It
does not hold values. The values of each FEEL type, and how they are
represented, are in the modules under Feel.Value, such as Feel.Value.Number
and Feel.Value.List. Each of them has a type/0 (or type/1 for the
parameterised types) that returns its descriptor, so Feel.Value.Number.type()
is :number. Feel.Value itself is the type Any.
What is here
- Relations between types:
conforms?/2,equivalent?/2, andjoin/2(seeFeel.Type.Conformance). - Relations between values and types:
of/1,instance?/2, andvalue_conforms?/2(seeFeel.Type.Instance). - Implicit conversion:
coerce/3(seeFeel.Type.Conversion). - Type expressions:
parse/2readslist<number>and the like, andformat/1writes them. Types defined by a model, like the item definitions of a DMN model, are found in theFeel.Scope, andresolve/2replaces a{:named, name}by its definition.
The relations work on resolved types, with no {:named, _} left in them.
In these types Null is under every type, so number means "a number or null". The
analyzer needs to say that a result is never null, and uses Feel.Type.Union, in
which the null is an alternative of its own (D-39).
Summary
Functions
Returns the built-in type with the given canonical name.
Returns the names of the built-in types, sorted.
Converts a value to fit a type with the implicit conversions of section
10.3.2.9.4, such as wrapping a value in a singleton list. Returns :error when
no conversion applies. See Feel.Type.Conversion.
Returns true when the first type conforms to the second: an instance of the
first can be used wherever the second is expected. See Feel.Type.Conformance.
Returns true when two types are equivalent. See Feel.Type.Conformance.
Formats a type as FEEL type syntax.
Turns a type node of the syntax tree (see Feel.Syntax.TypeParser) into a type.
The instance of operator (Table 62): null is an instance of :null only, and
every other value must fit the type. See Feel.Type.Instance.
Returns a conservative common supertype that both types conform to. It need not
be the least expressible supertype for functions or composite declarations.
See Feel.Type.Conformance.
Marks a type as not null (D-48): the type of a name that must have a value.
Returns the type of a value (Table 43). See Feel.Type.Instance.
Parses a type expression such as list<number> or context<a: string>.
Reads a type expression as the node of the syntax tree that from_ast/2 takes, with the places of the text. known is
the set of the names of the types that exist besides the built-in ones. :error for a text that is not a type
expression, which parse/2 says why.
Replaces every {:named, name} in a type by the definition found in the scope,
recursively.
Returns true when a value fits a type, where null fits every type. See
Feel.Type.Instance.
Types
@type t() :: :any | :null | :boolean | :number | :string | :date | :time | :date_and_time | :years_and_months_duration | :days_and_time_duration | {:list, t()} | {:range, t()} | {:context, %{optional(String.t()) => t()}} | {:function, [t()], t()} | {:named, String.t()} | Feel.Type.Recursive.t() | {:non_null, t()} | {:not, t()} | {:union, [t(), ...]}
Functions
@spec builtin(Feel.Name.t()) :: {:ok, t()} | :error
Returns the built-in type with the given canonical name.
iex> Feel.Type.builtin("date and time")
{:ok, :date_and_time}
iex> Feel.Type.builtin("Employee")
:error
@spec builtin_names() :: [Feel.Name.t()]
Returns the names of the built-in types, sorted.
@spec coerce(Feel.Value.t(), t(), [{:compose, boolean()}]) :: {:ok, Feel.Value.t()} | :error
Converts a value to fit a type with the implicit conversions of section
10.3.2.9.4, such as wrapping a value in a singleton list. Returns :error when
no conversion applies. See Feel.Type.Conversion.
Returns true when the first type conforms to the second: an instance of the
first can be used wherever the second is expected. See Feel.Type.Conformance.
Returns true when two types are equivalent. See Feel.Type.Conformance.
Formats a type as FEEL type syntax.
Context members are listed in key order, and an empty context type is written
context<>, which is not valid FEEL syntax.
iex> Feel.Type.format({:list, :number})
"list<number>"
iex> Feel.Type.format({:function, [:number, :string], :boolean})
"function<number, string> -> boolean"
iex> Feel.Type.format({:context, %{"name" => :string, "age" => :number}})
"context<age: number, name: string>"
iex> Feel.Type.format({:non_null, {:list, :number}})
"list<number>!"
iex> Feel.Type.format({:union, [:number, {:not, :string}]})
"number | (Any except string)"
iex> Feel.Type.format({:non_null, {:not, {:union, [:number, :string]}}})
"(Any except number | string)!"
@spec from_ast(Feel.AST.type_expression(), Feel.Scope.t()) :: {:ok, t()} | {:error, [Feel.Diagnostic.t(), ...]}
Turns a type node of the syntax tree (see Feel.Syntax.TypeParser) into a type.
Names are looked up among the built-in types and then in the scope. Returns the diagnostics of the first problem: an unknown or self-referential type name, or a context type with the same member twice.
@spec instance?(Feel.Value.t(), t()) :: boolean()
The instance of operator (Table 62): null is an instance of :null only, and
every other value must fit the type. See Feel.Type.Instance.
Returns a conservative common supertype that both types conform to. It need not
be the least expressible supertype for functions or composite declarations.
See Feel.Type.Conformance.
Marks a type as not null (D-48): the type of a name that must have a value.
A type that is already marked stays as it is. Null! has no values, so it is :error.
iex> Feel.Type.non_null(:number)
{:ok, {:non_null, :number}}
iex> Feel.Type.non_null({:non_null, :number})
{:ok, {:non_null, :number}}
iex> Feel.Type.non_null(:null)
:error
@spec of(Feel.Value.t()) :: t()
Returns the type of a value (Table 43). See Feel.Type.Instance.
@spec parse(String.t(), [{:scope, Feel.Scope.t()}]) :: {:ok, t()} | {:error, Feel.Error.t()}
Parses a type expression such as list<number> or context<a: string>.
The names of types defined by a model are looked up in the :scope option, a
Feel.Scope.t/0, and replaced by their definitions. A name that is neither
built in nor in the scope is an error, as is a name that refers to itself. A
context member can be a name or a string, so context<"first name": string>
works.
This is the reading of a type that a host declares, and it goes past the types of an expression (D-178): Any except T
is {:not, T} and T | U is {:union, [T, U]}, and a type can be put in parentheses. Any except takes everything
to its right up to the end of the bracket it is in, so Any except number | string is neither, and
(Any except number) | string is Any. A ! after a member of a union or after except is refused, since it is not
read; mark the whole type, (Any except number)!. The type of an expression (instance of, the parameters of a
function) is parsed by Feel.Syntax.TypeParser without these.
Errors
The error has kind :syntax when the text is not a type expression, and
:analysis when it is one but names a type that does not exist.
iex> Feel.Type.parse("list<date and time>")
{:ok, {:list, :date_and_time}}
iex> Feel.Type.parse("function<number, string> -> boolean")
{:ok, {:function, [:number, :string], :boolean}}
iex> Feel.Type.parse("list<number!>!")
{:ok, {:non_null, {:list, {:non_null, :number}}}}
iex> Feel.Type.parse("Any except number")
{:ok, {:not, :number}}
iex> Feel.Type.parse("number | list<string | boolean>")
{:ok, {:union, [:number, {:list, {:union, [:string, :boolean]}}]}}
iex> Feel.Type.parse("(Any except number | string)!")
{:ok, {:non_null, {:not, {:union, [:number, :string]}}}}
iex> {:error, error} = Feel.Type.parse("list<Employee>")
iex> {error.kind, error.code}
{:analysis, :invalid_type_expression}
@spec parse_ast(String.t(), [Feel.Name.t()]) :: {:ok, Feel.AST.type_expression()} | :error
Reads a type expression as the node of the syntax tree that from_ast/2 takes, with the places of the text. known is
the set of the names of the types that exist besides the built-in ones. :error for a text that is not a type
expression, which parse/2 says why.
iex> {:ok, {:non_null_type, _, {:type_name, _, ["number"]}}} = Feel.Type.parse_ast("number!", [])
iex> Feel.Type.parse_ast("number +", [])
:error
@spec resolve(t(), Feel.Scope.t()) :: {:ok, t()} | {:error, {:unknown_type | :cyclic_type, Feel.Name.t()}}
Replaces every {:named, name} in a type by the definition found in the scope,
recursively.
Returns {:error, {:unknown_type, name}} for a name that is not in the scope,
and {:error, {:cyclic_type, name}} for a type whose definition leads back to
itself through unresolved named references. Guarded descriptors produced by
Feel.ItemDefinitions.new/2 with recursive: :guarded are already resolved finite graphs;
they are retained without recursively expanding their back references.
iex> scope = Feel.Scope.put_type(%Feel.Scope{}, "Address", {:context, %{"zip" => :string}})
iex> Feel.Type.resolve({:list, {:named, "Address"}}, scope)
{:ok, {:list, {:context, %{"zip" => :string}}}}
iex> Feel.Type.resolve({:named, "Missing"}, scope)
{:error, {:unknown_type, "Missing"}}
@spec value_conforms?(Feel.Value.t(), t()) :: boolean()
Returns true when a value fits a type, where null fits every type. See
Feel.Type.Instance.