Skip to content

Commit

Permalink
add experimental fasten hook (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
artemeff authored Jan 14, 2025
1 parent ac3340e commit f32d9ce
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 4 deletions.
27 changes: 27 additions & 0 deletions bench/struct_bench.exs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,29 @@ defmodule Example do
field :b, :float
field :c, {:map, :integer}
field :d, Embedded
field :e, {:map, :integer}, default: %{}
end
end

defmodule EmbeddedFast do
use Construct
use Construct.Hooks.Fasten

structure do
field :e
end
end

defmodule ExampleFast do
use Construct
use Construct.Hooks.Fasten

structure do
field :a
field :b, :float
field :c, {:map, :integer}
field :d, EmbeddedFast
field :e, {:map, :integer}, default: %{}
end
end

Expand All @@ -22,6 +45,10 @@ Benchee.run(
"make" => fn ->
{:ok, _} = Example.make(%{a: "test", b: 1.42, c: %{a: 0, b: 42}, d: %{e: "embeds"}})
end,

"make fasten hook" => fn ->
{:ok, _} = ExampleFast.make(%{a: "test", b: 1.42, c: %{a: 0, b: 42}, d: %{e: "embeds"}})
end,
},
time: 3,
memory_time: 3,
Expand Down
2 changes: 1 addition & 1 deletion lib/construct/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule Construct.Compiler do
alias Construct.Compiler.AST

@registry Construct.Registry
@no_default :__construct_no_default__
@no_default :__construct_no_default_value__

def construct_module?(module) do
registered_type?(module) || ensure_compiled?(module) && function_exported?(module, :__construct__, 1)
Expand Down
126 changes: 126 additions & 0 deletions lib/construct/hooks/fasten.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
defmodule Construct.Hooks.Fasten do
defmacro __using__(_opts \\ []) do
quote do
structure_compile_hook :post do
Module.eval_quoted(__MODULE__, Construct.Hooks.Fasten.__compile__(__MODULE__, Enum.reverse(@fields)))

defoverridable make: 2
end
end
end

def __compile__(module, fields) do
cast_defs =
Enum.map(fields, fn({name, type, opts}) ->
function_name = :"__cast_#{name}__"

{default_before_clause, default_after_clause} =
case Keyword.get(opts, :default) do
:__construct_no_default_value__ ->
clause_after =
quote do
defp unquote(function_name)(_, _) do
{:error, %{unquote(name) => :missing}}
end
end

{[], clause_after}

nil ->
clause_after =
quote do
defp unquote(function_name)(_, _) do
{:error, %{unquote(name) => :missing}}
end
end

{[], clause_after}

term ->
term = Macro.escape(term)

clause_before =
quote do
defp unquote(function_name)(%{unquote(to_string(name)) => term}, _opts) when term == unquote(term) do
{:ok, unquote(term)}
end

defp unquote(function_name)(%{unquote(name) => term}, _opts) when term == unquote(term) do
{:ok, unquote(term)}
end
end

clause_after =
quote do
defp unquote(function_name)(_, _) do
{:ok, unquote(term)}
end
end

{clause_before, clause_after}
end

cast_clause =
quote do
defp unquote(function_name)(%{unquote(to_string(name)) => term}, opts) do
Construct.Type.cast(unquote(type), term, opts)
end

defp unquote(function_name)(%{unquote(name) => term}, opts) do
Construct.Type.cast(unquote(type), term, opts)
end
end

default_before_clause |> merge_blocks(cast_clause) |> merge_blocks(default_after_clause)
end)

cast_defs =
Enum.reduce(cast_defs, {:__block__, [], []}, fn(ast, acc) ->
merge_blocks(acc, ast)
end)

with_body =
Enum.map(fields, fn({name, _type, _opts}) ->
{name, Macro.var(name, nil)}
end)

with_body =
quote do
{:ok, struct(unquote(module), unquote(with_body))}
end

with_matches =
Enum.map(fields, fn({name, _type, _opts}) ->
quote do
{:ok, unquote(Macro.var(name, nil))} <- unquote(:"__cast_#{name}__")(params, opts)
end
end)

with_ast = {:with, [], with_matches ++ [[do: with_body]]}

make_ast =
quote do
def make(params, opts) do
unquote(with_ast)
end
end

merge_blocks(make_ast, cast_defs)
end

defp merge_blocks(a, b) do
{:__block__, [], block_content(a) ++ block_content(b)}
end

defp block_content({:__block__, [], content}) do
content
end

defp block_content({_, _, _} = expr) do
[expr]
end

defp block_content([]) do
[]
end
end
4 changes: 1 addition & 3 deletions lib/construct/type.ex
Original file line number Diff line number Diff line change
Expand Up @@ -468,11 +468,9 @@ defmodule Construct.Type do
{:ok, Enum.reverse(acc)}
end

defp map(list, type, fun, acc, opts \\ [])

defp map([{key, value} | t], type, fun, acc, opts) do
case fun.(type, value, opts) do
{:ok, value} -> map(t, type, fun, Map.put(acc, key, value))
{:ok, value} -> map(t, type, fun, Map.put(acc, key, value), opts)
{:error, reason} -> {:error, reason}
:error -> :error
end
Expand Down

0 comments on commit f32d9ce

Please sign in to comment.