Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
TypeBully is an (incomplete) toy module to force Elixir functions to respect their typespecs.
#
# TypeBully forces functions to respect their typespecs by rewriting the function clauses to include proper guards.
#
# For example, the Wimp module below: cry/3 has typespecs, but no enforcing guards, normally, calling cry/3 would
# always match the first function clause, but TypeBully forces it to select the correct one based on its typespec.
#
#
# Without TypeBully:
#
# iex(1)> Wimp.cry("whaa")
# "first"
# iex(2)> Wimp.cry(1)
# "first"
#
# With TypeBully:
#
# iex(1)> Wimp.cry("whaa")
# "first"
# iex(2)> Wimp.cry(1)
# "second"
#
defmodule TypeBully do
defmacro __using__(_env) do
quote do
Module.register_attribute(__MODULE__, :bodies, accumulate: true)
@on_definition unquote(__MODULE__)
@before_compile unquote(__MODULE__)
end
end
defmacro __before_compile__(env) do
functions = env.module
|> Module.get_attribute(:bodies)
|> Enum.map(fn {kind, name, args, _guards, body, spec} ->
guards =
Enum.zip(args, spec)
|> Enum.filter(fn {_, type} -> type != :any end)
|> Enum.map(fn {arg, type} ->
case type do
"string" -> "is_binary(#{arg})"
t -> "is_#{t}(#{arg})"
end
end)
|> Enum.join(" and ")
~s"""
#{kind} #{name}(#{Enum.join(args, ",")}) when #{guards} do
#{Macro.to_string body}
end
"""
|> Code.string_to_quoted!
end)
quote do
@bodies
|> Enum.map(fn {_kind, name, args, _guards, _body, _spec} ->
{name, length(args)}
end)
|> Enum.uniq
|> defoverridable
unquote_splicing(functions)
end
end
def __on_definition__(env, kind, name, args, guards, body) do
[{:spec, {:::, _, [{_, _, spec}, _]}, _} | _rest] = Module.get_attribute(env.module, :spec)
spec = Enum.map(spec, fn s ->
case s do
{_, _, [type]} -> type |> Atom.to_string |> String.downcase
{type, _, nil} -> type
end
end)
arg_names = Enum.map(args, fn {a, _, _} -> a end)
Module.put_attribute(env.module, :bodies, {kind, name, arg_names, guards, body, spec})
end
end
defmodule Wimp do
use TypeBully
@spec cry(String) :: String
def cry(a) do
"first"
end
@spec cry(Integer) :: String
def cry(a) do
"second"
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment