View Source TypeCheck.Macros (TypeCheck v0.12.0)
Contains the @spec!
, @type!
, @typep!
, @opaque!
macros to define runtime-checked function- and type-specifications.
usage
Usage
This module is included by calling use TypeCheck
.
This will set up the module to use the special macros.
Usually you'll want to use the module attribute-style of the macros, like
@spec!
and @type!
.
Using these forms has two advantages over using the direct calls:
- Syntax highlighting will highlight the types correctly and the Elixir formatter will not mess with the way you write your type.
- It is clear to people who have not heard of
TypeCheck
before that@type!
and@spec!
will work similarly to resp.@type
and@spec
.
avoiding-naming-conflicts-with-typecheck-builtin
Avoiding naming conflicts with TypeCheck.Builtin
If you want to define a type with the same name as one in TypeCheck.Builtin,
(which is not particularly recommended),
you should hide those particular functions from TypeCheck.Builtin by adding
import TypeCheck.Builtin, except: [...]
below use TypeCheck
manually.
calling-the-explicit-implementations
Calling the explicit implementations
In case you are working in an environment where the @/1
is already overridden
by another library, you can still use this library,
by simply adding import TypeCheck.Macros, except: [@: 1]
to your module
and calling the direct versions of the macros instead.
typecheck-and-metaprogramming
TypeCheck and metaprogramming
In certain cases you might want to use TypeCheck to dynamically generate
types or functions, such as to add @spec!
-s to functions
that themselves are dynamically generated.
TypeCheck's macros support 'unquote fragments',
just like many builtin 'definition' constructs like def
, but also @type
do.
(c.f. Elixir.Kernel.SpecialForms.quote/2
for more details about unquote fragments.)
An example:
defmodule MetaExample do
use TypeCheck
people = ~w[joe robert mike]a
for name <- people do
@type! unquote(name)() :: %{name: unquote(name), coolness_level: :high}
end
end
iex> MetaExample.joe
#TypeCheck.Type< TypeCheck.MacrosTest.MetaExample.joe() :: %{coolness_level: :high, name: :joe} >
iex> MetaExample.mike
#TypeCheck.Type< TypeCheck.MacrosTest.MetaExample.mike() :: %{coolness_level: :high, name: :mike} >
Macros
Inside macros, we use unquote fragments in the same way.
There is however one more thing to keep in mind:
You'll need to add a call to import Kernel, except: [@: 1]
in your macro (before the quote)
to make sure you can call @type!
, @spec!
etc.
This is a subtle consequence of Elixir's macro-hygiene rules.
See this issue on Elixir's GitHub repository for more info
(Alternatively, directly calls to type!
, spec!
etc. are possible without overriding the import.)
An example:
defmodule GreeterMacro do
defmacro generate_greeter(greeting) do
import Kernel, except: [@: 1] # Ensures TypeSpec's overridden `@` is used in the quote
quote do
@spec! unquote(greeting)(binary) :: binary
def unquote(greeting)(name) do
"#{greeting}, #{name}!"
end
end
end
end
defmodule GreeterExample do
use TypeCheck
require GreeterMacro
GreeterMacro.generate_greeter(:hi)
GreeterMacro.generate_greeter(:hello)
end
iex> GreeterExample.hi("John")
"hi, John!"
iex> GreeterExample.hello("Frank")
"hello, Frank!"
iex> GreeterExample.hi(42)
** (TypeCheck.TypeError) At test/type_check/macros_test.exs:32:
The call to `hi/1` failed,
because parameter no. 1 does not adhere to the spec `binary()`.
Rather, its value is: `42`.
Details:
The call `hi(42)`
does not adhere to spec `hi(binary()) :: binary()`. Reason:
parameter no. 1:
`42` is not a binary.
About use TypeCheck
The use TypeCheck
statement adds an @before_compile
-hook to the final module,
which is used to wrap functions with the specified runtime type-checks.
This means that some care needs to be taken to ensure that a call to use TypeCheck
exists
in the final module, if you're generating specs dynamically from inside macros.
Hiding the autogenerated typespec
By default, TypeCheck
will automatically generate @type
, @opaque
and @spec
-attributes,
which will be shown in the documentation, as well as used by tools such as Dialyzer.
In rare situations, TypeCheck
might try to generate typespecs which are invalid.
(In such case, please open a bug report!)
Or sometimes, you might want to alter the type which is exported.
In such situations, you can disable the autogeneration of these attributes,
by calling @autogen_typespec false
just before the next @type!
/@opaque!
/@spec!
:
defmodule AutogenTypespecsExample do
use TypeCheck
# The typespec of `foo` is auto-generated:
# A line `@type foo() :: integer()` will be visible in the documentation/Dialyzer.
@type! foo() :: integer()
# The typespec of `bar` is _not_ auto-generated.
# As such, we could write a completely different `@type` (or leave it out all-together).
@autogen_typespec false
@type! bar() :: integer()
end
iex>t AutogenTypespecsExample # Will show type of `foo` but not `bar`
"@type foo() :: integer()"
Link to this section Summary
Functions
Define a opaque type specification.
Define a function specification.
Define a public type specification.
Define a private type specification.
Link to this section Functions
Define a opaque type specification.
Usually invoked as @opaque!
This behaves similarly to Elixir's builtin @opaque
attribute,
and will create a type whose name is public
but whose structure is private.
Calling this macro will:
- Fill the
@opaque
-attribute with a Typespec-friendly representation of the TypeCheck type. - Add a (or append to an already existing)
@typedoc
detailing that the type is managed by TypeCheck, and containing the name of the TypeCheck type. (not the definition, since it is an opaque type). - Define a (hidden) public function with the same name (and arity) as the type
that returns the TypeCheck.Type as a datastructure when called.
This makes the type usable in calls to:
- definitions of other type-specifications (in the same or different modules).
- definitions of function-specifications (in the same or different modules).
TypeCheck.conforms/2
and variants,TypeCheck.Type.build/1
Define a function specification.
Usually invoked as @spec!
A function specification will wrap the function with checks that each of its parameters are of the types it expects. as well as checking that the return type is as expected.
usage
Usage
The syntax is essentially the same as for built-in @spec
attributes:
@spec! function_name(type1, type2) :: return_type
It is also allowed to introduce named types:
@spec! days_since_epoch(year :: integer, month :: integer, day :: integer) :: integer
Note that TypeCheck
does not allow the when
keyword to be used
to restrict the types of recurring type variables (which Elixir's
builtin Typespecs allow). This is because:
- Usually it is more clear to give a recurring type an explicit name.
- The
when
keyword is used instead for TypeCheck's type guards'. (SeeTypeCheck.Builtin.guarded_by/2
for more information.)
Define a public type specification.
Usually invoked as @type!
This behaves similarly to Elixir's builtin @type
attribute,
and will create a type whose name and definition are public.
Calling this macro will:
- Fill the
@type
-attribute with a Typespec-friendly representation of the TypeCheck type. - Add a (or append to an already existing)
@typedoc
detailing that the type is managed by TypeCheck, and containing the full definition of the TypeCheck type. - Define a (hidden) public function with the same name (and arity) as the type
that returns the TypeCheck.Type as a datastructure when called.
This makes the type usable in calls to:
- definitions of other type-specifications (in the same or different modules).
- definitions of function-specifications (in the same or different modules).
TypeCheck.conforms/2
and variants,TypeCheck.Type.build/1
usage
Usage
The syntax is essentially the same as for the built-in @type
attribute:
@type! type_name :: type_description
It is possible to create parameterized types as well:
@type! dict(key, value) :: [{key, value}]
named-types
Named types
You can also introduce named types:
@type! color :: {red :: integer, green :: integer, blue :: integer}
Not only is this nice to document that the same type is being used for different purposes, it can also be used with a 'type guard' to add custom checks to your type specifications:
@type! sorted_pair(a, b) :: {first :: a, second :: b} when first <= second
Define a private type specification.
Usually invoked as @typep!
This behaves similarly to Elixir's builtin @typep
attribute,
and will create a type whose name and structure is private
(therefore only usable in the current module).
- Fill the
@typep
-attribute with a Typespec-friendly representation of the TypeCheck type. - Define a private function with the same name (and arity) as the type
that returns the TypeCheck.Type as a datastructure when called.
This makes the type usable in calls (in the same module) to:
- definitions of other type-specifications
- definitions of function-specifications
TypeCheck.conforms/2
and variants,TypeCheck.Type.build/1