Feel.Value.Function (feel v0.1.0)

Copy Markdown View Source

The FEEL type function (DMN 1.6, section 10.3.2.8).

Functions are ordinary values: a decision model supplies business knowledge models and decision services as functions in the scope an expression runs in, and a host application can supply Elixir functions the same way. There are three kinds:

  • :feel - a function defined in FEEL with function(...). It closes over the scope where it was defined.
  • :builtin - one of the built-in functions of the specification, which a name stands for where nothing else has it (D-82). It has the signatures of the function, and a call finds the function by its name. One that a declared function type was given in place of a function made of several signatures (narrowings/2, D-197) has one signature, and a call of it takes the arguments as a function of FEEL with those parameters does.
  • :native - an Elixir function that the host application exposes. It is called with its arguments converted at the boundary to the Elixir types named in its signature.

The type of a function is function<T1, ..., Tn> -> R. Parameter names are not part of it. A literal FEEL function's result descriptor comes from kind-only analysis in its written environment and is preserved across analysis tiers, call specialization and optimization (D-219). Its precise body call result is separate analysis information, not runtime metadata. Signature inspection and callback checks use the descriptor; they do not inspect the implementation.

Treat the struct as opaque: build native functions with native/2 and read them through the functions of this module. A built-in function has one type for each number of arguments it takes (types_at/2), since a function type has a fixed number of parameters and date takes one argument or three.

Summary

Types

How a native function's argument or result is converted between FEEL and Elixir.

The Elixir function behind a native function.

A parameter: its name and its FEEL type.

A signature of a built-in function as a value needs it: the parameters (name, type, and whether a call may leave it out), whether the last parameter stands for all the arguments from its place on, and the type of the result. A type that the function takes in more than one kind is :any.

t()

Functions

Returns the number of parameters of a function.

Builds a built-in function as a value, from its name and its signatures (Feel.Builtin.value/2 makes them from the function). The parameters and the result of the first signature are those that params/1 and type/1 give. variants are the signatures that the first ones were made of when they differ only in types (D-171): they are no function type of the value, and say what a call of it does (refinements/1).

Returns the kind of a function.

Returns the name of a function, or nil when it has none.

The built-in function restricted to each signature it made one of (refinements/1) that takes arity arguments, each as a function of its own (D-197): abs for a number, for a days and time duration, and for a years and months duration. One of them is what a declared function type is given when the function itself is not an instance of it (Feel.Type.Conversion).

Builds a native function from an Elixir function.

Returns the parameters of a function with their FEEL types.

The function types that say what a call of a built-in function does for part of what it takes (D-171): those of the signatures that were made one for a call by name (Feel.Builtin.value/2), each as it was. abs is a function<Any> -> Any, and a call of it with a number gives a number, as function<number> -> number says; it says nothing of a call with a string. They are not function types of the value: abs instance of function<number> -> number is false, as the specification has it, and a name declared function<number> -> number is not given abs but the function of the signature for a number (narrowings/2, D-197). None for a function that was not made of several signatures, and for a function of FEEL or of the host.

Returns the FEEL function type of a function.

Returns the descriptor of function<T1, ..., Tn> -> R for the given parameter types and result type.

All the function types of a function: one for a function of FEEL or of the host. For a built-in function, one for each number of arguments that its signatures take, or :open if one of them is variadic and takes any number.

The function types that a function has for a number of arguments: [type(function)] for a function with as many parameters, and none for another. A built-in function has one for each of its signatures that takes this many arguments, the parameters of an optional parameter and of a variadic one being those of the arguments.

Returns true for a t/0.

A function of FEEL whose body reports its diagnostics with origin (D-201).

Types

boundary_type()

@type boundary_type() ::
  :feel | :number | :integer | :float | :string | :char | :boolean

How a native function's argument or result is converted between FEEL and Elixir.

boundary typeFEEL sideElixir side
:feelany valuethe value; a context is a plain map (D-135)
:numbernumberDecimal.t/0
:integerintegral numberinteger
:floatnumberfloat
:stringstringbinary
:charone-character stringcode point
:booleanbooleanboolean

callable()

@type callable() :: function() | {module(), atom()}

The Elixir function behind a native function.

kind()

@type kind() :: :feel | :builtin | :native

param()

@type param() :: {name :: String.t(), type :: Feel.Type.t()}

A parameter: its name and its FEEL type.

signature()

@type signature() :: %{
  parameters: [{String.t(), Feel.Type.t(), boolean()}],
  variadic?: boolean(),
  returns: Feel.Type.t()
}

A signature of a built-in function as a value needs it: the parameters (name, type, and whether a call may leave it out), whether the last parameter stands for all the arguments from its place on, and the type of the result. A type that the function takes in more than one kind is :any.

t()

@type t() :: %Feel.Value.Function{
  impl: term(),
  kind: kind(),
  name: String.t() | nil,
  origin: {:origin, term()} | nil,
  params: [param()],
  returns: Feel.Type.t()
}

Functions

arity(function)

@spec arity(t()) :: non_neg_integer()

Returns the number of parameters of a function.

builtin(name, signatures, variants \\ [])

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

Builds a built-in function as a value, from its name and its signatures (Feel.Builtin.value/2 makes them from the function). The parameters and the result of the first signature are those that params/1 and type/1 give. variants are the signatures that the first ones were made of when they differ only in types (D-171): they are no function type of the value, and say what a call of it does (refinements/1).

iex> signature = %{parameters: [{"n", :number, false}], variadic?: false, returns: :number}
iex> function = Feel.Value.Function.builtin("abs", [signature])
iex> {function.kind, Feel.Value.Function.type(function)}
{:builtin, {:function, [:number], :number}}

kind(function)

@spec kind(t()) :: kind()

Returns the kind of a function.

name(function)

@spec name(t()) :: String.t() | nil

Returns the name of a function, or nil when it has none.

narrowings(function, arity)

@spec narrowings(t(), non_neg_integer()) :: [t()]

The built-in function restricted to each signature it made one of (refinements/1) that takes arity arguments, each as a function of its own (D-197): abs for a number, for a days and time duration, and for a years and months duration. One of them is what a declared function type is given when the function itself is not an instance of it (Feel.Type.Conversion).

A restricted function has the one signature it was made of, with its parameter names and the types it declares for this many arguments, and it takes only those: a call with another number of arguments, or with a value that is not of the type of its parameter, is null with an error, as it is for a function of FEEL (Feel.Runtime.Invoke). It is an instance of the function type of its signature and of no other, and has no refinement. None for a function that is not a built-in, for one that was not made of several signatures, and for a variadic signature (a refinement is none either).

iex> {:ok, abs} = Feel.Builtin.value("abs")
iex> [number | _] = Feel.Value.Function.narrowings(abs, 1)
iex> {number.name, Feel.Value.Function.params(number), Feel.Value.Function.types(number)}
{"abs", [{"n", :number}], [{:function, [:number], :number}]}
iex> Feel.Value.Function.refinements(number)
[]
iex> Feel.Value.Function.narrowings(abs, 2)
[]

native(callable, options \\ [])

@spec native(callable(), keyword()) :: t()

Builds a native function from an Elixir function.

Options

  • :name - the function's name, for diagnostics.
  • :params - a list of {name, boundary_type} pairs, one per parameter. Defaults to no parameters.
  • :returns - the boundary type of the result. Defaults to :feel.

Raises ArgumentError for an unknown option or boundary type, or when callable does not accept as many arguments as there are parameters.

iex> function = Feel.Value.Function.native(&max/2, name: "maximum", params: [{"a", :integer}, {"b", :integer}], returns: :integer)
iex> {Feel.Value.Function.arity(function), Feel.Value.Function.type(function)}
{2, {:function, [:number, :number], :number}}

params(function)

@spec params(t()) :: [param()]

Returns the parameters of a function with their FEEL types.

refinements(function)

@spec refinements(t()) :: [Feel.Type.t()]

The function types that say what a call of a built-in function does for part of what it takes (D-171): those of the signatures that were made one for a call by name (Feel.Builtin.value/2), each as it was. abs is a function<Any> -> Any, and a call of it with a number gives a number, as function<number> -> number says; it says nothing of a call with a string. They are not function types of the value: abs instance of function<number> -> number is false, as the specification has it, and a name declared function<number> -> number is not given abs but the function of the signature for a number (narrowings/2, D-197). None for a function that was not made of several signatures, and for a function of FEEL or of the host.

iex> {:ok, abs} = Feel.Builtin.value("abs")
iex> Feel.Value.Function.refinements(abs)
[
  {:function, [:number], :number},
  {:function, [:days_and_time_duration], :days_and_time_duration},
  {:function, [:years_and_months_duration], :years_and_months_duration}
]
iex> {:ok, before} = Feel.Builtin.value("before")
iex> Feel.Value.Function.refinements(before)
[]

type(function)

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

Returns the FEEL function type of a function.

A parameter without a declared type is :any, and so is a result without one.

type(parameter_types, returns)

@spec type([Feel.Type.t()], Feel.Type.t()) :: Feel.Type.t()

Returns the descriptor of function<T1, ..., Tn> -> R for the given parameter types and result type.

types(function)

@spec types(t()) :: [Feel.Type.t()] | :open

All the function types of a function: one for a function of FEEL or of the host. For a built-in function, one for each number of arguments that its signatures take, or :open if one of them is variadic and takes any number.

iex> {:ok, length} = Feel.Builtin.value("string length")
iex> Feel.Value.Function.types(length)
[{:function, [:string], :number}]
iex> {:ok, sum} = Feel.Builtin.value("sum")
iex> Feel.Value.Function.types(sum)
:open

types_at(function, arity)

@spec types_at(t(), non_neg_integer()) :: [Feel.Type.t()]

The function types that a function has for a number of arguments: [type(function)] for a function with as many parameters, and none for another. A built-in function has one for each of its signatures that takes this many arguments, the parameters of an optional parameter and of a variadic one being those of the arguments.

iex> {:ok, date} = Feel.Builtin.value("date")
iex> Feel.Value.Function.types_at(date, 3)
[{:function, [:number, :number, :number], :date}]
iex> Feel.Value.Function.types_at(date, 2)
[]

value?(arg1)

@spec value?(term()) :: boolean()

Returns true for a t/0.

with_origin(function, origin)

@spec with_origin(t(), term()) :: t()

A function of FEEL whose body reports its diagnostics with origin (D-201).

The span of a diagnostic is a range of the text of the expression that made it. A function that one expression made and another calls, as a host binds the business knowledge model that a decision calls, runs the text of the first inside the second, so the diagnostics of its body have spans in a text that is not the one being run. With an origin they say whose text it is (Feel.Diagnostic, :origin), and the host finds the span in it. The origin is whatever term the host likes.

A function that an expression writes has the origin of the run it is made in (the :origin option of Feel.run/3, nil unless the host gave one, or the origin of the function whose body writes it), so a function passed to another and called there, and one a function makes and gives back, report in their own text. This replaces that origin, for a function that was made by a run that did not know whose text it is. A diagnostic of the call itself (the conversion of an argument, the end of the call) is the caller's. A function that is not of FEEL runs no text, and is not changed.

iex> body = fn env, [_a] ->
...>   {nil, Feel.Runtime.Env.report(env, Feel.Diagnostic.new(:invalid_property, "x", span: {0, 1}))}
...> end
iex> function = Feel.Runtime.Invoke.closure([{"a", :any}], :any, body) |> Feel.Value.Function.with_origin(:member)
iex> {nil, env} = Feel.Runtime.Invoke.call(Feel.Runtime.Env.new(), function, {:positional, [1]}, {0, 4})
iex> Enum.map(Feel.Runtime.Env.diagnostics(env), & &1.origin)
[:member]