Type (mavis v0.0.6) View Source

Type analysis system for Elixir.

Mavis implements the Type module, which contains a type analysis system specifically tailored for the BEAM VM. The following considerations went into its design.

  • Must be compatible with the existing dialyzer/typespec system
  • May extend the typespec system if it's unobtrusive and can be, at a minimum, 'opt-out'.
  • Does not have to conform to existing theoretical typesystems (e.g. H-M)
  • Take maximum advantage of Elixir programming features to achieve readability and extensibility.
  • Does not have to be easily usable from erlang, but must be able to handle modules produced in erlang.

You can read more about the Mavis typesystem here, and deviations from dialyzer types in the following documents:

Compile-Time Usage

The type analysis system is designed to be a backend for a compile-time typechecker (see Selectrix). This system will infer the types of functions in modules and emit errors and warnings if there appear to be conflicts.

One key function enabling this is fetch_type!/3; you can use this function to retrieve typing information on typing information stored in modules that have already been compiled.

iex> inspect Type.fetch_type!(String, :t, [])
"binary()"

Runtime Usage

There's no reason why you have to use the typing library exclusively at compile-time. Here is an example of using it at runtime:

defmodule TestJson do
  @type json :: String.t | number | boolean | nil | [json] | %{optional(String.t) => json}

  def validate_json(data) do
    json_type = Type.fetch_type!(__MODULE__, :json, [])

    if Type.type_match?(json_type, data), do: :ok, else: raise "not json"
  end
end

Note that the above example is not particularly performant.

Examples:

iex> import Type, only: :macros
iex> inspect Type.union(non_neg_integer(), :infinity)
"timeout()"
iex> Type.intersection(pos_integer(), -10..10)
1..10

The Type module implements two things:

  1. Core functionality for all typesytem operations.
  2. Support data structure for generic builtin and remote types.

Type representation in Mavis

The Type data structure is a struct with three parameters:

  • module: The module in which the type is defined; nil for builtins.
  • name: (atom) of the type
  • params: a list of type arguments to the type data structure. These must be types themselves "as applied" to the type definition if we consider it to be a function.

Representing other types.

The following literals are represented directly:

  • integers
  • Ranges (must be increasing)
  • atoms
  • empty list ([])

The following types have associated structs:

The following "type containers" have associated structs:

Supported Type operations

The Mavis typesystem provides five primary operations, which might not necessarily be the set of operations that one expects from a typesystem. These operations are chosen specifically reflect the needs of Erlang and Elixir's dynamic types and the type specification system provided by dialyzer.

The operation Type.type_match?/2 is also provided, which is a combination of Type.of/1 and Type.subtype?/2.

Deviations from standard Elixir and Erlang

The Curious case of String.t

String.t/0 has a special meaning in Elixir, it is a UTF-8 encoded binary/0. As such, it is special-cased to have some properties that other remote types don't have out of the box. This sort of behaviour may be changed to be extensible to custom types in a future release.

The nonexistent type String.t/1 is also implemented, with the type parameter indicating byte-lengths for compile-time constant strings. This is done entirely under the hood and should not otherwise affect operations. If you encounter strange results, report them to the issue tracker.

In the meantime, you may disable this feature by setting the following:

config :mavis, :use_smart_strings, false

"Aliased builtins"

Some builtins were not directly introduced into the typesystem logic, since they are easily represented as aliases or composite types. The following aliased builtins are usable with the builtin/1 macro, but will return false with is_primitive/1

NB in the future the name of is_primitive may change to prevent confusion.

iex> import Type, only: :macros
iex> timeout()
%Type.Union{of: [:infinity, %Type{name: :pos_integer}, 0]}
iex> Type.is_primitive(timeout())
false

Module and Node detail

The module/0 and node/0 types are given extra protection.

An atom will not be considered a module unless it is detected to exist in the VM; although for usable_as/3 it will return the :maybe result if unconfirmed.

iex> import Type, only: :macros
iex> Type.type_match?(module(), :foo)
false
iex> Type.type_match?(module(), Kernel)
true
iex> Type.type_match?(module(), :gen_server)
true
iex> Type.usable_as(Enum, module())
:ok
iex> Type.usable_as(:not_a_module, module())
{:maybe, [%Type.Message{target: %Type{name: :module}, type: :not_a_module}]}

A node will not be considered a node unless it has the proper form for a node. usable_as/3 does not check active node lists, however.

iex> import Type, only: :macros
iex> Type.type_match?(node_type(), :foo)
false
iex> Type.type_match?(node_type(), :nonode@nohost)
true

Link to this section Summary

Types

No members of this type will succeed in the operation.

type of group assignments

output type for c:Type.Inference.Api.infer/1 and Type.Inference.Api.infer/3

Represents that some but not all members of the type will succeed in the operation.

t()

output type for Type.usable_as/3

Type Macros

provides the any() primitive type.

provides the arity() primitive type.

provides the atom() primitive type.

provides the binary() primitive type.

provides the bitstring() primitive type.

provides the boolean() primitive type.

provides the byte() primitive type.

provides the char() primitive type.

provides the charlist() primitive type.

provides the float() primitive type.

provides the fun() primitive type.

provides the function() primitive type.

generates the function type from a type-like ast. Note that the AST must be in a parentheses set. If the parameters are ..., it will generate the any function; for the generic n-arity function use _ for each of the parameters.

provides the identifier() primitive type.

provides the integer() primitive type.

provides the iodata() primitive type.

provides the iolist() primitive type.

provides the keyword() primitive type.

provides the list() primitive type.

generates a list of a particular type. A last parameter of ... indicates that the list should be nonempty

provides the map() primitive type.

generates the map type from a map ast. Unspecified keys default to required if singletons, and optional if non-singletons.

provides the maybe_improper_list() primitive type.

provides the mfa() primitive type.

provides the module() primitive type.

provides the neg_integer() primitive type.

provides the no_return() primitive type.

provides the node_type() primitive type.

provides the non_neg_integer() primitive type.

provides the none() primitive type.

provides the nonempty_charlist() primitive type.

provides the nonempty_list() primitive type.

provides the nonempty_maybe_improper_list() primitive type.

provides the number() primitive type.

provides the pid() primitive type.

provides the port() primitive type.

provides the pos_integer() primitive type.

provides the reference() primitive type.

helper macro to match on remote types

provides the struct() primitive type.

provides the term() primitive type.

provides the timeout() primitive type.

provides the tuple() primitive type.

generates the tuple type from a tuple ast. If the tuple contains ... it will generate the generic any tuple.

Functions

use this for when you must use a runtime value to obtain a builtin type struct

Types have an order that facilitates calculation of collapsing values into unions.

true if the list of types constitutes a complete cover of the provided type.

retrieves a typespec for a function, and converts it to a Type.t/0 value.

retrieves a stored type from a module, and converts it to a Type.t/0 value.

resolves a remote type into its constitutent type. raises if the type is not found.

see Type.fetch_type/4, except raises if the type is not found.

outputs the type which is guaranteed to satisfy the following conditions

outputs the type which is guaranteed to satisfy the following conditions

guard that tests if the selected type is builtin

guard that tests if the selected type is remote

guard that tests if the selected type is a singleton type. This is a type that has only one value associated with it.

returns the type of the term.

partitions a type across a list of types

outputs whether one type is a subtype of itself. To be true, the following condition must be satisfied

true if the passed term is an element of the type.

The typegroup of the type.

outputs the type which is guaranteed to satisfy the following conditions

outputs the type which is guaranteed to satisfy the following conditions

Main utility function for determining type correctness.

Link to this section Types

Specs

error() :: {:error, Type.Message.t()}

No members of this type will succeed in the operation.

should typically result in a compile-time error.

output by Type.usable_as/3

Specs

group() :: 0..12

type of group assignments

Specs

output type for c:Type.Inference.Api.infer/1 and Type.Inference.Api.infer/3

Specs

maybe() :: {:maybe, [Type.Message.t()]}

Represents that some but not all members of the type will succeed in the operation.

should typically result in a compile-time warning.

output by Type.usable_as/3

Specs

t() ::
  %Type{module: nil | module(), name: atom(), params: [t()]}
  | integer()
  | Range.t()
  | atom()
  | Type.List.t()
  | []
  | Type.Bitstring.t()
  | Type.Tuple.t()
  | Type.Map.t()
  | Type.Function.t()
  | Type.Union.t()
  | Type.Opaque.t()
  | Type.Function.Var.t()

Specs

ternary() :: :ok | maybe() | error()

output type for Type.usable_as/3

Link to this section Type Macros

provides the any() primitive type.

Example:

iex> import Type, only: :macros
iex> any()
%Type{name: :any}

Usable in guards

provides the arity() primitive type.

Example:

iex> import Type, only: :macros
iex> arity()
0..255

Usable in guards

provides the atom() primitive type.

Example:

iex> import Type, only: :macros
iex> atom()
%Type{name: :atom}

Usable in guards

provides the binary() primitive type.

Example:

iex> import Type, only: :macros
iex> binary()
%Type.Bitstring{unit: 8}

Usable in guards

provides the bitstring() primitive type.

Example:

iex> import Type, only: :macros
iex> bitstring()
%Type.Bitstring{unit: 1}

Usable in guards

provides the boolean() primitive type.

Example:

iex> import Type, only: :macros
iex> boolean()
%Type.Union{of: [true, false]}

Usable in guards

provides the byte() primitive type.

Example:

iex> import Type, only: :macros
iex> byte()
0..255

Usable in guards

provides the char() primitive type.

Example:

iex> import Type, only: :macros
iex> char()
0..0x10_FFFF

Usable in guards

provides the charlist() primitive type.

Example:

iex> import Type, only: :macros
iex> charlist()
%Type.List{type: 0..0x10_FFFF}

Usable in guards

provides the float() primitive type.

Example:

iex> import Type, only: :macros
iex> float()
%Type{name: :float}

Usable in guards

provides the fun() primitive type.

Example:

iex> import Type, only: :macros
iex> fun()
%Type.Function{params: :any, return: any()}

Usable in guards

provides the function() primitive type.

Example:

iex> import Type, only: :macros
iex> function()
%Type.Function{params: :any, return: any()}

Usable in guards

Link to this macro

function(list)

View Source (macro)

generates the function type from a type-like ast. Note that the AST must be in a parentheses set. If the parameters are ..., it will generate the any function; for the generic n-arity function use _ for each of the parameters.

Examples:

iex> import Type, only: :macros
iex> function (atom() -> pos_integer())
%Type.Function{params: [%Type{name: :atom}], return: %Type{name: :pos_integer}}
iex> function (... -> pos_integer())
%Type.Function{params: :any, return: %Type{name: :pos_integer}}
iex> function (_, _ -> pos_integer())
%Type.Function{params: 2, return: %Type{name: :pos_integer}}

If you want to tag function variables or constrain them, you can pass a keyword or atom list to the second parameter. These variables must appear in the return.

iex> import Type, only: :macros
iex> function (i -> i when i: var)
%Type.Function{params: [%Type.Function.Var{name: :i}],
              return: %Type.Function.Var{name: :i}}
iex> function (i -> i when i: pos_integer())
%Type.Function{params: [%Type.Function.Var{name: :i, constraint: %Type{name: :pos_integer}}],
               return: %Type.Function.Var{name: :i, constraint: %Type{name: :pos_integer}}}

usable in guards

provides the identifier() primitive type.

Example:

iex> import Type, only: :macros
iex> identifier()
%Type.Union{of: [pid(), port(), reference()]}

Usable in guards

provides the integer() primitive type.

Example:

iex> import Type, only: :macros
iex> integer()
%Type.Union{of: [pos_integer(), 0, neg_integer()]}

Usable in guards

provides the iodata() primitive type.

Example:

iex> import Type, only: :macros
iex> iodata()
%Type.Union{of: [binary(), iolist()]}

Usable in guards

provides the iolist() primitive type.

Example:

iex> import Type, only: :macros
iex> iolist()
%Type{name: :iolist}

Usable in guards

provides the keyword() primitive type.

Example:

iex> import Type, only: :macros
iex> keyword()
%Type.List{type: tuple({atom(), any()})}

Usable in guards

provides the list() primitive type.

Example:

iex> import Type, only: :macros
iex> list()
%Type.List{type: any()}

Usable in guards

generates a list of a particular type. A last parameter of ... indicates that the list should be nonempty

Examples:

iex> import Type, only: :macros
iex> list(...)
%Type.List{type: %Type{name: :any}, nonempty: true}
iex> list(1..10)
%Type.List{type: 1..10}
iex> list(1..10, ...)
%Type.List{type: 1..10, nonempty: true}

if it's passed a keyword list, it is interpreted as a keyword list.

iex> import Type, only: :macros
iex> list(foo: pos_integer())
%Type.List{type: %Type.Tuple{elements: [:foo, %Type{name: :pos_integer}]}}
  • usable in guards *

provides the map() primitive type.

Example:

iex> import Type, only: :macros
iex> map()
%Type.Map{optional: %{any() => any()}}

Usable in guards

generates the map type from a map ast. Unspecified keys default to required if singletons, and optional if non-singletons.

iex> import Type, only: :macros
iex> map %{foo: pos_integer()}
%Type.Map{required: %{foo: %Type{name: :pos_integer}}}
iex> map %{required(1) => atom()}
%Type.Map{required: %{1 => %Type{name: :atom}}}
iex> map %{optional(:bar) => atom()}
%Type.Map{optional: %{bar: %Type{name: :atom}}}
Link to this macro

maybe_improper_list()

View Source (macro)

provides the maybe_improper_list() primitive type.

Example:

iex> import Type, only: :macros
iex> maybe_improper_list()
%Type.List{type: any(), final: any()}

Usable in guards

provides the mfa() primitive type.

Example:

iex> import Type, only: :macros
iex> mfa()
%Type.Tuple{elements: [module(), atom(), arity()]}

Usable in guards

provides the module() primitive type.

Example:

iex> import Type, only: :macros
iex> module()
%Type{name: :module}

Usable in guards

provides the neg_integer() primitive type.

Example:

iex> import Type, only: :macros
iex> neg_integer()
%Type{name: :neg_integer}

Usable in guards

provides the no_return() primitive type.

Example:

iex> import Type, only: :macros
iex> no_return()
%Type{name: :none}

Usable in guards

provides the node_type() primitive type.

Example:

iex> import Type, only: :macros
iex> node_type()
%Type{name: :node}

Usable in guards

Link to this macro

non_neg_integer()

View Source (macro)

provides the non_neg_integer() primitive type.

Example:

iex> import Type, only: :macros
iex> non_neg_integer()
%Type.Union{of: [pos_integer(), 0]}

Usable in guards

provides the none() primitive type.

Example:

iex> import Type, only: :macros
iex> none()
%Type{name: :none}

Usable in guards

Link to this macro

nonempty_charlist()

View Source (macro)

provides the nonempty_charlist() primitive type.

Example:

iex> import Type, only: :macros
iex> nonempty_charlist()
%Type.List{type: 0..0x10_FFFF, nonempty: true}

Usable in guards

Link to this macro

nonempty_list()

View Source (macro)

provides the nonempty_list() primitive type.

Example:

iex> import Type, only: :macros
iex> nonempty_list()
%Type.List{type: any(), nonempty: true}

Usable in guards

Link to this macro

nonempty_maybe_improper_list()

View Source (macro)

provides the nonempty_maybe_improper_list() primitive type.

Example:

iex> import Type, only: :macros
iex> nonempty_maybe_improper_list()
%Type.List{type: any(), final: any(), nonempty: true}

Usable in guards

provides the number() primitive type.

Example:

iex> import Type, only: :macros
iex> number()
%Type.Union{of: [float(), pos_integer(), 0, neg_integer()]}

Usable in guards

provides the pid() primitive type.

Example:

iex> import Type, only: :macros
iex> pid()
%Type{name: :pid}

Usable in guards

provides the port() primitive type.

Example:

iex> import Type, only: :macros
iex> port()
%Type{name: :port}

Usable in guards

provides the pos_integer() primitive type.

Example:

iex> import Type, only: :macros
iex> pos_integer()
%Type{name: :pos_integer}

Usable in guards

provides the reference() primitive type.

Example:

iex> import Type, only: :macros
iex> reference()
%Type{name: :reference}

Usable in guards

Specs

remote(Macro.t()) :: Macro.t()

helper macro to match on remote types

Example:

iex> Type.remote(String.t())
%Type{module: String, name: :t}

provides the struct() primitive type.

Example:

iex> import Type, only: :macros
iex> struct()
%Type.Map{required: %{__struct__: atom()}, optional: %{atom() => any()}}

Usable in guards

provides the term() primitive type.

Example:

iex> import Type, only: :macros
iex> term()
%Type{name: :any}

Usable in guards

provides the timeout() primitive type.

Example:

iex> import Type, only: :macros
iex> timeout()
%Type.Union{of: [:infinity, pos_integer(), 0]}

Usable in guards

provides the tuple() primitive type.

Example:

iex> import Type, only: :macros
iex> tuple()
%Type.Tuple{elements: [], fixed: false}

Usable in guards

generates the tuple type from a tuple ast. If the tuple contains ... it will generate the generic any tuple.

See Type.Tuple for an explanation of deviations from dialyzer in the implementation of this type.

iex> import Type, only: :macros
iex> tuple {...}
%Type.Tuple{elements: [], fixed: false}
iex> tuple {:ok, pos_integer()}
%Type.Tuple{elements: [:ok, %Type{name: :pos_integer}]}
iex> tuple {:error, atom(), pos_integer()}
%Type.Tuple{elements: [:error, %Type{name: :atom}, %Type{name: :pos_integer}]}
  • usable in guards *

Link to this section Functions

Link to this macro

builtin(type_ast)

View Source (macro)

use this for when you must use a runtime value to obtain a builtin type struct

not usable in guards

Specs

compare(t(), t()) :: :lt | :gt | :eq

Types have an order that facilitates calculation of collapsing values into unions.

Conforms to Elixir's compare api, so you can use this in Enum.sort/2

For literals this follows the order in the erlang type system. Where one type is a strict subtype of another, it should wind up less than its supertype

Types are organized into groups, which exist as a fastlane for comparing order between two different types (see typegroup/1).

The order is as follows:

Range.t/0 (group 1) comes after the highest integer in the range, with wider ranges coming after narrower ranges.

iolist/0 (group 10) comes in the appropriate place in the list group.

a member of Type.Union.t/0 comes after the highest represented item in its union.

Examples

iex> import Type, only: :macros
iex> Type.compare(integer(), pid())
:lt
iex> Type.compare(-5..5, 1..5)
:gt
Link to this function

covered?(type, type_list)

View Source

Specs

covered?(t(), [t()]) :: boolean()

true if the list of types constitutes a complete cover of the provided type.

see https://en.wikipedia.org/wiki/Cover_(topology)

iex> Type.covered?(-5..5, [1..5, 0, -5..-1])
true
iex> Type.covered?(-5..5, [1..5, 0, -5..-2])
false
Link to this function

fetch_spec(module, fun, arity)

View Source

retrieves a typespec for a function, and converts it to a Type.t/0 value.

Example:

iex> {:ok, spec} = Type.fetch_spec(String, :split, 1)
iex> inspect spec
"(String.t() -> list(String.t()))"
Link to this function

fetch_type(module, name, params \\ [], meta \\ [])

View Source

Specs

fetch_type(module(), atom(), [t()], keyword()) ::
  {:ok, t()} | {:error, Type.Message.t()}

retrieves a stored type from a module, and converts it to a Type.t/0 value.

Example:

iex> {:ok, type} = Type.fetch_type(String, :t)
iex> inspect type
"binary()"

If the type has non-zero arity, you can specify its passed parameters as the third argument.

Specs

fetch_type!(t()) :: t() | no_return()

resolves a remote type into its constitutent type. raises if the type is not found.

Link to this function

fetch_type!(module, name, params \\ [], meta \\ [])

View Source

Specs

fetch_type!(module(), atom(), [t()], keyword()) :: t() | no_return()

see Type.fetch_type/4, except raises if the type is not found.

Link to this function

find_elements(elements, so_far \\ [])

View Source

Specs

intersection([t()]) :: t()

outputs the type which is guaranteed to satisfy the following conditions:

  • if a term is in all of the types in the list, it is in the result type.
  • if a term is not in any of the types in the list, it is not in the result type.

Example:

iex> import Type, only: :macros
iex> Type.intersection([pos_integer(), -1..10, -6..6])
1..6

Specs

intersection(t(), t()) :: t()

outputs the type which is guaranteed to satisfy the following conditions:

  • if a term is in both types, it is in the result type.
  • if a term is not in either type, it is not in the result type.

Example:

iex> import Type, only: :macros
iex> Type.intersection(non_neg_integer(), -10..10)
0..10
Link to this macro

is_primitive(type)

View Source (macro)

guard that tests if the selected type is builtin

Example:

iex> Type.is_primitive(:foo)
false
iex> Type.is_primitive(%Type{name: :integer})
true
iex> Type.is_primitive(%Type{module: String, name: :t})
false

Note that composite builtin types may not match with this function:

iex> import Type, only: :macros
iex> Type.is_primitive(mfa())
false
Link to this macro

is_remote(type)

View Source (macro)

guard that tests if the selected type is remote

Example:

iex> Type.is_remote(:foo)
false
iex> Type.is_remote(%Type{name: :integer})
false
iex> Type.is_remote(%Type{module: String, name: :t})
true
Link to this macro

is_singleton(type)

View Source (macro)

guard that tests if the selected type is a singleton type. This is a type that has only one value associated with it.

Example:

iex> Type.is_singleton(:foo)
true
iex> Type.is_singleton(%Type{name: :any})
false
Link to this macro

list(ast, arg)

View Source (macro)

See Type.Properties.normalize/1.

Specs

of(term()) :: t()

returns the type of the term.

Examples:

iex> Type.of(47)
47
iex> inspect Type.of(47.0)
"float()"
iex> inspect Type.of([:foo, :bar])
"list(:bar | :foo, ...)"
iex> inspect Type.of([:foo | :bar])
"nonempty_improper_list(:foo, :bar)"

Note that for functions, this may not be correct unless you supply an inference engine (see Type.Function):

iex> inspect Type.of(&(&1 + 1))
"(any() -> any())"

For maps, atom and number literals are marshalled into required terms; other literals, like strings, are marshalled into optional terms.

iex> inspect Type.of(%{foo: :bar})
"map(%{foo: :bar})"
iex> inspect Type.of(%{1 => :one})
"map(%{1 => :one})"
iex> inspect Type.of(%{"foo" => :bar, "baz" => "quux"})
"map(%{optional(String.t()) => :bar | String.t()})"
iex> inspect Type.of(1..10)
"map(%Range{first: 1, last: 10})"
Link to this function

partition(type, type_list)

View Source

Specs

partition(t(), [t()]) :: [t()]

partitions a type across a list of types

see https://en.wikipedia.org/wiki/Partition_of_a_set

note however, that if some part of your type is not represented in the type list that is provided, those members will be discarded.

iex> import Type, only: :macros
iex> Type.partition(-5..5, integer().of)
[1..5, 0, -5..-1]

Specs

subtype?(t(), t()) :: boolean()

outputs whether one type is a subtype of itself. To be true, the following condition must be satisfied:

  • if a term is in the first type, then it is also in the second type.

Note that any type is automatically a subtype of itself.

Examples:

iex> import Type, only: :macros
iex> Type.subtype?(10, 1..47)
true
iex> Type.subtype?(10, integer())
true
iex> Type.subtype?(1..47, integer())
true
iex> Type.subtype?(integer(), 1..47)
false
iex> Type.subtype?(1..47, 1..47)
true

Remote Types

Remote types are considered to be a signal that terms in the remote type satisfy "special properties". For example, String.t/0 terms are not only binaries, but are UTF-8 encoded binaries. Thus a remote type is considered to be the subtype of its specification, but not vice versa:

iex> import Type, only: :macros
iex> binary = %Type.Bitstring{size: 0, unit: 8}
iex> Type.subtype?(remote(String.t()), binary)
true
iex> Type.subtype?(binary, remote(String.t()))
false

Specs

type_match?(t(), term()) :: boolean()

true if the passed term is an element of the type.

Important:

Note the argument order for this function, it does not have the same call order, as say, JavaScript's instanceof, or ruby's .is_a?

Example:

iex> import Type, only: :macros
iex> Type.type_match?(integer(), 10)
true
iex> Type.type_match?(neg_integer(), 10)
false
iex> Type.type_match?(pos_integer(), 10)
true
iex> Type.type_match?(1..9, 10)
false
iex> Type.type_match?(-47..47, 10)
true
iex> Type.type_match?(42, 10)
false
iex> Type.type_match?(10, 10)
true

Specs

typegroup(t()) :: group()

The typegroup of the type.

This is a 'fastlane' value which simplifies generating type ordering code. See Type.compare/2 for a list of which groups the types belong to.

NB: group assignments may change.

Specs

union([t()]) :: t()

outputs the type which is guaranteed to satisfy the following conditions:

  • if a term is in any of the types, it is in the result type.
  • if a term is not in any type, it is not in the result type.

union/1 will try to collapse types into the simplest representation, but the success of this operation is not guaranteed.

If you are in a situation where you would like to explicitly preserve the existence of a none() type, you can pass preserve_nones: true to the options list.

Example:

iex> import Type, only: :macros
iex> inspect Type.union([pos_integer(), -10..10, 32, neg_integer()])
"integer()"
iex> inspect Type.union([pos_integer(), none()], preserve_nones: true)
"none() | pos_integer()"

Specs

union(t(), t()) :: t()
union([t()], [{:preserve_nones, true}]) :: t()

outputs the type which is guaranteed to satisfy the following conditions:

  • if a term is in either type, it is in the result type.
  • if a term is not in either type, it is not in the result type.

union/2 will try to collapse types into the simplest representation, but the success of this operation is not guaranteed.

Example:

iex> import Type, only: :macros
iex> inspect Type.union(pos_integer(), -10..10)
"-10..-1 | non_neg_integer()"
Link to this function

usable_as(challenge, target, meta \\ [])

View Source

Specs

usable_as(t(), t(), keyword()) :: ternary()

Main utility function for determining type correctness.

Answers the question: If a system claims to require a certain "target type" to execute without crashing, what will happen if send a term that satisfies a "challenge type"?

The result may be one of:

  • :ok, which signals that no crash will occur
  • {:maybe, [messages]}, which signals that a crash may occur due to one of the listed potential problems, but there are terms which will not trigger a crash.
  • {:error, message} which signals that all terms in the challenge type will trigger a crash.

These three levels are intended to roughly correspond to:

for a running compile-time analysis.

usable_as/3 also may be passed metadata which can be used to correctly craft warning and error messages; as well as being filters for user-defined exceptions to warning or error rules.

Relationship to subtype/2

at first glance, it would seem that the subtype?/2 function is equivalent to usable_as/3, but there are cases where the relationship is not as clear. For example, if a function has the signature:

(integer() -> integer()), that is not necessarily usable as a function that is (any() -> integer()), since it may be sent a value outside of the integers. Conversely an (any() -> integer()) function IS usable as an (integer() -> integer()) function. The subtyping relationship between these function types is unclear; in the Mavis system they are considered to be independent functions that are not subtypes of each other.

Examples:

iex> import Type, only: :macros
iex> Type.usable_as(1, integer())
:ok
iex> Type.usable_as(1, neg_integer())
{:error, %Type.Message{type: 1, target: neg_integer()}}
iex> Type.usable_as(-10..10, neg_integer())
{:maybe, [%Type.Message{type: -10..10, target: neg_integer()}]}

Remote types:

A remote type is intended to indicate that there is a quality outside of the type system which specifies the type. Thus, a remote type should be usable as the type it encapsulates, but it should emit a maybe when going the other direction:

iex> import Type, only: :macros
iex> binary = %Type.Bitstring{size: 0, unit: 8}
iex> Type.usable_as(remote(String.t()), binary)
:ok
iex> Type.usable_as(binary, remote(String.t))
{:maybe, [%Type.Message{
            type: binary,
            target: remote(String.t()),
            meta: [message: """
  binary() is an equivalent type to String.t() but it may fail because it is
  a remote encapsulation which may require qualifications outside the type system.
  """]}]}