Skip to content

Instantly share code, notes, and snippets.

Embed
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
You can’t perform that action at this time.