Feel.Builtin.Function (feel v0.1.0)

Copy Markdown View Source

A built-in function of FEEL, as the registry describes it (D-56, which settles D-56).

The area modules under Feel.Builtin list their functions with functions/0, and Feel.Builtin.function/1 finds one by name. A function has:

  • a name, canonical (see Feel.Name).
  • signatures: each a list of parameters (a name, the types it accepts, and whether it is optional) and the types that the result can be. The names let a call with named arguments be put in order (Feel.Analysis.Calls); the number of parameters, and how many are optional, decide how many arguments a call may have. The types are the description of the function and what the analyzer types a call by: whether an argument is one of them is decided by the function, when it runs.
  • a result_from of a signature, if the result depends on the types of the arguments and not only on the signature (D-151): max(xs) of a list<number> is a number and append(xs, "a") is a list of numbers and strings, where the types of the result say Any and list<Any>. It says how the type of the result is made from the types of the arguments (result_from/0), and Feel.Analysis.Calls.result_type/3 makes it.
  • a same_kind of a signature, if some of its arguments have to be of one kind that has an order and not only each of one that its parameter takes (D-155): the two points of before(point1, point2), the items of max(c1, c2). A parameter says what one argument may be, and this says that the arguments agree, which Feel.Analysis.Calls checks (same_kind/0).
  • call: the function itself. It takes the environment of the run (Feel.Runtime.Env, for the clock that now() reads) and the list of its arguments, already evaluated and in order, and returns an outcome (Feel.Runtime.Env.outcome/0): {:ok, value}, :missing for a null that is not a mistake, or {:error, diagnostic} for a null that is. It is called only with a number of arguments that some signature accepts.
  • pure?: whether the result depends on the arguments alone. A call of a pure function with constants is folded by the analyzer and the optimizer; now() and today() read the clock, so they are not.
  • never_null?: whether the result is never null for an argument of a signature, as the result of is. The analyzer types the result of any other function with a null.
  • check_arguments?: whether an argument that no signature takes is null with an :argument_out_of_domain error before the function is called, which the analyzer knows (Feel.Analysis.Calls.mismatch/3) and a call by name checks against its signature (D-56). It is false for a function whose own error for a wrong kind is another, as not, whose error is a type mismatch like the operators': the function says it, and what it says does not change.

Summary

Types

A number of arguments that a function takes: exactly that many, or that many and more (the arguments of a variadic parameter).

What a function returns. An overload of an extension (with_overloads/2) may also say :next: the arguments are not for it. :next does not leave Feel.Builtin.call/4: the overloads are tried by with_overloads/2, and a function that is not an overload and says it has the arguments refused, as an error of their domain.

What an argument that has to be of one kind may be of (same_kind/0): {:kinds, kinds} the kinds that have an order that it can be of, which is none for a value that has no order, or :free if it is of no kind in particular (it may be null, or anything). The analyzer says it of the type of an argument, and the run of its value.

How the type of a result is made from the types of the arguments (D-151), by the places of the arguments (from 0; a place at or after a variadic parameter is that argument and all that follow)

The arguments of a call that have to be of one kind that has an order (D-155), by the places of the arguments (from 0; a place at or after a variadic parameter is that argument and all that follow): a number, a string, a date, a time, a date and time, or a duration of one of the two kinds. The call is out of the domain when the kinds that the arguments may be of have nothing in common, as before("a", 2) is. A part is

A signature. One that an extension adds (Feel.Extension, D-81) has extension: true.

t()

Functions

Returns true when a call with this many arguments is one that some signature accepts.

Whether the arguments, as the kinds they may be of, agree: some kind that has an order is the kind of every one of them that is not :free. A single argument that cannot be of such a kind (it has no order) does not agree with itself, and no argument that is not free is one that agrees.

The arguments (or the values) that are at a place of a signature: the one at the place, and for a variadic parameter all that follow. A parameter that the call leaves out has none.

The numbers of arguments that a call may have: for each signature, from its required parameters to all of them. A signature with a variadic parameter takes {:at_least, n}, and that covers the numbers from n of the others.

The type of the elements of a type of a parameter that is a list.

The signatures of extensions that have a parameter of each of the names (D-81). A call by name is for one signature, the one for_names/2 chooses, but an extension may take the call all the same, with the same names for other kinds of argument; a call that one fits is the extension's to answer.

The signature that a call with named arguments is for: the first one that has a parameter of each of the names. The call is then put in the order of its parameters, and a parameter that the call leaves out is null (see Feel.Analysis.Calls).

Returns the function with the signatures that take each number of arguments, up to 12, kept in it (D-56): the answer of accepts?/2 and with_arity/2 is then a lookup and not a pass over the signatures, each of which works out its numbers of arguments from its parameters. Feel.Builtin.function/1 does this once for the table it keeps. A function whose signatures are changed has to lose it (with_overloads/2 does), since the index is of the signatures it was made from.

The signatures with those that differ only in types made one: the same parameters (the names, and whether each is optional, variadic, quiet or an integer) taking the types of all of them, and the same result with the types of all.

The kinds of value that have an order (Table 55), which is what a same_kind is about: a number, a string, a date, a time, a date and time, and a duration of either kind. A boolean, a list, a context, a range and a function have none.

A parameter: its name, the types it accepts, and optional: true if a call may leave it out.

The parameter that the argument at this place (from 0) is for: the one at that place, or for an argument past the last, the variadic parameter that ends the signature; nil if there is none.

Whether the argument at this place (from 0) is read quietly: every one of the signatures has a quiet parameter there.

The signature with the arguments that have to be of one kind (same_kind/0, D-155). nil says there are none.

A signature: its parameters and the types of its result, and how the result follows the types of the arguments if it does (result_from/0).

The kind that a type of a parameter stands for: a primitive type is itself, and a list, a range, a context and a function are :list, :range, :context and :function, whatever they are of. :any takes every kind.

Returns true for a variadic parameter.

The signatures that a signature from for_names/2 stands for: those of the function that differ from it only in types (merge_variants/1), or itself if it is one of an extension. A call by name is typed by each of them.

The signatures that accept this many arguments.

The function with the overloads of extensions added to it (Feel.Extension, D-81): its signatures are those of the function and then those of the overloads, in the order of the extensions.

Types

argument_count()

@type argument_count() :: non_neg_integer() | {:at_least, non_neg_integer()}

A number of arguments that a function takes: exactly that many, or that many and more (the arguments of a variadic parameter).

call_outcome()

@type call_outcome() :: Feel.Runtime.Env.outcome() | :next

What a function returns. An overload of an extension (with_overloads/2) may also say :next: the arguments are not for it. :next does not leave Feel.Builtin.call/4: the overloads are tried by with_overloads/2, and a function that is not an overload and says it has the arguments refused, as an error of their domain.

kind_slot()

@type kind_slot() :: {:kinds, [atom()]} | :free

What an argument that has to be of one kind may be of (same_kind/0): {:kinds, kinds} the kinds that have an order that it can be of, which is none for a value that has no order, or :free if it is of no kind in particular (it may be null, or anything). The analyzer says it of the type of an argument, and the run of its value.

parameter()

@type parameter() :: %{
  :name => String.t(),
  :types => [Feel.Type.t(), ...],
  :optional? => boolean(),
  optional(:variadic?) => true,
  optional(:quiet?) => true,
  optional(:integer?) => true
}

result_from()

@type result_from() ::
  {:list_of, [{:elements | :flat | :items, non_neg_integer()}, ...]}
  | {:list_of, [{:elements | :flat | :items, non_neg_integer()}, ...],
     [{:elements | :items, non_neg_integer()}]}
  | {:list_of, [{:elements | :flat | :items, non_neg_integer()}, ...],
     [{:elements | :items, non_neg_integer()}],
     :always | [{:elements, non_neg_integer()}]}
  | {:element_of, non_neg_integer()}
  | {:item_of, non_neg_integer()}

How the type of a result is made from the types of the arguments (D-151), by the places of the arguments (from 0; a place at or after a variadic parameter is that argument and all that follow):

  • {:list_of, parts} and {:list_of, parts, non_empty_from}: a list of the elements that the parts give: {:elements, place} the elements of a list (a value that is not a list is the list of it, section 10.3.2.9.4), {:flat, place} the same with the lists inside them taken apart as flatten does, and {:items, place} the arguments themselves, as the items they are.
  • non_empty_from of a :list_of (D-152) lists the places that make the result a list that has an element: {:elements, place} when the argument there has one (a list that has an element, or a value, which is the list of itself), {:items, place} when an item is given there.
  • a fourth element of a :list_of says when the call is null (D-152): :always (a position out of range, a function that is not one, ...) or the places {:elements, place} whose argument being null makes it so, for a function that fails for nothing else (reverse, append, ...); the result is then not null when none of those may be.
  • {:element_of, place}: one of the elements of the list, or null if there is none (max, min); not null when the list has an element and they are of one kind that is ordered.
  • {:item_of, place}: one of the arguments (max(a, b, c)).

same_kind()

@type same_kind() :: [{:point | :item_of | :elements, non_neg_integer()}, ...]

The arguments of a call that have to be of one kind that has an order (D-155), by the places of the arguments (from 0; a place at or after a variadic parameter is that argument and all that follow): a number, a string, a date, a time, a date and time, or a duration of one of the two kinds. The call is out of the domain when the kinds that the arguments may be of have nothing in common, as before("a", 2) is. A part is:

  • {:point, place}: the argument at the place, as a point, which is the value itself or the list of one element that is it (section 10.3.2.9.4): before(point1, point2).
  • {:item_of, place}: the arguments from the place on, as the items of max(c1, ..., cn): each is an item, except for one argument alone, which is the list of items if it is a list (as for result_from).
  • {:elements, place}: the elements of the list at the place, which are the items of max(list).

An argument that is null, of any type, or of the name of a model type, is of no kind in particular and agrees with all.

signature()

@type signature() :: %{
  :parameters => [parameter()],
  :result => [Feel.Type.t(), ...],
  optional(:result_from) => result_from(),
  optional(:same_kind) => same_kind(),
  optional(:extension) => true
}

A signature. One that an extension adds (Feel.Extension, D-81) has extension: true.

t()

@type t() :: %Feel.Builtin.Function{
  arity_index: %{required(non_neg_integer()) => [signature()]} | nil,
  call: (Feel.Runtime.Env.t(), [Feel.Value.t()] -> call_outcome()),
  check_arguments?: boolean(),
  name: Feel.Name.t(),
  never_null?: boolean(),
  pure?: boolean(),
  signatures: [signature(), ...]
}

Functions

accepts?(function, arity)

@spec accepts?(t(), non_neg_integer()) :: boolean()

Returns true when a call with this many arguments is one that some signature accepts.

agree?(slots)

@spec agree?([kind_slot()]) :: boolean()

Whether the arguments, as the kinds they may be of, agree: some kind that has an order is the kind of every one of them that is not :free. A single argument that cannot be of such a kind (it has no order) does not agree with itself, and no argument that is not free is one that agrees.

iex> Feel.Builtin.Function.agree?([{:kinds, [:number, :string]}, {:kinds, [:string]}, :free])
true
iex> Feel.Builtin.Function.agree?([{:kinds, [:number]}, {:kinds, [:string]}])
false
iex> Feel.Builtin.Function.agree?([{:kinds, [:number, :string]}, {:kinds, [:number]}, {:kinds, [:string]}])
false
iex> Feel.Builtin.Function.agree?([{:kinds, []}])
false
iex> Feel.Builtin.Function.agree?([:free, :free])
true

arguments_at(signature, arguments, place)

@spec arguments_at(signature(), [argument], non_neg_integer()) :: [argument]
when argument: var

The arguments (or the values) that are at a place of a signature: the one at the place, and for a variadic parameter all that follow. A parameter that the call leaves out has none.

iex> {:ok, sum} = Feel.Builtin.function("sum")
iex> [list, items] = sum.signatures
iex> Feel.Builtin.Function.arguments_at(list, [1, 2, 3], 0)
[1]
iex> Feel.Builtin.Function.arguments_at(items, [1, 2, 3], 0)
[1, 2, 3]
iex> Feel.Builtin.Function.arguments_at(list, [1], 3)
[]

arities(function)

@spec arities(t()) :: [argument_count()]

The numbers of arguments that a call may have: for each signature, from its required parameters to all of them. A signature with a variadic parameter takes {:at_least, n}, and that covers the numbers from n of the others.

iex> {:ok, time} = Feel.Builtin.function("time")
iex> Feel.Builtin.Function.arities(time)
[1, 3, 4]

element_spec(arg)

@spec element_spec(Feel.Type.t()) :: Feel.Type.t()

The type of the elements of a type of a parameter that is a list.

iex> Feel.Builtin.Function.element_spec({:list, :string})
:string

extensions_for_names(function, names)

@spec extensions_for_names(t(), [String.t()]) :: [signature()]

The signatures of extensions that have a parameter of each of the names (D-81). A call by name is for one signature, the one for_names/2 chooses, but an extension may take the call all the same, with the same names for other kinds of argument; a call that one fits is the extension's to answer.

iex> {:ok, date} = Feel.Builtin.function("date")
iex> Feel.Builtin.Function.extensions_for_names(date, ["year", "month", "day"])
[]

for_names(function, names)

@spec for_names(t(), [String.t()]) :: {:ok, signature()} | :error

The signature that a call with named arguments is for: the first one that has a parameter of each of the names. The call is then put in the order of its parameters, and a parameter that the call leaves out is null (see Feel.Analysis.Calls).

Names have no order in a call, but two signatures can have the same names in different orders, as before(point, range) and before(range, point) do, and what the function says differs. Of the signatures that fit, the first that has the parameters in the order in which the names are written is the one; if there is none, the first that fits.

A signature that has exactly the names of the call goes before one that has more (D-81): the overload date and time(date, timezone) of an extension is the one for date: ..., timezone: ..., and not the signature (date, time, timezone) of DMN 1.6 with the time left out. This is only a choice among the signatures that fit, and it is the same as before where none has exactly the names.

Signatures that differ only in types are one signature here (merge_variants/1): the names do not say of what kind the arguments are.

iex> {:ok, time} = Feel.Builtin.function("time")
iex> {:ok, signature} = Feel.Builtin.Function.for_names(time, ["hour", "minute", "second"])
iex> Enum.map(signature.parameters, & &1.name)
["hour", "minute", "second", "offset"]
iex> Feel.Builtin.Function.for_names(time, ["hour", "hours"])
:error
iex> {:ok, before} = Feel.Builtin.function("before")
iex> {:ok, signature} = Feel.Builtin.Function.for_names(before, ["range", "point"])
iex> Enum.map(signature.parameters, & &1.name)
["range", "point"]

index_arities(function)

@spec index_arities(t()) :: t()

Returns the function with the signatures that take each number of arguments, up to 12, kept in it (D-56): the answer of accepts?/2 and with_arity/2 is then a lookup and not a pass over the signatures, each of which works out its numbers of arguments from its parameters. Feel.Builtin.function/1 does this once for the table it keeps. A function whose signatures are changed has to lose it (with_overloads/2 does), since the index is of the signatures it was made from.

iex> {:ok, abs} = Feel.Builtin.function("abs")
iex> abs.arity_index |> Map.fetch!(1) |> length()
3
iex> abs.arity_index |> Map.fetch!(2)
[]

merge_variants(signatures)

@spec merge_variants([signature()]) :: [signature()]

The signatures with those that differ only in types made one: the same parameters (the names, and whether each is optional, variadic, quiet or an integer) taking the types of all of them, and the same result with the types of all.

A call by name sees a function so. Its names say which parameters the call has and not of what kinds they are, so abs(n: x) is for the three kinds that abs takes, and not for the first signature that has an n. The signatures of an extension are never made one (D-81), and a signature that differs from every other in its parameters is as it was.

iex> {:ok, abs} = Feel.Builtin.function("abs")
iex> [merged] = Feel.Builtin.Function.merge_variants(abs.signatures)
iex> Enum.map(merged.parameters, & &1.types)
[[:number, :days_and_time_duration, :years_and_months_duration]]
iex> merged.result
[:number, :days_and_time_duration, :years_and_months_duration]
iex> {:ok, date} = Feel.Builtin.function("date")
iex> Feel.Builtin.Function.merge_variants(date.signatures) == date.signatures
true

ordered_kinds()

@spec ordered_kinds() :: [atom(), ...]

The kinds of value that have an order (Table 55), which is what a same_kind is about: a number, a string, a date, a time, a date and time, and a duration of either kind. A boolean, a list, a context, a range and a function have none.

iex> :date in Feel.Builtin.Function.ordered_kinds()
true
iex> :boolean in Feel.Builtin.Function.ordered_kinds()
false

parameter(name, types, options \\ [])

@spec parameter(String.t(), [Feel.Type.t(), ...], keyword()) :: parameter()

A parameter: its name, the types it accepts, and optional: true if a call may leave it out.

A parameter that is variadic: true stands for all the arguments from its place on, so it is the last of its signature (append(list, item...), sum(n1...)). It takes one argument at least, and none if it is also optional: true. The map has the key :variadic? only for such a parameter.

A parameter that is quiet: true is read without reporting (D-100): what is wrong in the argument, a name that is not bound, a path through an entry that is not there, is not reported, neither when the expression is prepared nor when it is run, and does not end a mode: :strict run. The argument is null then, as it is without the flag. The map has the key :quiet? only for such a parameter.

A parameter that is integer: true wants an integer: a number with a fractional part is cut to one before the function looks at it ("from decimal to integer", DMN 1.6 section 10.3.2.9.4, Feel.Builtin.Arguments.integer/1), and the run reports that (D-46). The map has the key :integer? only for such a parameter.

iex> Feel.Builtin.Function.parameter("offset", [:days_and_time_duration, :null], optional: true)
%{name: "offset", types: [:days_and_time_duration, :null], optional?: true}
iex> Feel.Builtin.Function.parameter("item", [:any], optional: true, variadic: true)
%{name: "item", types: [:any], optional?: true, variadic?: true}
iex> Feel.Builtin.Function.parameter("value", [:any], quiet: true)
%{name: "value", types: [:any], optional?: false, quiet?: true}
iex> Feel.Builtin.Function.parameter("scale", [:number], integer: true)
%{name: "scale", types: [:number], optional?: false, integer?: true}

parameter_at(map, index)

@spec parameter_at(signature(), non_neg_integer()) :: parameter() | nil

The parameter that the argument at this place (from 0) is for: the one at that place, or for an argument past the last, the variadic parameter that ends the signature; nil if there is none.

iex> {:ok, sum} = Feel.Builtin.function("sum")
iex> [_list, variadic] = sum.signatures
iex> Feel.Builtin.Function.parameter_at(variadic, 5).name
"n1"
iex> {:ok, abs} = Feel.Builtin.function("abs")
iex> Feel.Builtin.Function.parameter_at(hd(abs.signatures), 1)
nil

quiet_at?(signatures, index)

@spec quiet_at?([signature()], non_neg_integer()) :: boolean()

Whether the argument at this place (from 0) is read quietly: every one of the signatures has a quiet parameter there.

iex> {:ok, abs} = Feel.Builtin.function("abs")
iex> Feel.Builtin.Function.quiet_at?(abs.signatures, 0)
false
iex> Feel.Builtin.Function.quiet_at?([], 0)
false

same_kind(signature, parts)

@spec same_kind(signature(), same_kind() | nil) :: signature()

The signature with the arguments that have to be of one kind (same_kind/0, D-155). nil says there are none.

iex> points = [Feel.Builtin.Function.parameter("point1", [:number, :string]), Feel.Builtin.Function.parameter("point2", [:number, :string])]
iex> signature = Feel.Builtin.Function.signature(points, [:boolean])
iex> Feel.Builtin.Function.same_kind(signature, [{:point, 0}, {:point, 1}]).same_kind
[point: 0, point: 1]
iex> Feel.Builtin.Function.same_kind(signature, nil) == signature
true

signature(parameters, result, result_from \\ nil)

@spec signature([parameter()], [Feel.Type.t(), ...], result_from() | nil) ::
  signature()

A signature: its parameters and the types of its result, and how the result follows the types of the arguments if it does (result_from/0).

iex> param = Feel.Builtin.Function.parameter("from", [:string])
iex> Feel.Builtin.Function.signature([param], [:date])
%{parameters: [%{name: "from", types: [:string], optional?: false}], result: [:date]}
iex> list = Feel.Builtin.Function.parameter("list", [{:list, :any}])
iex> Feel.Builtin.Function.signature([list], [{:list, :any}], {:list_of, [{:elements, 0}]}).result_from
{:list_of, [{:elements, 0}]}

spec_kind(kind)

@spec spec_kind(Feel.Type.t()) :: atom()

The kind that a type of a parameter stands for: a primitive type is itself, and a list, a range, a context and a function are :list, :range, :context and :function, whatever they are of. :any takes every kind.

iex> Feel.Builtin.Function.spec_kind({:list, :string})
:list
iex> Feel.Builtin.Function.spec_kind(:number)
:number

variadic?(parameter)

@spec variadic?(parameter()) :: boolean()

Returns true for a variadic parameter.

variants_of(function, signature)

@spec variants_of(t(), signature()) :: [signature()]

The signatures that a signature from for_names/2 stands for: those of the function that differ from it only in types (merge_variants/1), or itself if it is one of an extension. A call by name is typed by each of them.

iex> {:ok, abs} = Feel.Builtin.function("abs")
iex> {:ok, signature} = Feel.Builtin.Function.for_names(abs, ["n"])
iex> length(Feel.Builtin.Function.variants_of(abs, signature))
3

with_arity(function, arity)

@spec with_arity(t(), non_neg_integer()) :: [signature()]

The signatures that accept this many arguments.

iex> {:ok, date} = Feel.Builtin.function("date")
iex> [signature] = Feel.Builtin.Function.with_arity(date, 3)
iex> Enum.map(signature.parameters, & &1.name)
["year", "month", "day"]

with_overloads(function, overloads)

@spec with_overloads(t(), [t()]) :: t()

The function with the overloads of extensions added to it (Feel.Extension, D-81): its signatures are those of the function and then those of the overloads, in the order of the extensions.

The function runs first. If it reports an error, or if none of its signatures takes the number of arguments, the overloads that take it are tried, in order, and the first that does not answer :next is the result. If none does, the result is the error of the function, which is the one it would give without the overloads (Feel.Builtin.arity_mismatch/2 when its signatures did not fit). A null that the function gives because an argument is null (:missing) is a result, and the overloads do not see it. So what was a result before is the result still.

The function is as pure as all of them are, and as free of null as all of them are.

iex> {:ok, number} = Feel.Builtin.function("number")
iex> overload = %Feel.Builtin.Function{
...>   name: "number",
...>   signatures: [
...>     Feel.Builtin.Function.signature(
...>       [Feel.Builtin.Function.parameter("from", [:string]), Feel.Builtin.Function.parameter("group", [:string])],
...>       [:number]
...>     )
...>   ],
...>   call: fn _env, [_from, _group] -> {:ok, Decimal.new(2)} end
...> }
iex> merged = Feel.Builtin.Function.with_overloads(number, [overload])
iex> Feel.Builtin.Function.arities(number)
[1, 3]
iex> Feel.Builtin.Function.arities(merged)
[1, 2, 3]
iex> merged.call.(Feel.Runtime.Env.new(), ["1", ","])
{:ok, Decimal.new(2)}
iex> merged.call.(Feel.Runtime.Env.new(), ["1.5"])
{:ok, Decimal.new("1.5")}