Skip to content

Instantly share code, notes, and snippets.

@eksperimental
Last active August 11, 2016 15:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eksperimental/a6df4348e9675109e49ccf4e34101bfe to your computer and use it in GitHub Desktop.
Save eksperimental/a6df4348e9675109e49ccf4e34101bfe to your computer and use it in GitHub Desktop.
## Introducing is_kind/2, and operators: is, is_not, is_any, are, are_not, are_any

Introducing is_kind/2, and operators: is, is_not, is_any, are, are_not, are_any

Guards clauses are a key feature in Elixir. Researching how to make it easier for developers to define guards, has led me to two enhancement proposal. This is the first one, which will allow developers to write guards, guard-safe macros and regular expressions in a more natural and succinct way.

TLDR;

The following macro is allowed in guards:

  • is_kind(term, kind) determines if a given term is of a certain kind.

as well as the following operators:

  • term is kinds determines if term is each kind in kinds.
  • term is_not kinds determines if term is not of any of the kinds.
  • term is_any kinds determines if term is of any of the kinds.
  • list are kinds determines if every term in list is of every kind in kinds.
  • list are_not kinds determines if every item in list terms is not of kind.
  • list are_any kinds determines if every item in list terms is at least of one of given kinds.

is_kind/2 macro

Read is_kind/2 documentation

My main initial goal was to find a way to easily translate from typespecs definitions into guard declarations.

Let's say, given:

@spec process_value(tuple, non_neg_integer, char)
def process_value(config, value, letter)
    when is_tuple(config) and is_integer(value) and value >= 0 and letter in 0..0x10FFFF do
  do_process_value(config, value, letter)
end

I thought of creating a guard-safe macro for every type in the language (is_* functions), but that meant that a lot of new functions had to be created, thus taking the risk of poluting the language.

So I came up with is_kind/2, which takes two arguments: term and kind, so in the previous case you would call: is_kind(value, :non_neg_integer)

@spec process_value(tuple, non_neg_integer, char)
def process_value(config, value, letter)
    when is_tuple(config) and is_kind(value, :non_neg_integer) and is_kind(letter, :char) do
  do_process_value(config, value, letter)
end

The is_kind/2 macro allow us to support new kinds provided they can be implemented in a guard-safe way. Convenience functions have been added. For example, for an integer that is >= 0, a :zero_or_pos_integer kind has been created, and :non_neg_integer has been aliased to a less confusing one named :zero_or_neg_integer.

A two-element tuple can be used as kind, which extends the use of this macro for certain kinds, for example. is_kind(term, {:function, 2}) checks that term is a function of arity 2. Or is_kind(term, {:tuple, 3}) checks for a tuple with 3 elements, and is_kind(term, {:>, 10}) checks that term is greater than 10.

is_kind/2 is the building block for every guard-safe macro in this proposal. It is worth noting that we could use is_kind/2 for creating guard-safe macros, but you will see that we can use is/2 which feels more natural.

For a list of all supported functions, see the list at the end of this document.

Examples

iex> is_kind(self, :pid)
true

iex> is_kind(&(&1), :function)
true

iex> is_kind(&(&1), {:function, 1})
true

iex> is_kind([1, 2], {:list, 1})
false

is operator

Read is/2 documentation

term is kinds — Returns true if term is of each kind in kinds.

kinds can be a single kind, or a list of them.

Examples

iex> :foo is :atom
true

iex> 100 is [:even, :zero_or_pos_integer]
true

iex> 100 is [:even, :zero_or_pos_float]
false
# it doesn't match both criteria

Use in guards:

The original function definition:

@spec is_two_element_tuple(term) :: boolean
def is_two_element_tuple(term) when is_tuple(term) and (tuple_size(term, 2),
  do: true

def is_two_element_tuple(_),
  do: false

could be rewritten like this:

@spec is_two_element_tuple(term) :: boolean
def is_two_element_tuple(term) when term is {:tuple, 2},
  do: true

def is_two_element_tuple(_),
  do: false

we could use many kinds by passing a list of them,

@spec is_integer_even_greater_than_ten(term) :: boolean
def is_integer_even_greater_than_ten(term) when term is [:even, {:gt, 10}]
  do: true

def is_integer_even_greater_than_ten(_),
  do: false

We can see that is acts as a boolean and when passed many kinds in a list. We can use the is operator as a more natural way of writing clauses, and it completely replaces is_kind/2.

is/2 heavily relies on is_kind/2, and these two macros are the building blocks for all the macros that we will explore from here onwards.

is' sister operators

is_not operator

Read is_not/2 documentation

term is kinds — Returns true if term is not of all the kinds.

iex> term = 123
...> term is_not nil
true

which gives us a way of expressing conditions closer to the English natural language: instead of writing: when not is_nil(term) vs. when term is_not nil

is_not can also accept a list of kinds on the right hand side (same as is). It will return true, if term does not evaluate to true with every one of the kinds.

iex> 5 is_not [:even, :atom, :negative]
true

iex> 5 is_not [:even, :atom, :negative, :integer]
false

is_any operator

Read is_any/2 documentation

term is_any kinds — Returns true if term is at least of one kind in kinds.

Examples

iex> :foo is_any [:atom]
true

iex> 100 is_any [:even, :zero_or_pos_float]
true

iex> self is_any [:atom, :module, :port]
false

We can see that is_any/2 acts as a boolean "or".

Usage in guards:

@spec is_identifier(term) :: boolean
def is_identifier(term) when term is_any [:port, :reference],
  do: true

def is_identifier(_),
  do: false

are operator

Read are/2 documentation

list are kinds — Returns true if every term in list is of every kind in kinds.

Note that while is/2 and its sister functions take the term on the left side as a single item, are/2 and sister functions must takes on the left side a list of terms to be evaluated.

Examples

iex> [1, 34, 255] are [:integer, :positive, :byte]
true

iex> [1, 34, 0] are [:integer, :positive, :byte]
false
@spec check(pos_integer, pos_integer, pos_integer) :: boolean
def check(int1, int2, int3)
    when is_integer(int1) and int1 > 0
      and is_integer(int2) and int2 > 0
      and is_integer(int3) and int3 > 0 do
  true
end

def check(_, _, _) do
  false
end

We could rewrite it in a much more succinct way:

@spec check(pos_integer, pos_integer, pos_integer) :: boolean
def check(int1, int2, int3) when [int1, int2, int3] are :pos_integer,
  do: true

def check(_, _, _),
  do: false

now let's say you'd want all those arguments to be odd as well, you could do

def check(int1, int2, int3) when [int1, int2, int3] are [:pos_integer, :odd],
  do: true

or simplify it even more

def check(int1, int2, int3) when [int1, int2, int3] are [:odd, :positive],
  do: true

because :odd implies :integer, so that's saving us a comparison in the guards.

are's sister operators

are_not operator

Read are_not/2 documentation

term_list are_not/2 kinds — Returns true if every term in list is not of any kind in kinds.

Examples

iex> [:foo, 10.0, "string"] are_not [:integer, :zero, :pid]
true

iex> [:atom, 0.0, "string"] are_not [:integer, :zero, :pid]
false

are_any operator

Read are_any/2 documentation

term_list are_any/2 kinds — Returns true if every term in list is at least of one kind in kinds.

Examples

iex> [:foo, Kernel, 10] are_any [:atom, :integer]
true

iex> [:foo, Kernel, 10.0] are_any [:atom, :integer]
false

Notes

is_kind/2 and consequently all macros in the is and are family listed in this article can take numbers as kind.

So you can do:

iex> [39, 41, 42, 43, 45, 47] are_any [:odd, 42]
true

you can see 42 on the left will match with 42 on the right.

Limitations when used in guard expressions

There are certain limitations when these macros are used in guard expressions:

Definitions:

  • A compile-time kind is a supported kind, can be a compile-time number, a compile-time atom, or a compile-time tuple where its first element is a compile-time supported atom.

  • A compile-time list of kinds is a compile-time list, where every element is a compile-time kind.

When these macros are used in guard expressions, they have certain requirements:

  • is_kind(term, kind)

    • kind argument has to be a compile-time kind.
  • term is kinds; term is_not kinds

    • kinds has to be a compile-time kind, or a compile-time list of kinds.
  • term is_any kinds

    • kinds hand operand has to be a compile-time list of kinds.
  • list are kinds, list are_not kinds:

    • list must be a compile-time list. Items in the list do not need to be available at compile-time.
    • kinds has to be a compile-time kind, or a compile-time list of kinds.
  • list are_any kinds:

    • list must be a compile-time list. Items in the list do not need to be available at compile-time.
    • kinds must be a compile-time kind, or a compile-time list of kinds.

If these items are not defined at compile time, you will get a compile-time error.

Examples of usage in guard expressions:

def is_42(term) when term is 42,
  do: true
def is_42(_, _),
  do: false

def is_not_three_element_tuple(term, 3) when term is_not {:tuple, 3},
  do: true
def is_not_three_element_tuple(_, _),
  do: false

def is_atom_or_number(term) when term is_any [:atom, :number],
  do: true
def is_atom_or_number(_, _),
  do: false

def are_atom_or_number(item1, item2, item3) when [item1, item2, item3] are_any [:atom, :number],
  do: true
def are_atom_or_number(_, _),
  do: false

List of supported kinds by is_kind/2

# Basic and built-in types
:arity |
:atom |
:binary |
:bitstring |
:boolean |
:byte |
:char |
:float |
:fun | :function |
{:fun, arity} | {:function, arity} |          # function with arity
:identifier |
:integer |
:list |
{:list, non_neg_integer} |                    # list of a certain length
:map |
{:map, non_neg_integer} |                     # map of a certain size
:mfa |
:module |
:neg_integer |                                # negative integer
:node |
:non_neg_integer | :zero_or_pos_integer |     # integer equal or greater than 0
:nonempty_list |
:number |                                     # any integer or float
:pid |
:port |
:pos_integer |                                # positive integer
:reference |
:timeout |
:tuple |
{:tuple, non_neg_integer} |                   # tuple of a certain size

# Additional: Derived from is_* functions
:even |                                       # even integer
:nil |
:odd |                                        # odd integer
:record |
{:record, atom} |                             # record of a certain kind
{:record, pos_integer} |                      # record of a certain size

# Additional: Comparison
{:==, term} | {:eq, term} |                   # equal
{:!=, term} | {:not_eq, term} |               # not equal
{:===, term} | {:strict_eq, term} |           # strict equal
{:!==, term} | {:not_strict_eq, term} |       # not strict equal
{:<, term} | {:lt, term} |                    # less than
{:<=, term} | {:lt_eq, term} |                # less than or equal
{:>, term} | {:gt, term} |                    # greater than
{:>=, term} | {:gt_eq, term} |                # greater than or equal

# Additional: Convenience
:empty_list |                                 # []
:false |
:falsey |                                     # false or nil
:negative |                                   # negative number (integer or float)
:neg_float |                                  # negative float
:positive |                                   # positive number (integer or float)
:pos_float |                                  # positive float
:true |
:truthy |                                     # any value that is not false nor nil
:zero |                                       # 0 or 0.0
:zero_or_negative |                           # number equal or less than 0
:zero_or_neg_float |                          # float equal or less than 0.0
:zero_or_neg_integer |                        # integer equal or less than 0
:zero_or_positive |                           # number equal or greater than 0
:zero_or_pos_float |                          # float equal or greater than 0.0

# Additional: Literal numbers
integer |                                     # literal integers. Ex: 42
float                                         # literal floats. Ex: 12.34

Note: It supports all built-in data types with the exception of:

  • charlist
  • iodata
  • iolist
  • maybe_improper_list

The full implemementation can be found at: https://github.com/eksperimental/elixir/tree/is-kind

— by Eksperimental

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment