DEV Community

gumi TECH for gumi TECH Blog

Posted on • Updated on

Elixir入門 15: 構造体

本稿はElixir公式サイトの許諾を得て「Structs」の解説にもとづき、加筆補正を加えて、Elixirにおける構造体の定め方と使い方についてご説明します。

構造体を定める

構造体はdefstruct/1で定めます。引数はキーワードリストです。加えるフィールドとそのデフォルト値を与えてください。

defmodule User do
  defstruct name: "John", age: 27
end
Enter fullscreen mode Exit fullscreen mode

構造体にはモジュール名を添え、マップと似た構文でつくります(「Elixir入門 07: キーワードリストとマップ」参照)。

iex> %User{}
%User{age: 27, name: "John"}
iex> %User{name: "Meg"}
%User{age: 27, name: "Meg"}
Enter fullscreen mode Exit fullscreen mode

構造体に定められたフィールドは、それらだけがしかもすべて揃っていることをコンパイル時に保証されます。

iex> %User{age: 20}
%User{age: 20, name: "John"}
iex> %User{name: 20}
%User{age: 27, name: 20}
iex> %User{oops: :field}
** (KeyError) key :oops not found in: %User{age: 27, name: "John"}
    (stdlib) :maps.update(:oops, :field, %User{age: 27, name: "John"})
    example.exs: anonymous fn/2 in User.__struct__/1
    (elixir) lib/enum.ex:1899: Enum."-reduce/3-lists^foldl/2-0-"/3
Enter fullscreen mode Exit fullscreen mode

構造体を参照して更新する

構造体のフィールドを参照したり、値を書き替えるときは、マップと同じ扱いができます。更新の構文(|)を用いると、VMは構造体に定めのないフィールドが含まれないことを確かめます。そのとき確認するフィールドの構造はひとつの参照で、同じ構造体はメモリを共有するのです。

iex> john = %User{}
%User{age: 27, name: "John"}
iex> john.name
"John"
iex> meg = %{john | name: "Meg"}
%User{age: 27, name: "Meg"}
iex> %{meg | oops: :field}
** (KeyError) key :oops not found in: %User{age: 27, name: "Meg"}
    (stdlib) :maps.update(:oops, :field, %User{age: 27, name: "Meg"})
    (stdlib) erl_eval.erl:255: anonymous fn/2 in :erl_eval.expr/5
    (stdlib) lists.erl:1263: :lists.foldl/3
Enter fullscreen mode Exit fullscreen mode

構造体にはパターンマッチングも使えます。キーが合致するどうかだけでなく、同じ構造体の値かどうかも確かめられるのです。

iex> meg = %{john | name: "Meg"}
%User{age: 27, name: "Meg"}
iex> %User{name: name} = meg
%User{age: 27, name: "Meg"}
iex> name
"Meg"
iex> %User{} = %{age: 27, name: "John"}
** (MatchError) no match of right hand side value: %{age: 27, name: "John"}
Enter fullscreen mode Exit fullscreen mode

構造体は素のマップ

構造体は決められたフィールドをもつ素のマップです。構造体の名前を__struct__/0という特別なフィールドをもっています。

iex> is_map(john)
true
iex> john.__struct__
User
Enter fullscreen mode Exit fullscreen mode

構造体が「素」のマップだというのは、マップの備えるプロトコルは使えないからです。なお、Enum.each/2は列挙可能な値を取り出し、引数の関数に渡して処理します。

iex> john_map = %{age: 27, name: "John"}
%{age: 27, name: "John"}
iex> john_struct = %User{age: 27, name: "John"}
%User{age: 27, name: "John"}
iex> john_map[:name]
"John"
iex> john_struct[:name]
** (UndefinedFunctionError) function User.fetch/2 is undefined (User does not implement the Access behaviour)
    User.fetch(%User{age: 27, name: "John"}, :name)
    (elixir) lib/access.ex:308: Access.get/3
iex> john_map.name
"John"
iex> john_struct.name
"John"
iex> Enum.each(john_map, fn({field, value}) -> IO.puts(value) end)
27
John
:ok
iex> Enum.each(john_struct, fn({field, value}) -> IO.puts(value) end)
** (Protocol.UndefinedError) protocol Enumerable not implemented for %User{age: 27, name:"John"}
    (elixir) lib/enum.ex:1: Enumerable.impl_for!/1
    (elixir) lib/enum.ex:141: Enumerable.reduce/3
    (elixir) lib/enum.ex:1911: Enum.each/2
Enter fullscreen mode Exit fullscreen mode

構造体はMapモジュールの関数で扱うことができます。

iex> kurt = Map.put(%User{}, :name, "Kurt")
%User{age: 27, name: "Kurt"}
iex> takashi = Map.merge(kurt, %User{name: "Takashi"})
%User{age: 27, name: "Takashi"}
iex> Map.keys(takashi)
[:__struct__, :age, :name]
Enter fullscreen mode Exit fullscreen mode
  • Map.put/3: 第1引数のマップに、第2引数のキーで第3引数の値を加えます。
  • Map.merge/2: 第1引数のマップに第2引数のマップのキーと値を加えます。同じキーの値は第2引数で上書きされます。
  • Map.keys/1: マップのキーをすべてリストに納めて返します。

構造体とそのプロトコルは、Elixirの開発者にとって重要な機能である多態性をもたらします。

デフォルト値と必須キー

構造体を定めるとき、キーのデフォルト値は省けます。その場合、値を渡さなければnilが与えられます。

defmodule User do
  defstruct [:name, :age]
end
Enter fullscreen mode Exit fullscreen mode
iex> %User{name: "John"}
%User{age: nil, name: "John"}
Enter fullscreen mode Exit fullscreen mode

さらに、構造体の定めに@enforce_keys属性でキーのリストを指定すると、そのキーの値は必ず与えなければなりません。

defmodule User do
  @enforce_keys [:name]
  defstruct [:name, :age]
end
Enter fullscreen mode Exit fullscreen mode
iex> %User{name: "John"}
%User{age: nil, name: "John"}
iex> %User{age: 27}
** (ArgumentError) the following keys must also be given when building struct User: [:name]
    expanding struct: User.__struct__/1
Enter fullscreen mode Exit fullscreen mode

Elixir入門もくじ

番外

Top comments (0)