如何为新类型定义宏?

How to define Macro for a new Type?

背景

所以,我正在研究一个名为“NewType”的概念,我从 F# 和 Scala 等语言中汲取灵感。

我的 objective,主要是为了学习目的,是构建一个宏,使创建这种抽象只需要一行代码。

预期用途

我想创建一个允许我做这样的事情的宏:

defmodule User do
  require NewType # an absolutely original name for the macro :D

  deftype Name, String.t() # Usage of said macro. Here I am defining a new type called "Name"

  @enforce_keys [:name, :age]
  defstruct [:name, :age]
  @type t :: %__MODULE__{
          name: Name.t,
          age: integer()
        }

  @spec new(Name.t, integer) :: User.t
  def new(name, age), do: %User{name: name, age, age}  
end

现在,我可以通过以下方式创建 User:

defmodule Test do
  alias User
  import User.Name

  @spec run :: User.t
  def run do
    name = Name("John")
    User.new(name, 25)
  end
end

如何实现这个接口?

这个界面可能会让您想起 Record 界面。那是因为我认为它 API 有一些我想探索的好主意。

因此,作为起点,我尝试阅读 Record 的源代码,但我并不能真正将其提取出来并使用它来为我的用例创建一个实现,主要是因为我不 need/want 与 Erlang 记录交互。

因此,一种实现可能性是,在引擎盖下将其转换为元组:

defmodule NewType do
  defmacro new(name, val) do
    quote do
      NewType.to_tuple(unquote(name), unquote(val))
    end
  end

  def to_tuple(name, val), do: {String.to_atom(name), val}
end

然而,这与我要创建的界面相去甚远...

问题

  1. 使用 Elixir 宏,是否可以创建我想要的 API?
  2. 如何更改我的代码以实现类似 Name("John") 的功能?

我的回答

在阅读了更多关于 Elixir 中的宏、与社区交谈并阅读了 NewType 之后,我完善了我的想法。虽然无法完全实现我最初的想法,但通过一些更改,您仍然可以获得 NewType.

的核心优势

原始想法的变化

  • 没有使用 Name("John") 语法。如 Elixir 中的 this post this syntax is not valid 所述。
  • 没有defguard。因为类型是 @opaque ,所以不可能有一个守卫来分析数据的内部结构而不会引起透析器的抱怨。由于这里的主要目标是让 Dialyzer 帮助我检测问题,并且由于不透明数据的内部结构只能由属于模块本身的函数来分析,这意味着这个想法是不可能的。
  • 调用时未验证数据类型new。最初我想有一些验证机制,但这不是必需的,因为如果用户使用不正确的参数调用 new,dialyzer 将让用户知道。
  • 没有 self-generated 功能。我没有选择 Age.age?Name.name?,而是选择了更通用的 NewType.is_type?/2,它将完成相同的工作并且更通用。

代码

考虑到这些变化,这就是我想出的宏:

defmodule NewType do
  defmacro deftype(name, type) do
    quote do
      defmodule unquote(name) do
        @opaque t :: {unquote(name), unquote(type)}

        @spec new(value :: unquote(type)) :: t
        def new(value), do: {unquote(name), value}

        @spec extract(new_type :: t) :: unquote(type)
        def extract({unquote(name), value}), do: value
      end
    end
  end

  @spec is_type?(data :: {atom, any}, new_type :: atom) :: boolean
  def is_type?({type, _data}, new_type) when type == new_type, do: true
  def is_type?(_data, _new_type), do: false
end

可以这样使用:

type.ex:

defmodule Type do
  import NewType

  deftype Name, String.t()
end

test.ex:

defmodule Test do
  alias Type.Name

  @spec print(Name.t()) :: binary
  def print(name), do: Name.extract(name)

  def run do
    arg = 1
    name = Name.new(arg) # dialyzer detects error !
    {:ok, name}
  end
end