はじめに
変数が設定できないー、はーー、そんなもの作れるかー、と愚痴ばかり。
いつかは変数を設定できるようにしてやってやるー、と息巻き、長い歳月が過ぎてしまった。
ここでは 現状、この変数問題をどう対処しているのか、を書いてみます。
なぜ、変数が定義できないのか
いやー、この件、書いては消し書いては消しと、まとまらず疲れてしまった。変数って外部変数が定義できない、データ保存ができないのが問題なので、そんなもの作れるか、とか、いろいろと書いていたが、どうも違うなぁ、と途方に暮れてしまった。
そうだ、ChapGptに聞いたら良いと聞いてみた。まぁ、最初からそうしろと言われそうだけど、頭の中では「そんなの当たり前の事じゃん」と、鷹を括っていた。といいわけ。
本題に戻り、「変数が定義できないとはどう言う事」とは聞いてみた。
回答: できないとは「代入によって値を変更する可変変数が存在しない」事。
そうそう、Elixirで最初に学ぶ不変(なんかかんやでimmutableと言っていた)事だ。
x = 1 #変数定義(xのエリアに1を入れる事)でなく、xと1を関連付けるとの意味
x = 2 #可変変数に見えるが、関連付けなので代入、更新するとの意味ではない
C言語で言えば、constの変数みたいなものと考えれば良いかも。関連付けなので意味が違うけど。
理由について、回答は
理由①:参照透過性
理由②:並行処理・分散処理を安全にするため
理由③:状態は「変数」ではなく「引数と戻り値」で表す
などとあった。
しかし、困っているのはこんな関数内の変数定義によるものではなく、外部変数(状態データ)用意できないので「作れない」との事なので、上記定義と理由には関係がない。従って、これ以上、深掘りしない。
外部変数が定義できないのはなぜ
ChatGptに以下の質問をした
「可変変数が定義できないのはわかったが、それは関数内での話じゃないのか、コードを書く上で関数外に静的変数がないので、関数同士で情報交換ができない。処理上、必要不可欠なのに定義できないとはどう言う事なのか。 処理は必ず関数同士でインターフェイスが発生する。メッセージのようなイベントだけでなく、状態データは必要とはならないのか。関数は独立していると言え、それだけで済まないとはならないのか」
回答:
これは 「関数型は理論」vs「実システムは状態を持つ」 の衝突点で、あなたの言っていることは 100% 正しい現場感覚です。
結論を先に言います。
関数型言語は「状態が不要」とは一切言っていない
「状態を“変数”として共有するな」と言っているだけ
との回答でした。あまり、ピンとこない。
つまり、「外部変数定義として、持つ事をせず、別な方法で持つ」との事。
最後の方に「なぜ静的変数を禁止するのか」の事について書かれていた。
静的変数の問題点
- 誰が書いたかわからない
- いつ変わったかわからない
- 排他が必要
- テスト不能
- 再利用不能
やっと、「変数(状態)の持ち方」の前置きが終わり、次から本題になる。
引数を利用する
まず最初に疑問だったのはgen_serverで、どのように状態を持っているのだろうか、と調べた。
def handle_call(:pop, _from, [head | tail]) do
{:reply, head, tail}
end
def handle_cast({:push, element}, state) do
{:noreply, [element | state]}
end
call関数を見ると現在の値が[head | tail]で、{:reply, head, tail}のtailが新しい値でheadが返信するデータ、castではstateが現在値、新しい値は{:noreply, [element | stat]}の[element | stat]]となり、引数で状態とわかった。
また、次の状態遷移処理(自作)
def ledon(cb) do
IO.puts("#{cb.func} (#{timestamp(cb)})")
ledon_loop(cb)
end
def ledon_loop(cb) do
{eve, _msg} = FSM.rcv_msg()
case eve do
:off ->
%{cb | func: :ledoff, arg: 0}
:flk ->
%{cb | func: :flicker, arg: 0}
_ ->
IO.puts("#{cb.func} cont(#{timestamp(cb)})")
ledon_loop(cb)
end
end
のcbは状態として、関数を呼び出す時に常に引数として付けられ、処理している。
通常プログラム言語ではこの引数のデータはスタックエリアに保存されるが、Elixirの場合はスタックと違い、モジュールで管理されたメモリエリア(heap?)で管理されていると思う。との言うのは、再帰呼び出しを繰り返すとスタックが消費されるのではないのか、との質問をChatGptに尋ねた時にheap領域で問題ないと回答だったので。
ただ、実際にはわからない。末尾呼びたしの場合に限定されているかも。
GenServerはごくごく一般的に使用しているが、処理間での話なのにわざわざ使うなんて、どうも納得がいかないと個人的には思う。
外部メモリのライブラリー:etsを使う
:etsモジュールを使用する。
:ets.insert(:user_lookup, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]}) #write
#
:ets.lookup(:user_lookup, "doomspork") #read
更新にinsert()、読込みにlookupを使用しているが、:estにはいろんな関数があり、処理が少々複雑な気がする。全て、パターンマッチングによりread/writeをし、指定番号により要素を取得するように簡単には処理できない。
複数の関数からread/writeする場合、どうしても排他制御する必要があるが、その機能はない。従って、writeする人はひとりにするとか、データの整合性がとれるのかを意識する必要がある。
別の話で、排他制御に関して、enif_mutex_lock()とenif_mutex_unlock()があり、NIF処理内では排他制御できるのか、と思ったが、プロセス間に共有メモリが存在せず、NIFと言えども排他制御はできないらしい。thread間で使用するものらしい。
ファイル化する
ファイルを読み書きして、外部状態として使用する。
これも、おそらく排他制御ができないので注意が必要かつ:etsよりもより複雑になると思う。
まとめ
コードを書いていて、状態データがあれば、処理がし易いのになぁ、といつも思う。
例えば、タイマー管理処理
def countdown(tick, lists) do
lists =
receive do
{eve, mod, timer, reply_eve} -> set_cb(lists, mod, eve, timer, reply_eve)
_ -> lists
after tick ->
do_count(lists)
end
countdown(tick, lists)
end
最初は:etsでタイマーセット情報を持ち、タイマーが必要なモジュールから直接タイマーセットするようにしていたが、排他制御がないので処理ができない。
仕方がなく、メッセージでセットするようにした。しかし、メッセージを受信した時にどうしても、タイマーカウントがずれてしまう現象が発生する。仕方がなく諦めた。
状態遷移処理では引数cbにセットしたものの、下位に繋がる関数を含め、すべて関数にcbを引き継がなねばならないと、なんか気持ち悪い処理となってしまう。
以上のような不都合があり、作りにくいなぁとなり、この記事となりました。