67
54

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Elixirの虜になったPythonプログラマが、6か月後にたどり着いた、Classを使わないプログラム

Posted at

はじめに

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流の書き方にしてみます。

cart.py
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_test.exs
    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}")

モジュール全体

cart.ex
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歴が長いわけではないので、別の見方などもあるかと思います。ご意見などあれば、コメントお願いします。

67
54
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
67
54

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?