Type.Map (mavis v0.0.6) View Source
Represents map terms. Note that some of the choices around how to handle maps may deviate from the expectations in the typesystem.
The associated struct has two parameters:
:required
a map of required key types and their associated value types.:optional
a map of optional key types and their associated value types.
Shortcut Form
The Type
module lets you specify a map using "shortcut form" via the
Type.map/1
macro:
Note that the empty map is not the same as map/0
iex> import Type, only: :macros
iex> map(%{optional(atom()) => pos_integer()})
%Type.Map{optional: %{%Type{name: :atom} => %Type{name: :pos_integer}}}
iex> map(%{}) # empty map
%Type.Map{optional: %{}, required: %{}}
iex> map() # t:map/0
%Type.Map{optional: %{%Type{name: :any} => %Type{name: :any}}}
Deviations from standard Erlang/Elixir:
Type.Map
only allows literal integers and literal atoms asrequired
key types. Typespecs that put other types in as required will downgrade those types to beoptional
- In the future, this may be extended to ranges.
- map types will not by default assume
optional(any()) => any()
- If multiple specifications exist for the same particular value (for example,
overlapping ranges in optional keys, or a overlapping types in required vs
optional), all specifications are applied to that value group. This may
result in the map being equivalent to
%Type{name: :none}
but that will not automatically be collapsed to that value.
Examples:
- The empty map is
%Type.Map{}
. This is not allowed to have any k/v pairs.iex> inspect %Type.Map{} "map(%{})"
- A map with a required atom key might look as follows:
iex> inspect %Type.Map{required: %{foo: %Type{name: :integer}}} "map(%{foo: integer()})"
- A map with a required integer key might look as follows:
"map(%{1 => integer()})"iex> inspect %Type.Map{required: %{1 => %Type{name: :integer}}}
- A map with an optional key type might look as follows:
iex> inspect %Type.Map{optional: %{%Type{name: :integer} => %Type{name: :integer}}} "map(%{optional(integer()) => integer()})"
- The "any" map has optional any mapping to any (note this is distinct from empty map
%{}
)iex> inspect %Type.Map{optional: %{%Type{name: :any} => %Type{name: :any}}} "map()"
Key functions:
comparison
Maps are ordered by required keys, then optional keys. A map type with a required key comes before an equivalent map type with the key optional.
iex> import Type, only: :macros
iex> Type.compare(map(%{foo: :bar}), map(%{foo: :quux}))
:lt
iex> Type.compare(map(%{foo: :bar}), map(%{bar: :baz}))
:gt
iex> Type.compare(map(%{optional(1) => integer(), foo: integer()}),
...> map(%{optional(2) => integer(), foo: integer()}))
:lt
iex> Type.compare(map(%{foo: :bar}), map(%{optional(:foo) => :bar}))
:lt
intersection
If map types cannot support the same required keys, their intersection is none()
If one of the required keys can be created from the pool of the other key's optional types,
Then it gets realized into the intersection. Otherwise, the intersection is the
intersection of key types with key values. If the optional key or value types are totally
disjoint, then the intersection is the empty map %{}
because that term is a member
of both types
iex> import Type, only: :macros
iex> Type.intersection(map(%{foo: :bar}), map(%{bar: :baz}))
%Type{name: :none}
iex> Type.intersection(map(%{foo: :bar}), map(%{optional(atom()) => atom()}))
%Type.Map{required: %{foo: :bar}}
iex> Type.intersection(map(%{1 => 1..10}), map(%{1 => 5..20}))
%Type.Map{required: %{1 => 5..10}}
iex> Type.intersection(map(%{optional(1..10) => integer()}),
...> map(%{optional(integer()) => 1..10}))
%Type.Map{optional: %{1..10 => 1..10}}
iex> Type.intersection(map(%{optional(1..10) => integer()}),
...> map(%{optional(1..10) => atom()}))
%Type.Map{}
iex> Type.intersection(map(%{optional(1..10) => integer()}),
...> map(%{optional(11..20) => integer()}))
%Type.Map{}
union
Map unions must be extremely parsimonious, because the specifications on the unions are coupled. Not all combinations of optional and required types can be safely subjected to a union. In general, the union algorithm will merge two types if one is a strict subtype of the other.
iex> import Type, only: :macros
iex> Type.union(map(%{foo: :bar}), map(%{}))
%Type.Map{optional: %{foo: :bar}}
iex> Type.union(map(%{foo: :bar}), map(%{optional(:foo) => :bar}))
%Type.Map{optional: %{foo: :bar}}
iex> Type.union(map(%{foo: 1..10}), map(%{foo: 1..20}))
%Type.Map{required: %{foo: 1..20}}
iex> Type.union(map(%{optional(1..10) => 1..10}), map(%{optional(1..20) => 1..10}))
%Type.Map{optional: %{1..20 => 1..10}}
subtype?
A map type is a subtype of the other if all of the keys and values of one are subtypes of the other.
iex> import Type, only: :macros
iex> Type.subtype?(map(%{foo: :bar}), map(%{foo: atom()}))
true
iex> Type.subtype?(map(%{foo: :bar}), map(%{optional(:foo) => atom()}))
true
iex> Type.subtype?(map(%{optional(:foo) => :bar}), map(%{optional(:foo) => atom()}))
true
iex> Type.subtype?(map(%{optional(:foo) => :bar}), map(%{foo: atom()}))
false
usable_as
optional keys are maybe usable as required keys; required keys are always usable as optional keys.
for optional keys, if a key type is a subtype of the target's key type, then it is maybe usable; if it as a supertype of the target's key type, then it is always usable.
generally optional types are :ok
with disjoint key types, but if overlapping key types
have conflicting value types, then it is :maybe
because keys must be
iex> import Type, only: :macros
iex> Type.usable_as(map(%{foo: :bar}), map(%{optional(:foo) => :bar}))
:ok
iex> Type.usable_as(map(%{optional(:foo) => :bar}), map(%{foo: :bar}))
{:maybe, [%Type.Message{type: %Type.Map{optional: %{foo: :bar}},
target: %Type.Map{required: %{foo: :bar}}}]}
iex> Type.usable_as(map(%{optional(1..10) => 1..10}), map(%{optional(1..20) => 11..20}))
{:maybe, [%Type.Message{type: %Type.Map{optional: %{1..10 => 1..10}},
target: %Type.Map{optional: %{1..20 => 11..20}}}]}
Helper functions
The docs for helper functions documented here use set theory language, treating maps as discrete functions with preimages and images. This page may be of help to understand this language:
https://en.wikipedia.org/wiki/Image_(mathematics)#Inverse_image
Link to this section Summary
Functions
calculates the image of a map when given a clamping type for the preimage.
takes all required terms and makes them optional
the full union of all possible key values for the passed map.
true if the map type specifies a singleton type as one of its required keys
Takes a list of (disjoint) preimage types "segments" and splits them further in such a way that each segment has a consistent single type in the map's image.
Link to this section Types
Specs
Specs
Specs
Link to this section Functions
calculates the image of a map when given a clamping type for the preimage.
If any part of the clamp doesn't exist in in the map's preimage, it is ignored.
Example:
(note that 0
is in the clamp -5..5
, but is not in the map preimage)
iex> alias Type.Map
iex> import Type, only: :macros
iex> Map.apply(map(%{neg_integer() => :foo,
...> pos_integer() => :bar}), -5..5)
%Type.Union{of: [:foo, :bar]}
If any part of the clamp has conflicting definitions, it is dropped.
iex> alias Type.Map
iex> import Type, only: :macros
iex> Map.apply(map(%{0..3 => :foo, 4..5 => :bar, 4 => :baz, 5 => :quux}), 0..5)
:foo
If any part of the clamp has a more restrictive definition, all type restrictions must apply
iex> alias Type.Map
iex> import Type, only: :macros
iex> Map.apply(map(%{0..3 => 1..10, pos_integer() => 0..5}), 1..3)
1..5
takes all required terms and makes them optional
the full union of all possible key values for the passed map.
iex> alias Type.Map
iex> import Type, only: :macros
iex> Map.preimage(map(%{pos_integer() => any()}))
%Type{name: :pos_integer}
iex> Map.preimage(map(%{0 => any(), pos_integer() => any()}))
%Type.Union{of: [%Type{name: :pos_integer}, 0]}
Specs
true if the map type specifies a singleton type as one of its required keys
Specs
Takes a list of (disjoint) preimage types "segments" and splits them further in such a way that each segment has a consistent single type in the map's image.
- Discards any preimage subsegments which are not present in the map.
- Does not validate that passed list of preimages are actually disjoint.
- No guarantees are made on the order of the resulting list.
iex> alias Type.Map
iex> import Type, only: :macros
iex> Map.resegment(map(%{neg_integer() => :neg}), [-10..10])
[-10..-1]
iex> Map.resegment(map(%{neg_integer() => :neg,
...> pos_integer() => :pos}), [-10..10])
[-10..-1, 1..10]