やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

【Elixirで実装】関数型言語でカプセル化をする方法

この記事の概要📖

  • 関数型言語の多くでは値は束縛されるため、不変である
  • 値を再束縛(≒再代入)することが出来ないため、値を更新する際には新たなデータを作る必要がある
  • 更新の度に新たなデータを作ることで、関数型言語でもカプセル化をすることが可能になる

関数型言語では値は不変⏳

多くの関数型言語では値は不変であり、一度、宣言した変数に束縛されている値を変更することが出来ません。

printName :: IO ()
printName = print name
  where name = "okb"
        name = "okb2"

コンパイルしてみると...

*Main> :l sample.hs
[1 of 1] Compiling Main             ( sample.hs, interpreted )

sample.hs:5:9: error:
    Conflicting definitions for ‘name’
    Bound at: sample.hs:5:9-12
              sample.hs:6:9-12
  |
5 |   where name = "okb"
  |         ^^^^^^^^^^^^...
Failed, no modules loaded.

Conflicting definitions for ‘name’(nameという定義が衝突している)というエラーが出て、コンパイルに失敗してしまいます。
この制限はリストやオブジェクト、マップであっても同じです。例えばリストのN番目だけを更新するということは基本的には出来ず、N番目の要素が変化した新たなリストを作り直す必要があります。

しかしRubyであれば同じ処理のコードが問題なく実行されます。Rubyは再代入(再束縛)を許可しているからです。 他の再代入を許可している言語でも同じ結果が得られます。

name = "okb"
name = "okb2"
print name # okb2

リストの更新

lst = [1,2,3]
lst[0] = 99
print lst # [99, 2, 3]

オブジェクト指向言語でのカプセル化💊

まずオブジェクト指向言語でのカプセル化から見ていきます。
オブジェクト指向言語でのカプセル化はオブジェクトから作成したインスタンスが内部に持つStateを更新するため、1つのインスタンスを利用し続けるという流れになります。

class Person
  def initialize(name)
    @name = name
  end
  
  # getter
  def get_name
    @name
  end
  
  # setter
  def set_name(new_name)
    @name = new_name
  end
end

okb = Person.new("okb")
p "init: #{okb.get_name}" # "init: okb"
okb.set_name("okb2")
p "first updated: #{okb.get_name}" # "first updated: okb2"

okb.set_name("okb3")
p "second updated: #{okb.get_name}" # "second updated: okb3"

しかしながら、関数型言語では値の再束縛が出来ないので、この方法は使用することが出来ません。

関数型言語でのカプセル化💊

関数型言語では、制限を回避してカプセル化を行うために値を更新する度に新たなデータを作成する必要があります。
つまり、先ほどのRubyの例をあげてコードを書いてみると、こんな感じになります。

class PersonMethods
  class << self
    def new_person(name)
      Person.new(name)
    end
    
    def get_name(person)
      person.get_name
    end
    
    def set_name(name)
      Person.new(name)
    end
  end
end

okb = PersonMethods.new_person("okb")
p "first: #{PersonMethods.get_name(okb)}" # "first: okb"

okb2 = PersonMethods.set_name("okb2")
p "second: #{PersonMethods.get_name(okb2)}" # "second: okb2"

okb3 = PersonMethods.set_name("okb3")
p "third: #{PersonMethods.get_name(okb3)}" # "third: okb3"

先程のコードと大きく変わっているのは、Personクラスの関数を呼び出すラッパークラスを作成したことと、set_nameの処理がPersonクラスのset_nameを呼び出すのではなく、Person.newとして新たなインスタンスを作成して返しているということです。

つまり更新する度に新たなデータを作成しているということになります。この動きであれば関数型言語でもカプセル化を実装することが出来ます。

Elixirでの実装🧪

さっそくElixirで実装してみます。少し実践的な内容にするために、httpリクエストを行うクライアントをカプセル化を使用して作成してみます。(※実際にリクエストは行われません)

このhttpクライアントは以下の3つの値を保持します。

  • ホスト(host)
  • メソッド(method)
  • エンドポイント(end_point)

3つの値の保持にはElixirの構造体を使用します。
構造体は

  • モジュールの中でしか定義できない
  • フィールドを定義することが出来る

ため、構造体を使うことで厳密なカプセル化を行うことが出来ます。

初期値は適当です。

defmodule HttpRequest do
  defstruct host: "https://example", method: "GET", end_point: "/"
end

次にhost, method, end_pointの3つを更新するための関数(setter)を定義します。合わせて、構造体を初期化する処理が共通だったのでinitという関数を定義しています。

defmodule HttpRequest do
  defstruct host: "https://example", method: "GET", end_point: "/"
  def init(host, method, end_point),do: %HttpRequest{ host: host, method: method, end_point: end_point }
  def set_host(%HttpRequest{ method: method, end_point: end_point }, new_host), do: init(new_host, method, end_point)
  def set_method(%HttpRequest{ host: host, end_point: end_point }, new_method), do: init(host, new_method, end_point)
  def set_end_point(%HttpRequest{ host: host, method: method }, new_end_point), do: init(host, method, new_end_point)
end

新たな構造体を作成する時の動きをset_hostを例にして考えてみます。
set_hostは引数にすでに作成されたHttpRequestの構造体と新たに設定したいhostの値を受け取ります。

受け取った構造体からhost以外のmethodend_pointの値を取得して、第2引数で受け取った新たなhostの3つの値を用いて、HttpRequestの構造体を新たに作成します。

# 第1引数で構造体を受け取り、methodとend_pointを取得
# 第2引数で新たなhostを受け取る
set_host(%HttpRequest{ method: method, end_point: end_point }, new_host)
# 3つの値を用いて構造体を新たに作成
init(new_host, method, end_point)

では、動作確認をしてみます。動作確認のために構造体の内部を出力するstate_infoというデバッグ関数を定義しました。

def state_info(struct) do
  """
  *** HttpRequest Request Infomations
    - Host: #{struct.host}
    - Method: #{struct.method}
    - end_point: #{struct.end_point}}
  """ |> IO.puts()

  struct
end

結果を見ていきます。
全ての関数がHttpRequestの構造体を返すので、パイプライン演算子で気持ちよく動作させることが出来ます🙌

HttpRequest.init("https://example.com", "GET", "/fake")
|> HttpRequest.state_info()
|> HttpRequest.set_host("https://dog.example.com")
|> HttpRequest.state_info()
|> HttpRequest.set_method("POST")
|> HttpRequest.state_info()
|> HttpRequest.set_end_point("/bark")
|> HttpRequest.state_info()
*** HttpRequest Request Infomations
  - Host: https://example.com
  - Method: GET
  - end_point: /fake}

*** HttpRequest Request Infomations
  - Host: https://dog.example.com
  - Method: GET
  - end_point: /fake}

*** HttpRequest Request Infomations
  - Host: https://dog.example.com
  - Method: POST
  - end_point: /fake}

*** HttpRequest Request Infomations
  - Host: https://dog.example.com
  - Method: POST
  - end_point: /bark}

hosthttps://dog.example.comに、methodPOSTに、end_point/barkに変わっているのが確認できます。更新した以外のフィールドの値は変化しないまま、保持されています。必要なフィールドのみを更新関数(setter)から更新出来ました🎉

全体のコード

replit.com

参考文献📚