The implicit conversions of DMN 1.6, section 10.3.2.9.4.
When a value sits where a particular type is expected, it can sometimes be converted to that type without losing information. The conversions are:
- from a singleton list: a list with exactly one element, where the element's type is expected, becomes that element.
- to a singleton list: a value, where a list of its type is expected, becomes a one-element list.
- date to date and time: a date becomes a date and time at midnight UTC.
- decimal to integer is done by the built-in functions that expect integers, not here.
- to the signature that fits (D-197): a built-in function that has a signature for each kind of its argument,
where a function type is expected that none of its function types is within, becomes the function of the signature
that fits. It is not in the list of the specification, whose lattice has no type for such a function; the DMN TCK
passes
absto afunction<number> -> number(0092 #014).
They apply where a value is bound to a parameter, to the source of a filter or iteration, and to the output of a decision. They do not apply to the operands of operators.
Guarded recursive targets from Feel.ItemDefinitions use the same conversions after unfolding
their declaration head. A recursive collection, including an alias of one, can wrap a singleton;
a recursive context can unwrap one. Membership checks the full finite value, without converting
fields inside contexts. Allowed values and type constraints are separate declaration checks:
coerce/3 and convert/3 alone do not enforce them.
The order
The specification gives no order. coerce/3 checks conformance first, so a value
that already fits is returned unchanged: a singleton list passed for an Any
parameter stays a list, and null is not wrapped. Only when it does not fit are
the conversions tried, in this order: from a singleton list, to a singleton list,
date to date and time, to the signature that fits. At most one is applied.
With compose: true, the two combinations the notes describe are also allowed: a
singleton list holding a date where a date and time is expected, and a date where
a list of date and time is expected.
Summary
Types
One conversion that was applied: :from_singleton (a one-element list became its
element), :to_singleton (a value became a one-element list), :date_to_date_time,
:function_signature (a built-in function became its signature that fits the function type, D-197), or
:decimal_to_integer (a number lost its fractional part: only an argument of a built-in function that wants an
integer, Feel.Builtin.Arguments, since the types have no integer, and convert/3 does not make it).
Functions
Converts value to fit type, or returns :error when it cannot be done. The
caller turns :error into null and a diagnostic.
Like coerce/3, and also says what was done: the steps that were applied, in order, or
[] for a value that fits as it is.
Describes steps in a sentence, for a diagnostic.
Types
@type step() ::
:from_singleton
| :to_singleton
| :date_to_date_time
| :function_signature
| :decimal_to_integer
One conversion that was applied: :from_singleton (a one-element list became its
element), :to_singleton (a value became a one-element list), :date_to_date_time,
:function_signature (a built-in function became its signature that fits the function type, D-197), or
:decimal_to_integer (a number lost its fractional part: only an argument of a built-in function that wants an
integer, Feel.Builtin.Arguments, since the types have no integer, and convert/3 does not make it).
Functions
@spec coerce(Feel.Value.t(), Feel.Type.t(), [{:compose, boolean()}]) :: {:ok, Feel.Value.t()} | :error
Converts value to fit type, or returns :error when it cannot be done. The
caller turns :error into null and a diagnostic.
Options
:compose- also allow the two combined conversions. Defaults tofalse.
Examples
iex> Feel.Type.Conversion.coerce(["foobar"], :string)
{:ok, "foobar"}
iex> Feel.Type.Conversion.coerce(Decimal.new(3), {:list, :number})
{:ok, [Decimal.new(3)]}
iex> Feel.Type.Conversion.coerce(["a", "b"], :string)
:error
iex> Feel.Type.Conversion.coerce(["foobar"], :any)
{:ok, ["foobar"]}
@spec convert(Feel.Value.t(), Feel.Type.t(), [{:compose, boolean()}]) :: {:ok, Feel.Value.t(), [step()]} | :error
Like coerce/3, and also says what was done: the steps that were applied, in order, or
[] for a value that fits as it is.
iex> Feel.Type.Conversion.convert("foobar", :string)
{:ok, "foobar", []}
iex> Feel.Type.Conversion.convert(["foobar"], :string)
{:ok, "foobar", [:from_singleton]}
iex> Feel.Type.Conversion.convert(Decimal.new(3), {:list, :number})
{:ok, [Decimal.new(3)], [:to_singleton]}
iex> {:ok, %Feel.Value.DateTime{}, [:date_to_date_time]} =
...> Feel.Type.Conversion.convert(~D[2020-01-01], :date_and_time)
iex> {:ok, [%Feel.Value.DateTime{}], steps} =
...> Feel.Type.Conversion.convert(~D[2020-01-01], {:list, :date_and_time}, compose: true)
iex> steps
[:to_singleton, :date_to_date_time]
iex> Feel.Type.Conversion.convert(["a", "b"], :string)
:error
iex> {:ok, abs} = Feel.Builtin.value("abs")
iex> {:ok, number, [:function_signature]} = Feel.Type.Conversion.convert(abs, {:function, [:number], :number})
iex> Feel.Value.Function.types(number)
[{:function, [:number], :number}]
Describes steps in a sentence, for a diagnostic.
iex> Feel.Type.Conversion.describe([:from_singleton])
"a list with one element is replaced by that element"
iex> Feel.Type.Conversion.describe([:to_singleton, :date_to_date_time])
"a value is wrapped in a list, and then a date becomes a date and time at midnight UTC"
iex> Feel.Type.Conversion.describe([:from_singleton, :decimal_to_integer])
"a list with one element is replaced by that element, and then the fractional part of a number is discarded"
iex> Feel.Type.Conversion.describe([:function_signature])
"a built-in function with a signature for each kind it takes becomes the one that fits the type"