はじめに
Elixirが、Qiitaアドベントカレンダー2022プログラミング言語ランキングで断トツのトップなのを知って、Elixirを学び始めたという方も多いかとおもいます。
私も昨年学び始めました。Elixirはとても楽しい言語です。
どこが楽しいのか?
introductionに書いてある説明をみたり、パイプ演算子や、Enumの使い方を理解し、Elixirのデータの処理をプログラムで記述する術に触れてみて、この半年間で、Elixirの「虜」になってきました。
しかし、私がいままで使ってきた、Python,JavaScript等にあった、Classがありません。
虜になったからといって、Classの無い言語でいままでのように、プログラムを作れるだろうか?
今までの、クラスを使ったプログラミングパラダイムを捨てて、Elixirに移行していいのでしょうか?
Elixirには、Classはない
Elixirの特徴の一つがデータ構造がimmutableであることです。このおかげで、副作用がなくなり、テストが行いやすい等のメリットがあります。
これは歓迎なんですが、副作用の温床Classがありません
Classには、いくつかのデータをまとめて扱う事ができ、Class自体が悪者でもありません。
Elixirの場合、Classはありませんが、同じような事は実現できている事がわかりました。
PythonのClassを使ったプログラム
Class Cartを作ってみます。品物を追加するaddと数量の合計を取得するget_total_countを実装しました。
実際の例では、インスタンス変数は複数あり、もう少し複雑ですが、この例では、単純化して、cart_listだけにします。
これをElixir流の書き方にしてみます。
from functools import reduce
class Cart:
def __init__(self):
self.cart_list = []
def add(self, name, count):
self.cart_list.append((name, count))
def get_total_count(self):
return reduce(lambda acc, x: acc + x[1], self.cart_list, 0)
cart = Cart()
cart.add("apple", 2)
cart.add("banana", 3)
total_count = cart.get_total_count()
print(f"total_count={total_count}") # total_count=5
Elixirの場合
Elixirの場合、データと関数を組み合わせて保存するClassに相当するものはありません。
あえて、対応づけをしてみると
Python | Elixir |
---|---|
class | defmodule |
インスタンス変数(self) | defstruct |
__init__() |
new() 関数名は任意 |
こんな感じかとおもいます。
これに従って、PythonのプログラムをElixirで書き直して見ます。
defmodule/defstruct
defstructは、キーの値があらかじめ固定された、キーワードリスト。キーの値を過不足なく指定できるので、データの構造が確定している場合には便利です。
データはdefstructで作る事は必須ではありません。defstructは、キーが同じでもキーワードマップは受け付けられないので、defstructでガチガチにはせず、キーワードリストで作る場合もあるとおもいます。
defmodule Cart do
defstruct cart_list: []
new()
cartの初期状態を作るための処理を行い、%Cart{}を返します。関数名に決まりはありませんが、new()にしてみました。
今回の例では、単純で関数にするまでもない内容ですが、__init__()
で行うような初期化したデーターを作成する処理になります。
def new() do
struct!(Cart)
end
add()
addの第一引数(Pythonの場合、selfになっているところ)をcart状態を受け取る引数とします。
%Cart{}
のパターンマッチングを使用して記述をしておけば、第一引数の間違った呼び出しを防げます。
注意点は、変更後のcartを返す事。
addは、struct!()
によって更新された、%Cart{}
を返します。
Pythonプログラムの場合のインスタンス変数の値を変更する処理の場合は、defstructのデータを返す処理になります。
def add(%Cart{cart_list: cart_list} = cart, name, count) do
struct!(cart, cart_list: cart_list ++ [{name, count}])
end
get_total_count()
get_total_count()'もaddと同様selfにあたる部分には、cart状態渡します。
get_total_count()'は、cartの状態は変更しないので、cartの状態を返す必要はありません。結果だけを返します。
def get_total_count(%Cart{cart_list: cart_list}) do
Enum.reduce(cart_list, 0, fn x, acc -> acc + elem(x, 1) end)
end
実行方法
addを実行する都度、cartの値を最新のものに変えていく必要があります。Pythonの場合のように、内部状態を保存はしてくれません。
cart = Cart.new()
cart = Cart.add(cart, "apple", 2)
cart = Cart.add(cart, "banana", 3)
total_count = Cart.get_total_count(cart)
# total_count=5
IO.puts("total_count=#{total_count}")
モジュール全体
defmodule Cart do
defstruct cart_list: []
def new() do
struct!(Cart)
end
def add(%Cart{cart_list: cart_list} = cart, name, count) do
struct!(cart, cart_list: cart_list ++ [{name, count}])
end
def get_total_count(%Cart{cart_list: cart_list}) do
Enum.reduce(cart_list, 0, fn x, acc -> acc + elem(x, 1) end)
end
end
cart = Cart.add(cart, ~~)面倒だな!
Elixirでは、データの書き換えはできず、書き換えた新たなデータを作成する事しかできません。
なので、cart = Cart.add(cart, ~~)
のような書き方をするしかありません。諦めてください。
逆に言えば、cart=の行が、状態を更新している処理で、これ以外では更新していない。と言えます。
どうしても値を保存を任せたい場合
cart = Cart.add(cart, ~~)
のような更新方法では、関数の中の中など、奥で更新した場合、更新後の値を持って戻る必要があって、難しい場合もあります。
このような場合は、Agentなどを使って保存する事ができます。
cartの値をAgentに保存する例
defmodule Cart.Agent do
use Agent
def start_link() do
Agent.start_link(fn -> Cart.new() end, name: __MODULE__)
end
def add(name, count) do
Agent.update(__MODULE__, &Cart.add(&1, name, count))
end
def get_total_count() do
Agent.get(__MODULE__, &Cart.get_total_count(&1))
end
end
使用例
Cart.Agent.start_link()
Cart.Agent.add("apple", 2)
Cart.Agent.add("banana", 3)
total_count = Cart.Agent.get_total_count()
# total_count=5
IO.puts("total_count=#{total_count}")
Classのインスタンス生成=Agentの感覚で、Agentを使ってみたことがあるんですが、Agentを呼び出す処理を毎回入れる必要があって、見づらいです。
それに、構造を見直せば、Agentにしなくても十分対応できる内容だったりします。
特定の関数内で更新、保持すればよい場合は、Agent使わないほうがすっきり書けるとおもいます。
structの扱い方
structの値を変更は、次の記述で同じように行えます。
%SomeStruct{struct | key: :value}
struct(%SomeStruct{}, key: :value)
struct!(%SomeStruct{}, key: :value)
struct関数を使うほうが、関数なのでパイプ演算子でも使えたり、記述がシンプルなのでお気に入りです。
理由がない限り、キーの値が正しいかチェックしてエラーになるstruct!
を使うようにしています。
まとめ
- Pythonのインスタンス変数で保持していた値は、defstructで定義してdefstruct型のデータとして管理する
-
__init__()
にあたる処理が欲しいときは、new()のような関数を使って、defstruct型のデータを返すようにすうる - インスタンスメソットの第一引数(selfにあたる部分)にdefstruct型のデータを渡す
- インスタンス変数の値を変更する関数では、更新後のdefstruct型のデータを戻り値とする
- defstruct型のデータが更新は自分でする
cart = Cart.add(cart, ~~)
のような記述 - defstruct型のデータ保持がどうしても難しい場合は、Agentを検討
- Classがなくても、Classを使って実現していたことができなくなるわけではない(パラダイムシフト)
私自身、Elixir歴が長いわけではないので、別の見方などもあるかと思います。ご意見などあれば、コメントお願いします。