技術メモ

プログラミングとか電子工作とか

Elixir入門(第十五章 Structs)

f:id:ysmn_deus:20190122112104p:plain

どうも、靖宗です。
今回はStructsということで構造体?C言語ではないしたぶんもっと違うなにか・・・

まずは、mapのおさらい。

iex> map = %{a: 1, b: 2}
%{a: 1, b: 2}
iex> map[:a]
1
iex> %{map | a: 3}
%{a: 3, b: 2}

mapの作成、参照、更新ですね。
Structsはコンパイル時にチェックとデフォルトの値をつけるmapの拡張と書かれてますがどういうことなんでしょうか。
とりあえず次へ。

Defining structs(構造体の定義?)

Structの定義はdefstructを用いて行うそうです。

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

やっぱり構造体って認識で問題無い?
構造体の名前はdefmoduleで宣言し、内部の構造はdefstructで作って行くみたいです。
この構造体の定義から構造体を作成するには下記の通りにするようです。

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

イメージ的にはクラスからインスタンスを作成するオブジェクト指向的なところを感じますが関数型言語でそのイメージを持つのは危険なのかもしれないです。
一応コンパイル時に内部のフィールドに変数名?キー名?があるかないかはチェックしてくれるようです。

iex> %User{oops: :field}
** (KeyError) key :oops not found in: %User{age: 27, name: "John"}

厳密に型を定義してるわけではないのでおまけみたいに考えておけば良いのかな。
とりあえず次へ。

Accessing and updating structs(構造体の値の取得と更新)

主にmapと同じ様に扱えるようです。

iex> john = %User{}
%User{age: 27, name: "John"}
iex> john.name
"John"
iex> jane = %{john | name: "Jane"}
%User{age: 27, name: "Jane"}
iex> %{jane | oops: :field}
** (KeyError) key :oops not found in: %User{age: 27, name: "Jane"}

作成時に%{}の間に構造体名があるかないかぐらいの違いでしょうか。
上記の例ではmap更新の記法|を使用してますが、構造体の場合は同じキー名で値が同じなら(今回はnameが違うのでコレを除く)、他の構造はメモリ上では共有されるようです。
ここは関数型言語ならではの仕様でしょうか?一般的なオブジェクト指向ならとりあえずインスタンス化してメモリを分けておかないと勝手に更新しちゃいそうですが、基本的に変数の値を変えに行く操作がないので安全なのでしょう。
構造体はパターンマッチも使えるそうです。

iex> %User{name: name} = john
%User{age: 27, name: "John"}
iex> name
"John"
iex> %User{} = %{}
** (MatchError) no match of right hand side value: %{}

いろんなところでパターンマッチでてくるなぁ。
高速かつシンプルに書くにはパターンマッチを如何に駆使するかが肝になってきそうです。

Structs are bare maps underneath

構造体は根本的にはフィールドが決まっているmapであるようです。

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

なので、is_map/1で確認してみてもtrueが返ってきてます。
また、構造体には__struct__という特殊なキーが登録されているようです。おそらくElixirはこれで構造体であるか否かを判断しているんじゃないでしょうか。
(ということは後発的な機能?)
また、bare mapsと書かれている由来が、mapのようにアトムでアクセスできないようです。

iex> john = %User{}
%User{age: 27, name: "John"}
iex> john[:name]
** (UndefinedFunctionError) function User.fetch/2 is undefined (User does not implement the Access behaviour)
             User.fetch(%User{age: 27, name: "John"}, :name)
iex> Enum.each john, fn({field, value}) -> IO.puts(value) end
** (Protocol.UndefinedError) protocol Enumerable not implemented for %User{age: 27, name: "John"}

うーん、この辺は内部構造を理解しないとなんとも言えないような・・・
おそらくコードを書いていく上でそこまで影響無さそうなのでとりあえず放置!

Mapモジュールは使用できるようです。

iex> jane = Map.put(%User{}, :name, "Jane")
%User{age: 27, name: "Jane"}
iex> Map.merge(jane, %User{name: "John"})
%User{age: 27, name: "John"}
iex> Map.keys(jane)
[:__struct__, :age, :name]

とりあえずMapモジュールでやりとりすることが多いんじゃないでしょうか?
ちょいちょい説明文にprotocolsって出てきてますが、次のチャプターなので気にしないで進みます。

Default values and required keys(デフォルト値と必須キー)

構造体を定義するときにデフォルトの値を入力しないとnilが代入されるようです。

iex> defmodule Product do
...>   defstruct [:name]
...> end
iex> %Product{}
%Product{name: nil}

一応違う記法でも試す。

iex(10)> defmodule Product do
...(10)>   defstruct name: nil
...(10)> end
{:module, Product,
 <<70, 79, 82, 49, 0, 0, 5, 216, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 184,
   0, 0, 0, 18, 14, 69, 108, 105, 120, 105, 114, 46, 80, 114, 111, 100, 117, 99,
   116, 8, 95, 95, 105, 110, 102, 111, 95, ...>>, %Product{name: nil}}
iex(11)> %Product{}
%Product{name: nil}

デフォルト値を定義には書かないけど作成時に必須にするには、前回のModule attributesを利用するようです。

iex> defmodule Car do
...>   @enforce_keys [:make]
...>   defstruct [:model, :make]
...> end
iex> %Car{}
** (ArgumentError) the following keys must also be given when building struct Car: [:make]
    expanding struct: Car.__struct__/1

全体的にちゃんと説明しようとするとこういう構成(Getting-started)になるんでしょうけど、サクッと始めて後から掘り下げる方がいい気がしてきました(今更)。
とはいえあともう少しなのでこのまま進めます。