• Stars
    star
    510
  • Rank 83,235 (Top 2 %)
  • Language
    Elixir
  • License
    MIT License
  • Created almost 4 years ago
  • Updated 10 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

TypeCheck: Fast and flexible runtime type-checking for your Elixir projects.

TypeCheck: Fast and flexible runtime type-checking for your Elixir projects.

hex.pm version Documentation ci Coverage Status

Core ideas

  • Type- and function specifications are constructed using (essentially) the same syntax as built-in Elixir Typespecs.
  • When a value does not match a type check, the user is shown human-friendly error messages.
  • Types and type-checks are generated at compiletime.
    • This means type-checking code is optimized rigorously by the compiler.
  • Property-checking generators can be extracted from type specifications without extra work.
    • Automatically create a spectest which checks for each function if it adheres to its spec.
  • Flexibility to add custom checks: Subparts of a type can be named, and 'type guards' can be specified to restrict what values are allowed to match that refer to these types.

Prefer to watch a presentation instead of reading? See "TypeCheck: Effortless Runtime Type Checking" - Marten Wijnja - ElixirConf EU 2022.

Usage Example

We add use TypeCheck to a module and wherever we want to add runtime type-checks we replace the normal calls to @type and @spec with @type! and @spec! respectively.

defmodule User do
  use TypeCheck
  defstruct [:name, :age]

  @type! t :: %User{name: binary, age: integer}
end

defmodule AgeCheck do
  use TypeCheck

  @spec! user_older_than?(User.t, integer) :: boolean
  def user_older_than?(user, age) do
    user.age >= age
  end
end

Now we can try the following:

iex> AgeCheck.user_older_than?(%User{name: "Qqwy", age: 11}, 10)
true
iex> AgeCheck.user_older_than?(%User{name: "Qqwy", age: 9}, 10)
false

So far so good. Now let's see what happens when we pass values that are incorrect:

iex> AgeCheck.user_older_than?("foobar", 42)
** (TypeCheck.TypeError) At lib/type_check_example.ex:28:
The call to `user_older_than?/2` failed,
because parameter no. 1 does not adhere to the spec `%User{age: integer(), name: binary()}`.
Rather, its value is: `"foobar"`.
Details:
  The call `user_older_than?("foobar", 42)` 
  does not adhere to spec `user_older_than?(%User{age: integer(), name: binary()},  integer()) :: boolean()`. Reason:
    parameter no. 1:
      `"foobar"` does not check against `%User{age: integer(), name: binary()}`. Reason:
        `"foobar"` is not a map.
    (type_check_example 0.1.0) lib/type_check_example.ex:28: AgeCheck.user_older_than?/2
iex> AgeCheck.user_older_than?(%User{name: nil, age: 11}, 10)
** (TypeCheck.TypeError) At lib/type_check_example.ex:28:
The call to `user_older_than?/2` failed,
because parameter no. 1 does not adhere to the spec `%User{age: integer(), name: binary()}`.
Rather, its value is: `%User{age: 11, name: nil}`.
Details:
  The call `user_older_than?(%User{age: 11, name: nil}, 10)` 
  does not adhere to spec `user_older_than?(%User{age: integer(), name: binary()},  integer()) :: boolean()`. Reason:
    parameter no. 1:
      `%User{age: 11, name: nil}` does not check against `%User{age: integer(), name: binary()}`. Reason:
        under key `:name`:
          `nil` is not a binary.
    (type_check_example 0.1.0) lib/type_check_example.ex:28: AgeCheck.user_older_than?/2
iex> AgeCheck.user_older_than?(%User{name: "Aaron", age: nil}, 10) 
** (TypeCheck.TypeError) At lib/type_check_example.ex:28:
The call to `user_older_than?/2` failed,
because parameter no. 1 does not adhere to the spec `%User{age: integer(), name: binary()}`.
Rather, its value is: `%User{age: nil, name: "Aaron"}`.
Details:
  The call `user_older_than?(%User{age: nil, name: "Aaron"}, 10)` 
  does not adhere to spec `user_older_than?(%User{age: integer(), name: binary()},  integer()) :: boolean()`. Reason:
    parameter no. 1:
      `%User{age: nil, name: "Aaron"}` does not check against `%User{age: integer(), name: binary()}`. Reason:
        under key `:age`:
          `nil` is not an integer.
    (type_check_example 0.1.0) lib/type_check_example.ex:28: AgeCheck.user_older_than?/2
    
iex> AgeCheck.user_older_than?(%User{name: "José", age: 11}, 10.0) 
** (TypeCheck.TypeError) At lib/type_check_example.ex:28:
The call to `user_older_than?/2` failed,
because parameter no. 2 does not adhere to the spec `integer()`.
Rather, its value is: `10.0`.
Details:
  The call `user_older_than?(%User{age: 11, name: "José"}, 10.0)` 
  does not adhere to spec `user_older_than?(%User{age: integer(), name: binary()},  integer()) :: boolean()`. Reason:
    parameter no. 2:
      `10.0` is not an integer.
    (type_check_example 0.1.0) lib/type_check_example.ex:28: AgeCheck.user_older_than?/2

And if we were to introduce an error in the function definition:

defmodule AgeCheck do
  use TypeCheck

  @spec! user_older_than?(User.t, integer) :: boolean
  def user_older_than?(user, age) do
    user.age
  end
end

Then we get a nice error message explaining that problem as well:

** (TypeCheck.TypeError) The call to `user_older_than?/2` failed,
because the returned result does not adhere to the spec `boolean()`.
Rather, its value is: `26`.
Details:
  The result of calling `user_older_than?(%User{age: 26, name: "Marten"}, 10)` 
  does not adhere to spec `user_older_than?(%User{age: integer(), name: binary()},  integer()) :: boolean()`. Reason:
    Returned result:
      `26` is not a boolean.
    (type_check_example 0.1.0) lib/type_check_example.ex:28: AgeCheck.user_older_than?/2

Features & Roadmap

Implemented

  • Proof and implementation of the basic concept
  • Custom type definitions (type, typep, opaque)
    • Basic
    • Parameterized
  • Hide implementation of opaque from documentation
  • Spec argument types checking
  • Spec return type checking
  • Spec possibly named arguments
  • Implementation of Elixir's builtin types
    • Primitive types
    • More primitive types
    • Compound types
    • special forms like |, a..b etc.
    • Literal lists
    • Maps with keys => types
    • Structs with keys => types
    • More map/list-based structures.
    • Bitstring type syntax (<<>>, <<_ :: size>>, <<_ :: _ * unit>>, <<_ :: size, _ :: _ * unit>>)
  • A when to add guards to typedefs for more power.
  • Make errors raised when types do not match humanly readable
    • Improve readability of spec-errors by repeating spec and which parameter did not match.
  • Creating generators from types
  • Don't warn on zero-arity types used without parentheses.
  • Hide structure of opaque and typep from documentation
  • Make sure to handle recursive (and mutually recursive) types without hanging.
    • A compile-error is raised when a type is expanded more than a million times
    • A macro called lazy is introduced to allow to defer type expansion to runtime (to within the check).
  • the Elixir formatter likes the way types+specs are constructed
  • A type impl(ProtocolName) to work with 'any type implementing protocol Protocolname'.
    • Type checks.
    • StreamData generator.
  • High code-coverage to ensure stability of implementation.
  • Make sure we handle most (if not all) of Typespec's primitive types and syntax. (With the exception of functions and binary pattern matching)
  • Option to turn @type/@opaque/@typep-injection off for the cases in which it generates improper results.
  • Manually overriding generators for user-specified types if so desired.
  • Creating generators from specs
    • Wrap spec-generators so you have a single statement to call in the test suite which will prop-test your function against all allowed inputs/outputs.
  • Option to turn the generation of runtime checks off for a given module in a particular environment (enable_runtime_checks).
  • Support for function-types (for typechecks as well as property-testing generators):
    • (-> result_type)
    • (...-> result_type)
    • (param_type, param2_type -> result_type)
  • Basic support for maps with a single required(type) or optional(type).
  • Overrides for builtin remote types (String.t,Enum.t, Range.t, MapSet.t etc.) (75% done) Details
  • Overrides for more builtin remote types
  • Support for maps with mixed required(type) and optional(type) syntaxes.
  • Configurable setting to turn checks on/off at compile-time, on a per-OTP-app basis (so you have control over your dependencies) as well as your individual modules.
  • Hide named types from opaque types.
  • A way to define structs and their field types at the same time.
  • Finalize formatter specification and make a generator for this so that people can easily test their own formatters.

Pre-stable

Longer-term future ideas

  • Per-module or even per-spec settings to turn on/off, configure formatter, etc.

Installation

TypeCheck is available in Hex. The package can be installed by adding type_check to your list of dependencies in mix.exs:

def deps do
  [
    {:type_check, "~> 0.13.3"},
    # To allow spectesting and property-testing data generators (optional):
    {:stream_data, "~> 0.5.0", only: :test}, 
  ]
end

The documentation can be found at https://hexdocs.pm/type_check.

Formatter

TypeCheck exports a couple of macros that you might want to use without parentheses. To make mix format respect this setting, add import_deps: [:type_check] to your .formatter.exs file.

Changelog

The full changelog can be found here

TypeCheck compared to other tools

TypeCheck is by no means the other solution out there to reduce the number of bugs in your code.

Elixir's builtin typespecs and Dialyzer

Elixir's builtin type-specifications use the same syntax as TypeCheck. They are however not used by the compiler or the runtime, and therefore mainly exist to improve your documentation.

Besides documentation, extra external tools like Dialyzer can be used to perform static analysis of the types used in your application.

Dialyzer is an opt-in static analysis tool. This means that it can point out some inconsistencies or bugs, but because of its opt-in nature, there are also many problems it cannot detect, and it requires your dependencies to have written all of their typespecs correctly.

Dialyzer is also (unfortunately) infamous for its at times difficult-to-understand error messages.

An advantage that Dialyzer has over TypeCheck is that its checking is done without having to execute your program code (thus not having any effect on the runtime behaviour or efficiency of your projects).

Because TypeCheck adds @type, @typep, @opaque and @spec-attributes based on the types that are defined, it is possible to use Dialyzer together with TypeCheck.

Norm

Norm is an Elixir library for specifying the structure of data that can be used for both validation and data-generation.

On a superficial level, Norm and TypeCheck seem similar. However, there are important differences in their design considerations.

Is it any good?

yes

More Repositories

1

elixir-tensor

The Tensor library adds support for Vectors, Matrixes and higher-dimension Tensors to Elixir.
Elixir
140
star
2

elixir-map_diff

Calculates the difference between two (nested) maps, and returns a map representing the patch of changes.
Elixir
116
star
3

elixir-arrays

Well-structured Arrays with fast random-element-access for Elixir, offering a common interface with multiple implementations with varying performance guarantees that can be switched in your configuration.
Elixir
76
star
4

ruby-prop_check

Property Testing library in Ruby
Ruby
70
star
5

elixir-fun_land

Algebraic Data Types for Elixir: Both functional and fun.
Elixir
69
star
6

elixir-capture_pipe

A pipe-macro for Elixir that allows bare function captures
Elixir
45
star
7

elixir-currying

Currying enables partial function application in Elixir.
Elixir
41
star
8

raii_with

A simple library to provide RAII in standard-compliant C99, using raii_with(resource, initializer, destructor) { ... }-syntax:
C
39
star
9

elixir-rational

Rational number library for Elixir.
Elixir
39
star
10

elixir-number

Numbers -- A generic wrapper to use *any* custom Numeric type in Elixir!
Elixir
36
star
11

elixir-specify

A library to create Comfortable, Explicit, Multi-Layered and Well-Documented Specifications for all your configurations, settings and options in Elixir.
Elixir
33
star
12

c_exceptional

A simple Exception-handling library for C99, that uses some fancy macros for true try{...}catch(err){...}finally{...} syntax!
C++
33
star
13

elixir-blocked

An Elixir-library that helps you to keep track of when hotfixes can be removed by showing compile-time warnings when issues (in your project repository or any other source-code GitHub repository) are closed.
Elixir
29
star
14

elixir-okasaki

Multiple different implementations of efficient functional queues and dequeues for Elixir
Elixir
28
star
15

elixir-revisionair

Keep track of your data structure's revisions, persistence layer agnostic.
Elixir
26
star
16

elixir-sequences

Defines a module containing many common integer sequences (even numbers, odd numbers, primes, factorials, fibonacci, etc)
Elixir
21
star
17

elixir-rustler_elixir_fun

Calling Elixir or Erlang functions from native code written in Rust
Rust
20
star
18

inscryption-secrets-hints

19
star
19

elixir-agarex

An example game for the presentation "Multiplayer Games & Collaborative Editing with Phoenix LiveView" which was written for ElixirConf.EU 2020.
Elixir
19
star
20

js1k_powder_game

JavaScript
18
star
21

elixir-revisionair_ecto

A Revisionair adapter based on Ecto. Allows you to persist and keep track of revisions of your data structures in any of Ecto's supported databases.
Elixir
18
star
22

elixir-tea_vent

Elixir library to do event-dispatching in an Event Sourcing and The Elm Architecture-like way
Elixir
16
star
23

elixir-solution

A Macro-based approach to ease working with ok/error tuples in Elixir
Elixir
16
star
24

elixir-prioqueue

Priority Queues for Elixir.
Elixir
15
star
25

elixir_gen_frp

Elixir
15
star
26

cpp-parser_combinators

An example of how to construct a parser combinator library in C++. Focus on simplicity, not efficiency.
C++
14
star
27

elixir-riak_ecto3

RiakEcto3 is an Elixir Ecto 3 Adapter for the Riak KV database (For Riak KV v 2.0 and upward).
Elixir
13
star
28

Jux

Elixir
12
star
29

elixir_persistent_gen_server

Elixir
11
star
30

ExtremeBytebeats

A collection of bytebeat programs, some of them rather involved.
JavaScript
11
star
31

elixir_complex_num

Complex Numbers for Elixir, wrapping not only floats but _any_ kind of numeric data type.
Elixir
11
star
32

elixir-arrays_aja

Provides an `Arrays` implementation for `Aja`'s `Vector` datatype, which is an implementation of a 'Hickey Trie' Vector written in Elixir.
HTML
8
star
33

elixir-orderable

A protocol for making your custom datastructures orderable (for sorting and comparing.).
Elixir
7
star
34

rust-overloaded_literals

Rust crate: Overloaded Literals to construct your datatypes without boilerplate and with compile-time validation.
Rust
7
star
35

rust-backdrop_arc

An Arc (atomically reference counted smart pointer) that supports customized dropping strategies using Backdrop.
Rust
5
star
36

LastMail

LastMail is the world's first passive post-mortem message system. It lets you send a good-bye to your friends and contacts, and pass on your (digital) belongings.
PHP
4
star
37

elixir-unicode

The Elixir Unicode package provides functionality to check properties of unicode codepoints, graphemes and strings.
Elixir
4
star
38

elixir_ordered_siblings

Ordered Siblings: Allows reordering (in Ecto) of comments under posts, images under albums, songs under playlists, etc.
Elixir
4
star
39

elixir_guard_safe_modulo

The ridiculous guard-safe implementation for a floored modulo operation.
Elixir
4
star
40

rust-backdrop

Drop your large or complex Rust objects in the background using Backdrop!
Rust
4
star
41

SimpleRNG

A very simple Xorshift-based RNG, whose goal is to reproduce the same results regardless of platform, language or environment
Haskell
4
star
42

JS-CoinStackBar

A customizable vertical progress bar in the shape of a stack of coins.
JavaScript
3
star
43

elixir-cubdb_nerves_example

An example of using CubDB with Nerves
Elixir
3
star
44

elixir-extractable

A lightweight reusable Extractable protocol for Elixir, allowing extracting elements one-at-a-time from a collection.
Elixir
3
star
45

elixir-fun_landic

Implementations of common Algorithmic Data Types for Elixir, built on top of the `fun_land` interface.
Elixir
3
star
46

ruby-doctest2

Doctests (documentation testing) for Ruby
Ruby
3
star
47

elixir-password-bloomex

Example of using a bloom filter to check for prohibited passwords
Elixir
3
star
48

elixir-sets

Sets for Elixir, with a single interface and varying implementations.
Elixir
3
star
49

photoboothy

Nerves Rpi 3 Photobooth
JavaScript
3
star
50

rust-slimmer_box

`SlimmerBox<T>` is a packed alternative to `Box<T>` whose 'fat' pointer is 'slimmer'
Rust
3
star
51

elixir-insertable

A lightweight reusable Insertable protocol for Elixir, allowing inserting elements one-at-a-time into a collection.
Elixir
3
star
52

elixir-benchmarking_then_genserver

A test to benchmark the performance of `Kernel.then/2` when running it in a semi 'real-life' scenario when managing the state of a GenServer
Erlang
2
star
53

elixir-stream_data-property-example

Example of how I'd like to nest multiple properties together with the stream_data library
Elixir
2
star
54

c_treat

A proof-of-concept Trait system for C
C
2
star
55

alchemud

An (very early, very crude) to write a Multi-User-Dungeon in Elixir
Elixir
2
star
56

concurrency_elixir_slides

This repository contains both the code snippets as well as the slides from my presentation "A Winner is You: Concurrency in Elixir"
Elixir
2
star
57

elixir-sliding_window

Elixir 'Sliding Window'
Elixir
2
star
58

rust-bassert

Better assertions for Rust
Rust
2
star
59

haskell-vary

A fast and user-friendly implementation of variant types (aka open unions, open sum types, coproducts)
Haskell
2
star
60

python-multiple_indexed_collection

A collection type for arbitrary objects, that indexes them based on (a subset of) their properties.
Python
2
star
61

elixir-arrays_rrb_vector

Wraps Rust's `im::Vector`, an immutable Relaxed-Radix-Balanced Trie-based vector, for usage with the Elixir `Arrays` library
HTML
2
star
62

rust-naperian

(Experimental) Rust implementation of the paper 'APLicative programming using Naperian functors'
Rust
2
star
63

EpicAuth

Epic! Auth!
Ruby
1
star
64

cc_freeops

An implementation of automatic free binary operator generation when their respective compound operators are known.
Forth
1
star
65

elixir-test-benchmrking_then

A test repo to benchmark the overhead of Kernel.then/2
Elixir
1
star
66

elixir-coerce

Automatic coercion of types that can be promoted to each-other for Elixir.
Elixir
1
star
67

elixir-iter

Iterators for Elixir datastructures
Elixir
1
star
68

cpp-traits-with-variants

Example of how to implement Traits in C++ that also work when constructing a collection of trait-implementing objects.
C++
1
star
69

okasaki_benchmark

Elixir
1
star
70

cpp-thunk

Experimental code to do lazy (i.e. on-demand) evaluation of values in C++
C++
1
star
71

elixir-enum_benchmark_example

Microbenchmark comparing the Elixir iteration abstractions for, enum and iter
Elixir
1
star
72

Haskell-sound-playing

A simple example of how to use the SDL2 Haskell bindings to play sound files (as a complete Stack setup with dependencies, because that was most of the hassle)
Haskell
1
star
73

elixir_online-game

A proof-of-concept online game, to test scalability of having many concurrent player-states updating.
Elixir
1
star
74

rust-polars_s3

POC to write to Object Stores like S3 with Polars
Rust
1
star
75

ifs_zoom

W-M's Bachelor Project about IFS zooming
HTML
1
star
76

elixir-experimental_comparable

An experimental implementation of a possible Comparable protocol for Elixir.
Elixir
1
star
77

haskell-snek

A simple implementation of Snake. Runs in a terminal
Haskell
1
star
78

roc-bf_example

A simple (and incomplete) example of a BrainFukc interpreter in the Roc programming language.
LLVM
1
star
79

symmath

Elixir
1
star
80

elixir_calendrical

Elixir
1
star
81

c_hash_map

A very simple HashMap-implementation in C
C
1
star
82

riak-cluster-test

Repository of code about running a Riak cluster using docker
1
star
83

presentation-communicating_between_ruby_and_elixir

Presentation on how you can communicate between a separately running Ruby system and Elixir system, as we do it in Planga.io
JavaScript
1
star
84

haskell-MonotonicityTypes

An implementation of 'Sfuns', functions that keep track of their monotonicity, from the Monotonicity Types paper
HTML
1
star
85

RUG-WebEngineering-Elm

Presentation + Sample project for the Elm (and Sass) tutorial.
JavaScript
1
star