構造体は、マップを基盤にした拡張で、コンパイル時のチェックとデフォルト値を提供します。構造体の名前は、それが定義されているモジュール名になります。構造体を定義するには、defstructを使います。この構文は通常、モジュール定義のすぐ後に書きます。defstructには、アトムのリスト(デフォルト値がnilの場合)またはキーワードリスト(デフォルト値を指定する場合)を渡します。デフォルト値のないフィールドは、デフォルト値のあるフィールドより前に置く必要があります。
defmodule Plane do
defstruct [:engine, wings: 2]
end
plane = %Plane{}
# => %Plane{engine: nil, wings: 2}
構造体はマップを基盤にしているので、ほとんどのマップ関数を使って値の取得や操作ができます。構造体には、_Access Behaviour_は実装されていません。構造体のフィールドにアクセスするには、代わりに_静的アクセス演算子_の.を使うことをおすすめします。
フィールド値の取得
plane = %Plane{}
plane.engine
# => nil
Map.fetch(plane, :wings)
# => {:ok, 2}
フィールド値の更新
plane = %Plane{}
%{plane | wings: 4}
# => %Plane{engine: nil, wings: 4}
構造体は、構造体名を付けてでも、付けなくても、パターンマッチングに使えます。
plane = %Plane{}
%Plane{wings: wings} = plane
%{wings: wings} = plane
パターンに構造体名を含めることで、左辺と右辺がどちらも同じ型の構造体であることを保証できます。
defmodule Helicopter do
defstruct [:engine, rotors: 1]
end
%Plane{} = %Helicopter{}
# => (MatchError) no match of right hand side value: %Helicopter{engine: nil, rotors: 1}
@enforce_keysモジュール属性にフィールドキーのリストを指定すると、構造体の作成時に値が初期化されていることを保証できます。キーがリストに含まれていない場合、その値は上の例で見たようにnilになります。強制されたキーが初期化されていない場合は、エラーが発生します。
defmodule User do
@enforce_keys [:username]
defstruct [:username]
end
%User{}
# => (ArgumentError) the following keys must also be given when building struct User: [:username]
この演習では、ラジコンカーを動かして遊びます。このラジコンカーは、お金を貯めてやっと買えるようになったものです。
車のバッテリーは、最初は満充電(100%)です。リモコンで車を走らせるたびに、20メートル進み、バッテリーを1パーセント消費します。車のニックネームは、車が作成されるまでわかりません。
このラジコンカーには、2つの情報を表示するおしゃれなLEDディスプレイが付いています。
"<METERS> meters"のように表示されます。"Battery at <PERCENTAGE>%"のように表示されます。バッテリーが0%になると、車はもう走らせられなくなり、バッテリー表示は"Battery empty"になります。
新品のラジコンカーの構造体を返すRemoteControlCar.new/0関数を実装しましょう。
RemoteControlCar.new()
# => %RemoteControlCar{
# battery_percentage: 100,
# distance_driven_in_meters: 0,
# nickname: "none"
# }
ニックネームは構造体で必須の項目です。値はnew関数の中で初期化し、構造体では初期化しないようにしてください。
指定したニックネームを持つ新品のラジコンカーの構造体を返すRemoteControlCar.new/1関数を実装しましょう。
RemoteControlCar.new("Blue")
# => %RemoteControlCar{
# battery_percentage: 100,
# distance_driven_in_meters: 0,
# nickname: "Blue"
# }
LEDディスプレイに表示されるのと同じ走行距離を返すRemoteControlCar.display_distance/1関数を実装しましょう。
car = RemoteControlCar.new()
RemoteControlCar.display_distance(car)
# => "0 meters"
この関数は、引数としてRemoteControlCar構造体のみを受け取るようにしてください。
LEDディスプレイに表示されるのと同じバッテリー残量を返すRemoteControlCar.display_battery/1関数を実装しましょう。
car = RemoteControlCar.new()
RemoteControlCar.display_battery(car)
# => "Battery at 100%"
この関数は、引数としてRemoteControlCar構造体のみを受け取るようにしてください。バッテリーが0%になると、バッテリー表示は"Battery empty"になります。
次の処理を行うRemoteControlCar.drive/1関数を実装しましょう。
RemoteControlCar.new("Red")
|> RemoteControlCar.drive()
# => %RemoteControlCar{
# battery_percentage: 99,
# distance_driven_in_meters: 20,
# nickname: "Red"
# }
この関数は、引数としてRemoteControlCar構造体のみを受け取るようにしてください。
バッテリーが切れている(0%)ときは、走行距離を増やさず、バッテリー残量も減らさないようにRemoteControlCar.drive/1関数を更新しましょう。
%RemoteControlCar{
battery_percentage: 0,
distance_driven_in_meters: 1980,
nickname: "Red"
}
|> RemoteControlCar.drive()
# => %RemoteControlCar{
# battery_percentage: 0,
# distance_driven_in_meters: 1980,
# nickname: "Red"
# }