Runtime Types: Deeper Down the Rabbit Hole

When I started writing the note " Types where they were not expected ", it seemed to me that I had mastered to bring Erlang types to runtime and now I can use them in the client code on the elixir. Haha, how naive I was.



Anything suggested by the link will work for explicit place-of-use type definitions like use Foo, var: type(). Unfortunately, this approach is doomed if we want to define types somewhere else: next to it in the code using module attributes, or, there, in the config. For example, to define a structure, we might want to write something like this:



# @fields [foo: 42]
# defstruct @fields

@definition var: atom()
use Foo, @definition


Lighthouse in French Catalonia



The code above is not that it won't handle the type the way we want it to - it won't collect at all because it will @definition var: atom()throw an exception ** (CompileError) undefined function atom/0.



Naive approach



 β€” Β« Β» ( @tsilb, , .) , , , , β€” .



, , __using__/1: , ( field β†’ type()),  β€” , , , {Module, :type, [params]}. ~q||, , , , AST. quote/1 : foo: ~q|atom()|. , , . . , - , , , , . , , - - - , .



. , erlang  β€”  , , .  β€” , , ( ).



, . , , , , , , β€”  . , .



Tyyppi



,  β€” , XY . , , β€”   β€” . Tyyppi.



Code.Typespec, . : . , , , . , , .  β€”  Tyyppi.of?/2, ,  β€” «»/«» , .



iex|tyyppi|1  Tyyppi.of? GenServer.on_start(), {:ok, self()}
#β‡’ true
iex|tyyppi|2  Tyyppi.of? GenServer.on_start(), :ok
#β‡’ false


- , Tyyppi.T. Tyyppi.of?/2 - β€” Tyyppi.of_type?/2.



iex|tyyppi|3  type = Tyyppi.parse(GenServer.on_start)
iex|tyyppi|4  Tyyppi.of_type? type, {:ok, self()}
#β‡’ true


, , , , , . :erlang.term_to_binary/1, Config.Provider.





, : . , . , key: type(). Access, upserts. , Ecto.Changeset cast_field/1 validate/1.



, , , , ( , ).



defmodule MyStruct do
  import Kernel, except: [defstruct: 1]
  import Tyyppi.Struct, only: [defstruct: 1]

  @typedoc "The user type defined before `defstruct/1` declaration"
  @type my_type :: :ok | {:error, term()}

  @defaults foo: :default,
            bar: :erlang.list_to_pid('<0.0.0>'),
            baz: {:error, :reason}
  defstruct foo: atom(), bar: GenServer.on_start(), baz: my_type()

  def cast_foo(atom) when is_atom(atom), do: atom
  def cast_foo(binary) when is_binary(binary),
    do: String.to_atom(binary)

  def validate(%{foo: :default} = my_struct), do: {:ok, my_struct}
  def validate(%{foo: foo} = my_struct), do: {:error, {:foo, foo}
end


I have no idea what the practical value of this library is in production (lie, I know: none), but it can certainly be a great helper during development, allowing you to narrow your search and isolate strange errors associated with the dynamic nature of types in Elixir especially when dealing with external sources.



All the library code is available, as always, on the github .






Happy runtime typing!




All Articles