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 as required key types. Typespecs that put other types in as required will downgrade those types to be optional
  • 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:
    iex> inspect %Type.Map{required: %{1 => %Type{name: :integer}}}
    "map(%{1 => 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

optional() :: %{optional(Type.t()) => Type.t()}

Specs

required() :: %{optional(integer() | atom()) => Type.t()}

Specs

t() :: %Type.Map{optional: optional(), required: required()}

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
Link to this function

optionalize(map, opts \\ [])

View Source

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

required_key?(t(), atom() | integer()) :: boolean()

true if the map type specifies a singleton type as one of its required keys

Link to this function

resegment(map, preimages \\ [%Type{name: :any}])

View Source

Specs

resegment(t(), [Type.t()]) :: [Type.t()]

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]