From 75b3fd0ca5ef6938b7b46be6deed40b6ac425c04 Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Sat, 19 Nov 2022 12:28:42 +0300 Subject: [PATCH] add experimental fasten hook --- bench/struct_bench.exs | 35 ++++++++++ lib/construct/compiler.ex | 2 +- lib/construct/hooks/fasten.ex | 126 ++++++++++++++++++++++++++++++++++ lib/construct/type.ex | 4 +- 4 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 lib/construct/hooks/fasten.ex diff --git a/bench/struct_bench.exs b/bench/struct_bench.exs index 8dc5346..0b6c795 100644 --- a/bench/struct_bench.exs +++ b/bench/struct_bench.exs @@ -14,14 +14,49 @@ 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 + +# System.halt(0) + +Example.make(%{a: "test", b: 1.42, c: %{a: 0, b: 42}, d: %{e: "embeds"}}) +|> IO.inspect() + +ExampleFast.make(%{a: "test", b: 1.42, c: %{a: 0, b: 42}, d: %{e: "embeds"}}) +|> IO.inspect() + Benchee.run( %{ "make" => fn -> {:ok, _} = Example.make(%{a: "test", b: 1.42, c: %{a: 0, b: 42}, d: %{e: "embeds"}}) end, + + "make optimized" => fn -> + {:ok, _} = ExampleFast.make(%{a: "test", b: 1.42, c: %{a: 0, b: 42}, d: %{e: "embeds"}}) + end, }, time: 3, memory_time: 3, diff --git a/lib/construct/compiler.ex b/lib/construct/compiler.ex index e7c7de2..49570f4 100644 --- a/lib/construct/compiler.ex +++ b/lib/construct/compiler.ex @@ -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) diff --git a/lib/construct/hooks/fasten.ex b/lib/construct/hooks/fasten.ex new file mode 100644 index 0000000..a7941e2 --- /dev/null +++ b/lib/construct/hooks/fasten.ex @@ -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 diff --git a/lib/construct/type.ex b/lib/construct/type.ex index 0705810..503f1fc 100644 --- a/lib/construct/type.ex +++ b/lib/construct/type.ex @@ -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