Typed Elixir

An update after months!

So far these all work as expected, it uses types and spec’s when it can, and infer’s it otherwise when it cannot (or complains):

iex> use TypedElixir
TypedElixir

iex> defmodulet TypedTest_Empty do
...> end
{:module, TypedTest_Empty,
 <<70, 79, 82, 49, 0, 0, 3, 244, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 94,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, nil}

iex> defmodulet TypedTest_Typed_Simple do
...>   @spec simple() :: nil
...>   def simple(), do: nil
...> end
{:module, TypedTest_Typed_Simple,
 <<70, 79, 82, 49, 0, 0, 4, 196, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 129,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:simple, 0}}
iex> TypedTest_Typed_Simple.simple()
nil

iex> defmodulet TypedTest_Untyped_Simple do
...>   def simple(), do: nil
...> end
{:module, TypedTest_Untyped_Simple,
 <<70, 79, 82, 49, 0, 0, 4, 176, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 129,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:simple, 0}}
iex> TypedTest_Untyped_Simple.simple()
nil

iex> defmodulet TypedTest_Untyped_Recursive_Simple_BAD_NoSet do
...>   def simple(), do: simple()
...>   def willFail() do
...>     x = simple()
...>   end
...> end
** (throw) {:INVALID_ASSIGNMENT_NOT_ALLOWED, :no_return}
    (typed_elixir) lib/typed_elixir.ex:232: TypedElixir.type_check_expression/3
    (typed_elixir) lib/typed_elixir.ex:198: TypedElixir.type_check_def_body/3
    (typed_elixir) lib/typed_elixir.ex:121: TypedElixir.type_check_body/3
    (typed_elixir) lib/typed_elixir.ex:104: anonymous fn/3 in TypedElixir.typecheck_module/4
          (elixir) lib/enum.ex:1755: Enum."-reduce/3-lists^foldl/2-0-"/3
    (typed_elixir) lib/typed_elixir.ex:103: TypedElixir.typecheck_module/4
    (typed_elixir) expanding macro: TypedElixir.defmodulet/2
                   iex:5: (file)
iex> # Cannot set the return value of a function that never returns...
nil

iex> # The extra type is to give a name to the type in simple, so the input and output become the same type.
nil
iex> # If the spec was `simple(any()) :: any()` then you could not state that the output type is based on the input type.
nil
iex> defmodulet TypedTest_Typed_Identity do
...>   @type identity_type :: any()
...>   @spec identity(identity_type) :: identity_type
...>   def identity(x), do: x
...> end
{:module, TypedTest_Typed_Identity,
 <<70, 79, 82, 49, 0, 0, 5, 48, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 191,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:identity, 1}}
iex> TypedTest_Typed_Identity.identity(42)
42
iex>
nil

iex> defmodulet TypedTest_Typed_Identity_badtype do
...>   @type identity_type :: any()
...>   @spec identity(identity_type) :: identity_type
...>   def identity(_x), do: nil
...> end
** (throw) {:NO_TYPE_RESOLUTION, %TypedElixir.Type.Const{const: :atom, meta: %{values: [nil]}}, %TypedElixir.Type.Ptr.Generic{id: 0, named: true}}
    (typed_elixir) lib/typed_elixir.ex:514: TypedElixir.resolve_types_nolinks/3
    (typed_elixir) lib/typed_elixir.ex:454: TypedElixir.resolve_types!/3
    (typed_elixir) lib/typed_elixir.ex:168: TypedElixir.resolve_fun_return_type_/4
    (typed_elixir) lib/typed_elixir.ex:146: TypedElixir.resolve_fun_return_type/4
    (typed_elixir) lib/typed_elixir.ex:127: TypedElixir.type_check_body/3
    (typed_elixir) lib/typed_elixir.ex:104: anonymous fn/3 in TypedElixir.typecheck_module/4
          (elixir) lib/enum.ex:1755: Enum."-reduce/3-lists^foldl/2-0-"/3
    (typed_elixir) lib/typed_elixir.ex:103: TypedElixir.typecheck_module/4
iex> # Since it is a named type the input and output must match
nil

iex> defmodulet TypedTest_Typed_Identity_AnyReturn do
...>   @spec identity(any()) :: any()
...>   def identity(_x), do: nil
...> end
{:module, TypedTest_Typed_Identity_AnyReturn,
 <<70, 79, 82, 49, 0, 0, 4, 248, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 152,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:identity, 1}}
iex> TypedTest_Typed_Identity_AnyReturn.identity(42)
nil
iex> # An unnamed `any()` means it can literally return anything, the input does not matter
nil

iex> defmodulet TypedTest_Untyped_Identity do
...>   def identity(x), do: x
...> end
{:module, TypedTest_Untyped_Identity,
 <<70, 79, 82, 49, 0, 0, 4, 204, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 149,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:identity, 1}}
iex> TypedTest_Untyped_Identity.identity(42)
42

iex> defmodulet TypedTest_Untyped_Identity_AnyReturn do
...>   def identity(_x), do: nil
...> end
{:module, TypedTest_Untyped_Identity_AnyReturn,
 <<70, 79, 82, 49, 0, 0, 4, 236, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 152,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:identity, 1}}
iex> TypedTest_Untyped_Identity_AnyReturn.identity(42)
nil
iex> # Since it is not typed it gets an inferred return value of `nil`
nil

iex> defmodulet TypedTest_Typed_Recursive_Counter do
...>   @spec counter(integer()) :: integer()
...>   def counter(x), do: counter(x)
...> end
{:module, TypedTest_Typed_Recursive_Counter,
 <<70, 79, 82, 49, 0, 0, 4, 248, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 148,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:counter, 1}}
iex> # Not calling it because infinite recursion...
nil

iex> defmodulet TypedTest_Untyped_Recursive_Counter do
...>   def counter(x), do: counter(x)
...> end
{:module, TypedTest_Untyped_Recursive_Counter,
 <<70, 79, 82, 49, 0, 0, 4, 224, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 148,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:counter, 1}}
iex> # Again, not calling it because infinite recursion...

iex> defmodulet TypedTest_Typed_Recursive_Counter_Bad do
...>   @spec counter(integer()) :: integer()
...>   def counter(x), do: counter(6.28)
...> end
** (throw) {:NO_TYPE_UNIFICATION, :NO_PATH, %TypedElixir.Type.Const{const: :integer, meta: %{}}, %TypedElixir.Type.Const{const: :float, meta: %{values: [6.28]}}}
    (typed_elixir) lib/typed_elixir.ex:567: TypedElixir.unify_types_nolinks/3
    (typed_elixir) lib/typed_elixir.ex:521: TypedElixir.unify_types!/3
          (elixir) lib/enum.ex:1229: Enum."-map/2-lists^map/1-0-"/2
    (typed_elixir) lib/typed_elixir.ex:250: TypedElixir.type_check_expression/3
    (typed_elixir) lib/typed_elixir.ex:201: TypedElixir.type_check_def_body/3
    (typed_elixir) lib/typed_elixir.ex:121: TypedElixir.type_check_body/3
    (typed_elixir) lib/typed_elixir.ex:104: anonymous fn/3 in TypedElixir.typecheck_module/4
          (elixir) lib/enum.ex:1755: Enum."-reduce/3-lists^foldl/2-0-"/3


iex> defmodulet TypedTest_Untyped_Recursive_Counter_RecallingDifferentType do
...>   def counter(_x), do: counter(6.28)
...> end
{:module, TypedTest_Untyped_Recursive_Counter_RecallingDifferentType,
 <<70, 79, 82, 49, 0, 0, 5, 56, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 151,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:counter, 1}}
iex> # Not calling this either, infinite recursion
nil

iex> defmodulet TypedTest_Typed_MultiFunc0 do
...>   @spec simple() :: nil
...>   def simple(), do: nil
...> 
...>   @type identity_type :: any()
...>   @spec identity(identity_type) :: identity_type
...>   def identity(x), do: x
...> 
...>   @spec call_simple(any()) :: any()
...>   def call_simple(_x), do: simple()
...> 
...>   @spec call_simple_constrain_to_nil(any()) :: nil
...>   def call_simple_constrain_to_nil(_x), do: simple()
...> 
...>   @spec call_simple_through_identity(any()) :: nil
...>   def call_simple_through_identity(_x), do: simple() |> identity()
...> end
{:module, TypedTest_Typed_MultiFunc0,
 <<70, 79, 82, 49, 0, 0, 7, 232, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 1, 167,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>,
 {:call_simple_through_identity, 1}}
iex> TypedTest_Typed_MultiFunc0.call_simple_through_identity(42)
nil
iex> TypedTest_Typed_MultiFunc0.call_simple(42)
nil
iex> TypedTest_Typed_MultiFunc0.identity(42)
42
iex> # call_simple_through_identity is properly typed with a return of nil as you notice
nil
11 Likes