Type.Function (mavis v0.0.6) View Source
Represents a function type.
There are two fields for the struct defined by this module.
params
a list of types for the function arguments. Note that the arity of the function is the length of this list. May also be the atom:any
which corresponds to "a function of any arity".return
the type of the returned value.
Examples:
(... -> integer())
would be represented as%Type.Function{params: :any, return: %Type{name: :integer}}
(integer() -> integer())
would be represented as%Type.Function{params: [%Type{name: :integer}], return: %Type{name: :integer}}
Shortcut Form
The Type
module lets you specify a function using "shortcut form" via the Type.function/1
macro:
iex> import Type, only: :macros
iex> function((atom() -> pos_integer()))
%Type.Function{params: [%Type{name: :atom}], return: %Type{name: :pos_integer}}
Inference
By default, Mavis will not attempt to perform inference on function types.
iex> inspect Type.of(&(&1 + 1))
"(any() -> any())"
If you would like to perform inference on the function to obtain
more details on the acceptable function types, set the inference
environment variable. For example, if you're using the :mavis_inference
hex package, do:
Application.put_env(:mavis, :inference, Type.Inference)
The default module for this is Type.NoInference
Key functions:
comparison
Functions are ordered first by the type order on their return type, followed by type order on their parameters.
iex> import Type, only: :macros
iex> Type.compare(function(( -> atom())), function(( -> integer())))
:gt
iex> Type.compare(function((integer() -> integer())),
...> function((atom() -> integer())))
:lt
intersection
Functions with distinct parameter types are nonoverlapping, even if their parameter types overlap. If they have the same parameters, then their return values are intersected.
iex> import Type, only: :macros
iex> Type.intersection(function(( -> 1..10)), function(( -> integer())))
%Type.Function{params: [], return: 1..10}
iex> Type.intersection(function((integer() -> integer())),
...> function((1..10 -> integer())))
%Type{name: :none}
functions with :any
parameters intersected with a function with specified parameters
will adopt the parameters of the intersected function.
iex> import Type, only: :macros
iex> Type.intersection(function((... -> pos_integer())),
...> function((1..10 -> pos_integer())))
%Type.Function{params: [1..10], return: %Type{name: :pos_integer}}
union
Functions are generally not merged in union operations, but if their parameters are identical then their return types will be merged.
iex> import Type, only: :macros
iex> Type.union(function(( -> 1..10)), function(( -> 11..20)))
%Type.Function{params: [], return: 1..20}
subtype?
A function type is the subtype of another if it has the same parameters and its return value type is the subtype of the other's
iex> import Type, only: :macros
iex> Type.subtype?(function((integer() -> 1..10)),
...> function((integer() -> integer())))
true
usable_as
The usable_as
relationship for functions may not necessarily be obvious. An
easy way to think about it, is: if I passed a function with this type to a
function that demanded the other type how confident would I be that it would
not crash.
A function is usable_as
another function if all of its parameters are
supertypes of the targeted function; and if its return type is subtypes of the
return type of the targeted function.
iex> import Type, only: :macros
iex> Type.usable_as(function((pos_integer() -> 1..10)), function((1..10 -> pos_integer())))
:ok
iex> Type.usable_as(function((1..10 -> 1..10)), function((pos_integer() -> pos_integer())))
{:maybe, [%Type.Message{type: %Type.Function{params: [1..10], return: 1..10},
target: %Type.Function{params: [%Type{name: :pos_integer}], return: %Type{name: :pos_integer}}}]}
iex> Type.usable_as(function(( -> atom())), function(( -> pos_integer())))
{:error, %Type.Message{type: %Type.Function{params: [], return: %Type{name: :atom}},
target: %Type.Function{params: [], return: %Type{name: :pos_integer}}}}
Link to this section Summary
Functions
applies types to a function definition.
Link to this section Types
Specs
return() :: {:ok, Type.t()} | {:maybe, Type.t(), [Type.Message.t()]} | {:error, Type.Message.t()}
Specs
t() :: %Type.Function{ params: [Type.t()] | :any | pos_integer(), return: Type.t() }
Link to this section Functions
Specs
applies types to a function definition.
Raises with Type.FunctionError
if one of the following is true:
- an :any function is attempted to be applied
- a
top-arity
function is attempted to be applied - a non-function (or union of functions) is attempted to be applied
Returns
{:ok, return_type}
when the function call is successful.{:maybe, return_type, [messages]}
when one or more of the parameters is overspecified.{:error, message}
when any of the parameters is disjoint
Examples:
iex> import Type, only: :macros
iex> func = function ((pos_integer() -> float()))
iex> Type.Function.apply_types(func, [pos_integer()])
{:ok, %Type{name: :float}}
iex> Type.Function.apply_types(func, [non_neg_integer()])
{:maybe, %Type{name: :float}, [
%Type.Message{
type: %Type.Union{of: [%Type{name: :pos_integer}, 0]},
target: %Type{name: :pos_integer},
meta: [message: "non_neg_integer() is overbroad for argument 1 (pos_integer()) of function (pos_integer() -> float())"]
}]}
iex> Type.Function.apply_types(func, [float()])
{:error,
%Type.Message{
type: %Type{name: :float},
target: %Type{name: :pos_integer},
meta: [message: "float() is disjoint to argument 1 (pos_integer()) of function (pos_integer() -> float())"]
}}
iex> var_func = function((i -> i when i: integer()))
iex> Type.Function.apply_types(var_func, [1..10])
{:ok, 1..10}