Current section
Files
Jump to
Current section
Files
lib/defconst.ex
# SPDX-FileCopyrightText: 2024 Łukasz Niemier <#@hauleth.dev>
#
# SPDX-License-Identifier: MIT
defmodule Defconstant do
@moduledoc """
Helper functions for defining constant values in your modules.
## Usage
```elixir
defmodule Foo do
import #{inspect(__MODULE__)}
defconst comptime do
# This will be evaluated at compile time
Enum.sum([
0, 2, 3, 4, 5, 6, 8, 12, 30, 32, 33, 34, 35, 36, 38, 40, 42, 43, 44, 45,
46, 48, 50, 52, 53, 54, 55, 56, 58, 60, 62, 63, 64, 65, 66, 68, 80, 82,
83, 84, 85, 86, 88
])
end
defonce runtime do
2 * 1068 + 1
end
end
```
"""
defguardp is_empty_args(value) when is_atom(value) or value == []
@doc """
Defines function that will be evaluated once, *in compile time*, and will
return computation result.
Defined function can only be 0-ary.
Example:
defmodule MyConstants do
import Defconstant
defconst the_answer, do: 42
def my_func do
the_answer() * 2 # returns 84
end
end
"""
defmacro defconst(call, opts), do: do_defconst(:def, call, __CALLER__, opts)
@doc """
Defines private function that will be evaluated in compile time.
For details see `defconst/2`.
"""
defmacro defconstp(call, opts), do: do_defconst(:defp, call, __CALLER__, opts)
defp do_defconst(type, {name, _, args}, _ctx, do: body) when is_empty_args(args) do
fbody =
quote unquote: false do
unquote(result)
end
quote do
result = unquote(body)
unquote(type)(unquote(name)(), do: unquote(fbody))
end
end
defp do_defconst(type, {name, _, args}, %Macro.Env{} = ctx, _) when length(args) > 0 do
raise CompileError,
description:
"`defconst#{if type == :defp, do: "p", else: ""}` can define only 0-ary functions, tried to define #{name}/#{length(args)}-ary.",
line: ctx.line,
file: ctx.file
end
@doc """
Defines function that will be evaluated once, *in runtime*, and will cache the result.
Defined function can only be 0-ary.
"""
defmacro defonce(call, opts), do: do_defonce(:def, call, __CALLER__, opts)
@doc """
Defines private function that will be evaluated once, *in runtime*, and will cache the result.
For details see `defonce/2`.
"""
defmacro defoncep(call, opts), do: do_defonce(:defp, call, __CALLER__, opts)
@max_hash 4_294_967_295
defp do_defonce(type, {name, _, args}, ctx, do: body) when is_atom(args) or args == [] do
body_hash = :erlang.phash2(body, @max_hash)
if Atom.to_string(name) =~ ~r/[!?]$/ do
raise CompileError,
description:
"`defonce#{if type == :defp, do: "p", else: ""}` cannot end with `!` nor `?` as `#{name}!/0` is used to force recompilation",
line: ctx.line,
file: ctx.file
end
quote do
unquote(type)(unquote(name)()) do
case :persistent_term.get({__MODULE__, unquote(name)}, :__no_val__) do
{unquote(body_hash), value} ->
value
_ ->
unquote(:"#{name}!")()
end
end
@doc unquote("""
Force recomputation of `#{name}/0`
""")
unquote(type)(unquote(:"#{name}!")()) do
result = unquote(body)
:persistent_term.put({__MODULE__, unquote(name)}, {unquote(body_hash), result})
result
end
end
end
defp do_defonce(type, {name, _, args}, ctx, _) when length(args) > 0 do
raise CompileError,
description:
"`defonce#{if type == :defp, do: "p", else: ""}` can define only 0-ary functions, tried to define #{name}/#{length(args)}-ary.",
line: ctx.line,
file: ctx.file
end
end