この記事の概要📖
- 関数型言語の多くでは値は束縛されるため、不変である
- 値を再束縛(≒再代入)することが出来ないため、値を更新する際には新たなデータを作る必要がある
- 更新の度に新たなデータを作ることで、関数型言語でもカプセル化をすることが可能になる
関数型言語では値は不変⏳
多くの関数型言語では値は不変であり、一度、宣言した変数に束縛されている値を変更することが出来ません。
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
以外のmethod
とend_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}
host
がhttps://dog.example.com
に、method
がPOST
に、end_point
が/bark
に変わっているのが確認できます。更新した以外のフィールドの値は変化しないまま、保持されています。必要なフィールドのみを更新関数(setter
)から更新出来ました🎉
全体のコード
[https://replit.com/@okabeyuya/Functionalprogramingwithstate#main.exs:embed:cite]