Protocolの@deriveについて調べた
Elixir 1.9がそろそろ出そうなところに1.8の話をするんですけど、1.8からInspect
プロトコルで文字列化される構造体のメンバをキーで指定できるようになりました。
defmodule User do @derive {Inspect, only: [:id, :name, :age]} defstruct [:id, :name, :age, :email, :encrypted_password] end
こうしておくと、意図せずログにメールアドレスとか個人情報やらが出力されることを防げたり、人間が見てもあんま意味ないものを出さないようにできるというわけですね。
で本題は、こういうものを自分で実装するにはどうしたらいいんでしょうか、ということを思ったので書いてみました、というものです。
defstruct
のドキュメントとProtocol.derive/3
のドキュメントに全て書いてあるんですけど、Jason.Encoder
のコードも読んで参考にしました。
Elixirのドキュメント、こっちが知りたいことは大抵書いてあるんですよね。どういうことなんですかね。
練習課題
構造体メンバの値を"*****"
に置き換えるMask
プロトコルを実装します。:only
オプションを付けた場合は、指定したものだけ"*****"
に置き換えます。使われ方の例はこちら。
defmodule User do @derive {Mask, only: [:password]} defstruct [:name, :age, :password] end Mask.mask %User{name: "John", age: 29, password: "secret123"} #=> %User{name: "John", age: 29, password: "*****"}
実装
defprotocol Mask do def mask(struct) end defimpl Mask, for: Any do defmacro __deriving__(module, struct, opts) do all_fields = struct |> Map.drop([:__struct__]) |> Map.keys() mask_target = Keyword.get(opts, :only, all_fields) quote do defimpl Mask, for: unquote(module) do def mask(struct) do Enum.reduce(unquote(mask_target), struct, fn (field, acc) -> Map.put(acc, field, "*****") end) end end end end def mask(_any) do raise Protocol.UndefinedError end end
説明
@derive
はモジュール属性を設定するだけなので、特にこれが何をやってるとかではないです。何かやってるのはdefstruct
の方で、@derive
属性が設定されている場合に、この辺りから諸々呼ばれてMask.__deriving__/3
マクロが展開されます。このマクロの中でdefimpl Mask, for: User
しておいてあげると、Mask.mask(%User{})
できるようになるという寸法です。__deriving__/3
の第3引数には@derive
に書いたキーワードリスト(上の例だと[only: [:password]]
が渡ってくるので、よしなに利用してあげればマスクするべきキーが判定できます。今回はdefimpl
してないもの以外はraise
するようにしたので、例えばnil
なんか投げるとこうなります。
Mask.mask(nil) # ** (Protocol.UndefinedError) protocol Mask not implemented for nil. This protocol is implemented for: User # lib/deriver/protocol.ex:1: Mask.impl_for!/1 # lib/deriver/protocol.ex:2: Mask.mask/1