Effortless Runtime Type-Checking
in Elixir
Wiebe-Marten Wijnja
Outline
- What and why?
- Type-Checking?
- Runtime Type-Checking?
- Effortless Runtime Type-Checking?
- How to use TypeCheck?
- Basic Usage
- 'Behind the scenes'
- Advanced features
- The future
What is type checking?
What is a type?
→ a set of possible values
What is type checking?
→ Is value one of the allowed possibilities?
Thinking of types depends a lot on language
Dynamically-typed
Statically-typed
Dynamically-typed
- Values can be of many types
- Type known at the very last moment
Statically-typed
Dynamically-typed
- Values can be of many types
- Type known at the very last moment
Statically-typed
def myfun(a, b) do
# ... more code here
c = a - b # ???
# ... more code here
end
Dynamically-typed
- Values can be of many types
- Type known at the very last moment
Statically-typed
def myfun(a, b) do
# ... more code here
c = a - b # ???
# ... more code here
end
myfun(10, 20)
myfun(10, "abcd")
Dynamically-typed
- Values can be of many types
- Type known at the very last moment
Statically-typed
- Values have single type
- Type known beforehand
- (explicitly specified)
def myfun(a, b) do
# ... more code here
c = a - b # ???
# ... more code here
end
myfun(10, 20)
myfun(10, "abcd")
Dynamically-typed
- Values can be of many types
- Type known at the very last moment
Statically-typed
- Values have single type
- Type known beforehand
- (explicitly specified)
def myfun(a, b) do
# ... more code here
c = a - b # ???
# ... more code here
end
fn myfun(a: Int, b: Int) -> Int {
// ... more code here
c = a - b // <- Known to be fine
// ... more code here
}
myfun(10, 20)
myfun(10, "abcd")
Dynamically-typed
- Values can be of many types
- Type known at the very last moment
Statically-typed
- Values have single type
- Type known beforehand
- (explicitly specified)
def myfun(a, b) do
# ... more code here
c = a - b # ???
# ... more code here
end
fn myfun(a: Int, b: Int) -> Int {
// ... more code here
c = a - b // <- Known to be fine
// ... more code here
}
myfun(10, 20)
myfun(10, "abcd")
Find problems earlier...
... by being more explicit
Dynamically-typed
Statically-typed
It Depends
→ root cause
To Type or Not to Type? | Bogner & Merkel
Dynamically-typed
Statically-typed
Dynamically-typed
Statically-typed
Elixir (and Erlang)
- Dynamically Typed
- ... but also:
- Typespecs
- Dialyzer
- Typespecs
Typespecs
- Pros:
- Extra clarity for input & output of functions
- Behaviours + callbacks
- Cons:
- On its own, documentation only
- Some common types not supported!
- string constants -> maps with string keys!
- Protocols
- Custom invariants?
@spec is_even(integer()) :: boolean()
Dialyzer
- Pros:
- Checks (using the typespecs) for common mistakes
- Runs statically
- Cons:
- Misses common mistakes (false negatives)
- Complains about some OK code (false positives)
- Slow to run, difficult to understand errors
- → Opt-in
Gradualizer: Promising, but experimental
Alternative approach?
- Type system has limitations
- Errors are unclear
- Not everything can be checked statically
The Contract of a Function
The Contract of a Function
- Property of the algorithm
- Visibility varies by implementation
- Breaking contract → bug
Clarity
defmodule Foo do
def bar(a, b) do
# ...
end
end
defmodule Users.Highscore do
def top10(a, b) do
# ...
end
end
defmodule Users.Highscore do
def top10(users, scoring_fun) do
# ...
end
end
defmodule Users.Highscore do
@spec top10(list(User.t()), (User.t() -> float())) :: list(User.t())
def top10(users, scoring_fun) do
# ...
end
end
defmodule Users.Highscore do
@spec top10(list(User.t()), (User.t() -> float())) :: list(User.t())
def top10(users, scoring_fun)
when is_list(users) and is_function(scoring_fun, 1) do
# ...
end
end
defmodule Users.Highscore do
@spec top10(list(User.t()), (User.t() -> float())) :: list(User.t())
def top10(users, scoring_fun)
when is_function(scoring_fun, 1) do
ensure_list_of_users!(users)
# ...
end
defp ensure_list_of_users!(users) do
Enum.each(users, fn element ->
case element do
%User{} -> :ok
other -> raise("Error, #{inspect(other)} is not a user.")
end)
end
end
defmodule Users.Highscore do
@spec top10(list(User.t()), (User.t() -> float())) :: list(User.t())
def top10(users, scoring_fun)
when is_function(scoring_fun, 1) do
ensure_list_of_users!(users)
# ...
end
defp ensure_list_of_users!(users) do
Enum.each(users, fn element ->
case element do
%User{} -> :ok
other -> raise("Error, #{inspect(other)} is not a user.")
end)
end
end
- Diaspora of error formats
- More difficult checks not done at all
- Duplication! (in different syntax!!)
defmodule Users.Highscore do
use TypeCheck
@spec! top10(list(User.t()), (User.t() -> float())) :: list(User.t())
def top10(users, scoring_fun) do
# ...
end
end
→ Generate checks from typespecs
→ Same typespec still usable for Dialyzer
→ Errors are standardized
→ Difficult checks also happen!
'Effortless'
use TypeCheck:
defmodule Users.Highscore do
use TypeCheck
@spec! top10(list(User.t()), (User.t() -> float())) :: list(User.t())
def top10(users, scoring_fun) do
# ...
end
end
defmodule Users.Highscore do
use TypeCheck
@spec top10(list(User.t()), (User.t() -> float())) :: list(User.t())
def top10(users, scoring_fun) do
# ...
end
defoverridable top10: 2
def top10(users, scoring_fun) do
TypeCheck.conforms!(users, list(User.t))
TypeCheck.conforms!(scoring_fun, (User.t() -> float()))
result = super(users, scoring_fun)
TypeCheck.conforms!(result, list(User.t()))
result
end
# Used for reflection & spectests
def __TypeCheck_spec_for_'top10/2'__ do
%TypeCheck.Builtin.Spec{
#...
}
end
end
NOTE: actual checks
- compiled inline
- keep track of parameter index
How readable are the error messages?
Example of error messages
Example of error messages
Example of error messages
Example of error messages
Example of error messages
Type extensions
- ranges for floats
- fixed-size lists
- string constants
- maps with string constant keys
- impl(ProtocolName)
- 'Type guards' to add custom checks
Need more detailed types?
Is it fast enough?
- Yes: It grows with the size of your input
- And you can always turn it off in certain environments
- (per env, per OTP app, per module, per function)
OTP 25: Type-based optimizations in the JIT
https://www.erlang.org/blog/type-based-optimizations-in-the-jit/
Advanced Features
-
Manual use of
- Custom formatters
- Overrides for external types
-
defstruct!
- Spectests
TypeCheck.conforms! / TypeCheck.conforms? / TypeCheck.conforms
Spectests
- Effortless property-based testing
- Test against any valid input automatically
Higher Confidence
Spectests
defmodule Rating do
use TypeCheck
defstruct [:value, :author]
@type! t() :: %Rating{value: 1..5, author: String.t()}
@spec! average(list(t())) :: number()
def average(ratings) do
values = Enum.map(ratings, &(&1.value))
Enum.sum(values) / Enum.count(values)
end
end
Spectests
defmodule RatingTest do
use ExUnit.Case, async: true
use TypeCheck.ExUnit
spectest Rating
# ... other tests :-)
end
Spectests
If spectests are not enough,
write your own property-based tests
→ Types are usable as generators
How resilient is TypeCheck itself?
Dogfooding
Type definitions
Type-checking macros
Dogfooding
- TypeCheck is thorougly (spec)tested
- Metatypes
- When using TypeCheck, same checks performed
- At compile time?!
Future Plans
- Stable release (nearly there!)
- Companion libraries
- type overrides for common libs
(Phoenix, Ecto, Jason, Decimal, Absinthe, etc.)
- type overrides for common libs
- Improve efficiency
- Support PropEr as well (as StreamData)
Help greatly appreciated!
Conclusion
- Type checks improve code resilience + clarity
- TypeCheck: runtime type-checking to Elixir
- Simple to get started, effortless to use
Out now!
TypeCheck - Effortless Type-Checking in Elixir
By qqwy
TypeCheck - Effortless Type-Checking in Elixir
TypeCheck is an Elixir library that takes your existing Elixir types + specs and builds runtime type-checks, clear error messages, improved documentation and automated property-based ‘spectests’ on top of them! In this talk, Marten will explain how simple it is to use TypeCheck in your projects and why and when you’d want to. Also, he will give a small peek behind the curtain of the extensive not-so-simple metaprogramming required to make all of this happen ;-). https://github.com/Qqwy/elixir-type_check
- 664