Elixir入門 15: 構造体
Tags: elixir, webdev, tutorial, programming
Published: 2018-12-25 01:28:23

---
title: Elixir入門 15: 構造体
published: true
description: Elixir公式サイトの許諾を得て「Structs」の解説にもとづき、加筆補正を加えて、Elixirにおける構造体の定め方と使い方についてご説明します。
tags: #elixir, #webdev, #tutorial, #programming
---

本稿はElixir公式サイトの許諾を得て「[Structs](https://elixir-lang.org/getting-started/structs.html)」の解説にもとづき、加筆補正を加えて、Elixirにおける構造体の定め方と使い方についてご説明します。

# 構造体を定める

構造体は[`defstruct/1`](https://hexdocs.pm/elixir/Kernel.html#defstruct/1)で定めます。引数はキーワードリストです。加えるフィールドとそのデフォルト値を与えてください。

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

構造体にはモジュール名を添え、マップと似た構文でつくります(「[Elixir入門 07: キーワードリストとマップ](https://dev.to/gumi/elixir-07--39hi)」参照)。

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

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

```elixir
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は構造体に定めのないフィールドが含まれないことを確かめます。そのとき確認するフィールドの構造はひとつの参照で、同じ構造体はメモリを共有するのです。

```elixir
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
```

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

```elixir
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`](https://hexdocs.pm/elixir/Task.html#__struct__/0)という特別なフィールドをもっています。

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

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

```elixir
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`モジュールの関数で扱うことができます。

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

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


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

```elixir
defmodule User do
  defstruct [:name, :age]
end
```

```elixir
iex> %User{name: "John"}
%User{age: nil, name: "John"}
```

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

```elixir
defmodule User do
  @enforce_keys [:name]
  defstruct [:name, :age]
end
```

```elixir
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入門もくじ
- [Elixir入門 01: コードを書いて試してみる](https://dev.to/gumi/elixir-01--2585)
- [Elixir入門 02: 型の基本](https://dev.to/gumi/elixir-02--30n1)
- [Elixir入門 03: 演算子の基本](https://dev.to/gumi/elixir-03--33im)
- [Elixir入門 04: パターンマッチング](https://dev.to/gumi/elixir-04--1346)
- [Elixir入門 05: 条件 - case/cond/if](https://dev.to/gumi/elixir-05----casecondif-60o)
- [Elixir入門 06: バイナリと文字列および文字リスト](https://dev.to/gumi/elixir-06--35na)
- [Elixir入門 07: キーワードリストとマップ](https://dev.to/gumi/elixir-07--39hi)
- [Elixir入門 08: モジュールと関数](https://dev.to/gumi/elixir-08--1c4c)
- [Elixir入門 09: 再帰](https://dev.to/gumi/elixir-09--1a0p)
- [Elixir入門 10: EnumとStream](https://dev.to/gumi/elixir-10-enumstream-4fpb)
- [Elixir入門 11: プロセス](https://dev.to/gumi/elixir-11--2mia)
- [Elixir入門 12: 入出力とファイルシステム](https://dev.to/gumi/elixir-12--4og6)
- [Elixir入門 13: aliasとrequireおよびimport](https://dev.to/gumi/elixir-13-aliasrequireimport-55c1)
- [Elixir入門 14: モジュールの属性](https://dev.to/gumi/elixir-14--3511)
- Elixir入門 15: 構造体
- [Elixir入門 16: プロトコル](https://dev.to/gumi/elixir-16--lif)
- [Elixir入門 17: 内包表記](https://dev.to/gumi/elixir-17--5gci)
- [Elixir入門 18: シギル](https://dev.to/gumi/elixir-18--5791)
- [Elixir入門 19: tryとcatchおよびrescue](https://dev.to/gumi/elixir-19-trycatchrescue-50i8)
- [Elixir入門 20: 型の仕様とビヘイビア](https://dev.to/gumi/elixir-20--j50)
- [Elixir入門 21: デバッグ](https://dev.to/gumi/elixir-21--21a1)
- [Elixir入門 22: Erlangライブラリ](https://dev.to/gumi/elixir-22-erlang-2492)
- [Elixir入門 23: つぎのステップ](https://dev.to/gumi/elixir-23--50ik)

##### 番外
- [Elixir入門: Plugを使うには](https://dev.to/gumi/elixir-plug-40lb)