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

Link to this function

apply_types(fun, vars, meta \\ [])

View Source

Specs

apply_types(t() | Type.Union.t(t()), [Type.t()], keyword()) :: return()

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}