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:
- Core functionality for all typesytem operations.
- 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 typeparams
: 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
Range
s (must be increasing)- atoms
- empty list (
[]
)
The following types have associated structs:
- lists:
Type.List.t/0
- bitstrings/binaries:
Type.Bitstring.t/0
- tuples:
Type.Tuple.t/0
- maps:
Type.Map.t/0
- funs:
Type.Function.t/0
The following "type containers" have associated structs:
- unions:
Type.Union.t/0
- opaque types:
Type.Opaque.t/0
- function vars:
Type.Function.Var.t/0
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.
term/0
integer/0
non_neg_integer/0
arity/0
as_boolean/1
binary/0
bitstring/0
byte/0
char/0
charlist/0
nonempty_charlist/0
fun/0
function/0
identifier/0
iodata/0
keyword/0
keyword/1
list/0
nonempty_list/0
maybe_improper_list/0
nonempty_maybe_improper_list/0
mfa/0
no_return/0
number/0
struct/0
timeout/0
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.
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
inferred() :: Type.Function.t() | Type.Union.t(Type.Function.t())
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
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
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}}}
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
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
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
provides the nonempty_list()
primitive type.
Example:
iex> import Type, only: :macros
iex> nonempty_list()
%Type.List{type: any(), nonempty: true}
Usable in guards
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
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
use this for when you must use a runtime value to obtain a builtin type struct
not usable in guards
Specs
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:
- group 0:
none/0
and remote types - group 1 (integers):
- [negative integer literal]
neg_integer/0
- [nonnegative integer literal]
pos_integer/0
- group 2:
float/0
- group 3 (atoms):
- group 4:
reference/0
- group 5 (
Type.Function.t/0
):params: list
functions (ordered byretval
, thenparams
in dictionary order)params: :any
functions (ordered byretval
, thenparams
in dictionary order)
- group 6:
port/0
- group 7:
pid/0
- group 8 (
Type.Tuple.t/0
):- defined tuples, in ascending order of arity, with cartesian dictionary ordering intrenally within an arity group.
- minimum size tuples, in descending order of size.
- group 9 (
Type.Map.t/0
): maps - group 10 (
Type.List.t/0
):nonempty: true
list- empty list literal
nonempty: false
lists
- group 11 (
Type.Bitstring.t/0
): bitstrings and binaries - group 12:
any/0
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
Specs
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
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()))"
Specs
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
resolves a remote type into its constitutent type. raises if the type is not found.
Specs
see Type.fetch_type/4
, except raises if the type is not found.
Specs
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
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
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
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
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
Specs
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})"
Specs
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
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
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
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
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
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()"
Specs
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:
- "no notification to the user"
- "emit a warning using
IO.warn/2
" - "halt compilation with
CompileError
"
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.
"""]}]}