loading...
gumi TECH Blog

Elixir入門 15: 構造体

gumitech profile image gumi TECH Updated on ・3 min read

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

構造体を定める

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

defmodule User do
  defstruct name: "John", age: 27
end

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

iex> %User{}
%User{age: 27, name: "John"}
iex> %User{name: "Meg"}
%User{age: 27, name: "Meg"}

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

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

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

構造体のフィールドを参照したり、値を書き替えるときは、マップと同じ扱いができます。更新の構文(|)を用いると、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

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

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"}

構造体は素のマップ

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

iex> is_map(john)
true
iex> john.__struct__
User

構造体が「素」のマップだというのは、マップの備えるプロトコルは使えないからです。なお、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

構造体は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]
  • Map.put/3: 第1引数のマップに、第2引数のキーで第3引数の値を加えます。
  • Map.merge/2: 第1引数のマップに第2引数のマップのキーと値を加えます。同じキーの値は第2引数で上書きされます。
  • Map.keys/1: マップのキーをすべてリストに納めて返します。

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

デフォルト値と必須キー

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

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

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

defmodule User do
  @enforce_keys [:name]
  defstruct [:name, :age]
end
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

Elixir入門もくじ

番外

Posted on by:

gumitech profile

gumi TECH

@gumitech

gumi TECH は、株式会社gumiのエンジニアによる技術記事公開やDrinkupイベントなどの技術者交流を行うアカウントです。 gumi TECH Blog: http://dev.to/gumi / gumi TECH Drinkup: http://gumitech.connpass.com

gumi TECH Blog

株式会社gumiのエンジニアによる技術記事を公開しています。

Discussion

markdown guide