It was long overdue:
Version 0.4.0 has been released!
This adds two main features:
- Protocol-based types
- Type overrides.
Protocol-based types
This adds supports for impl(ProtocolName)
. This is a way to use "any type that implements protocol ProtocolName
" in your types and specs.
TypeCheck supports this both in type-checks (checking whether a protocol is implemented for the given term), as well as for property-testing generation (generating "any value of any type that implements ProtocolName
"):
An example of using protocols in specs:
defmodule OverrideExample do
use TypeCheck
@spec! average(impl(Enumerable)) :: {:ok, float()} | {:error, :empty}
def average(enumerable) do
if Enum.empty?(enumerable) do
{:error, :empty}
else
res = Enum.sum(enumerable) / Enum.count(enumerable)
{:ok, res}
end
end
end
OverrideExample.average([10, 20])
{:ok, 15.0}
iex(18)> OverrideExample.average(MapSet.new([1,2,3,4]))
{:ok, 2.5}
OverrideExample.average([])
{:error, :empty}
OverrideExample.average(10)
** (TypeCheck.TypeError) At iex:11:
The call to `average/1` failed,
because parameter no. 1 does not adhere to the spec `impl(Enumerable)`.
Rather, its value is: `10`.
Details:
The call `average(10)`
does not adhere to spec `average(impl(Enumerable)) :: {:ok, float()} | {:error, :empty}`. Reason:
parameter no. 1:
`10` does not implement the protocol `Elixir.Enumerable`
lib/type_check/spec.ex:156: OverrideExample.average/1
And an example of some generating some data:
iex> require TypeCheck.Type
iex> import TypeCheck.Builtin
iex> TypeCheck.Type.build(impl(Enumerable)) |> TypeCheck.Protocols.ToStreamData.to_gen |> Enum.take(5)
[
%{{false, ""} => -1.0},
#MapSet<[]>,
-2..2,
0..3,
%{:EG => {}, {false} => 1.0, [] => %{-1 => ""}}
]
Type Overrides
From time to time we need to interface with modules written in other libraries (or the Elixir standard library) which do not expose their types through TypeCheck yet.
We want to be able to use those types in our checks, but they exist in modules that we cannot change ourselves.
The solution is to allow a list of ‘type overrides’ to be given as part of the options passed to use TypeCheck
, which allow you to use the original type in your types and documentation, but have it be checked (and potentially property-generated) as the given TypeCheck-type.
An example:
defmodule Original do
@type t() :: any()
end
defmodule Replacement do
use TypeCheck
@type! t() :: integer()
end
defmodule Example do
use TypeCheck, overrides: [{&Original.t/0, &Replacement.t/0}]
@spec! times_two(Original.t()) :: integer()
def times_two(input) do
input * 2
end
end
Or indeed:
defmodule TypeOverrides do
use TypeCheck
import TypeCheck.Builtin
@opaque! custom_enum() :: impl(Enumerable)
end
defmodule Example do
use TypeCheck, overrides: [{&Enum.t/0, &TypeOverrides.custom_enum/0}]
@spec! average(Enum.t()) :: {:ok, float()} | {:error, :empty}
def average(enumerable) do
# ... (see first example of post)
end
end
As this feature is still very new, there are bound to still be some bugs or edge cases in there.
Also, it would be nice to have support by default already provided for all remote types of Elixir’s standard library.
This is something which will be added in the very near future; probably in the next release.
What is next?
A detailed long-term roadmap is available in the Readme.
In the short-term, focus is on the following:
- Improve code-coverage of the testing suite
- Move the CI from Travis to GitHub’s workflows, and test against newer (and possibly some older) Elixir versions
- Add a set of ‘default overrides’ for the common remote types that are part of the Elixir standard library (such as
Enum.t()
, Range.t()
, DateTime.t()
etc.).
- Be able to limit the depth of the generated checks, to further increase performance for production environments.