fukuoka.ex代表のpiacereです
今回もご覧いただいて、ありがとうございます
まもなく、私のQiitaコラム1番人気が、「重力プログラミング入門『第2回:Pythonで重力波を解析する』」から、「Excelから関数型言語マスター1回目:行の『並べ替え』と『絞り込み』」に取って代わられそうな勢いで、Elixirへの注目が集まり始めてきました
「重力プログラマ」としての私は危機感を感じています(笑)が、「福岡Elixirプログラマ」としての私はホクホクしています
さて今回は、Elixirをやると疑問に感じる方も多いであろう、他言語にあってElixirに無い(もしくは簡単に使えない)イディオム…たとえば、whileが、何故無いのかについて、答えてみようと思います
なお、内容については、関数型やElixirの由緒正しい文献を見ながら持ってきた、とかでは無く、思考と記憶から引っ張ってきているので、より良い解釈や理解があったら、コメントいただけると有り難いです
お知らせ:Elixirもくもく会(リモート参加OK、入門トラック有)を今日、開催です
「fukuoka.ex#14:Elixir/Phoenixもくもく会~入門もあるよ」を9/28(金)に開催します
今回から「Zoomによるリモート参加」を正式に受け付けるようになりましたので、福岡以外の都心や地方からでも参加できます(申し込みいただいたら、追ってZoom URLをconnpassメールでお送りします)
また、これまではElixir/Phoenix経験者を対象とした、もくもく会オンリーでしたが、今回から、入門者トラックも併設し、fukuoka.exアドバイザーズ/キャストに質問できるようにアップグレードしました
お申込みはコチラから
https://fukuokaex.connpass.com/event/100659/
Q「何故、ループ中に変数の更新ができないの?」
Elixirで、Enum.mapなどを覚えないまま、forを使った方のあるあるですが、以下の通り、forループ中の変数更新は、反映されません
iex> sum = 0
0
iex> for i <- [ 1, 3, 5 ] do
...> sum = sum + i
...> end
[1, 3, 5]
iex> sum
0
これは、forの内側ではスコープが維持されないことと、Elixirがイミュータブルな言語であること※が背景としてあります
※ただし再代入は可なので、こんな書き方もwarningで通してしまう(初心者向きにはerrorにしても良い位)
ここを、Elixirらしく書いたら、forでイチイチループを回すまどろっこしさとかは、一切無いです
iex> [ 1, 3, 5 ] |> Enum.sum
9
さて、そもそもやりたかったことに立ち返ると、「リスト中にある全ての数を合計する」です
その点で、後者のコードは、やりたいことまんまなのに対し、前者のコードは、そのやりたいことを叶えるために、「ループ」と「途中状態を保持する変数」という、余計なものが2つも混入している訳です
それでも前者の方は、慣れ親しんでいて、馴染みがあるため、自動思考としてナチュラルなものと扱っている訳ですが、一度立ち止まり、冷静に考えれば、「問題を解くことと関係が無いことをしている」とも言えます
「ループ」と「途中状態を保持する変数」という慣習的な手段に囚われており、問題を解くことに集中していない…とも言い換えられます
また、このカンタンなコードでは実感できないのですが、「途中状態」という余分なものを持つことで、そこを起因とするバグ…たとえば、時間経過や状態変化によって挙動が変わる状況…を生み出す可能性があります
なお、Elixirには「状態遷移」のような、明確に状態を扱う問題に対しては、ちゃんと「状態遷移」を扱える構文(ライブラリ)として「GenStateMachine」というものが用意されています
詳しくは、@kikuyuta さんのこちらのコラムやこちらのコラムをご参考ください
Q「何故、カウンタ付きのループ構文が無いの?」
Elixirでは、「ElixirのStreamでカウンタ付き無限ループ」のように、カウンタ付きのループを書くのも、一苦労です
start = 0
count_up = 1
sleep_msec = 1000
Stream.iterate( start, &( &1 + count_up ) ) |>
Enum.map( fn count ->
IO.puts( "count => #{ count }, count_up => #{ count_up }" )
:timer.sleep( sleep_msec )
end )
また、Enum.with_indexという、リストの各要素にインデックスを付与する方法もあり、これによりカウンタ付き処理のようなものが書きやすくなるのですが、Enum.with_indexが書かれたコードは、だいたいElixirらしく無い書き方になっているケースが多く、ちゃんと背景があって使われているかを疑う、バッドパターンの1つとして捉えています
iex> [ "a", "b", "z" ] |> Enum.with_index
[{"a", 0}, {"b", 1}, {"z", 2}]
このように手軽にカウンタを扱えないようにしているのも、バグを混入させず、問題を解くことに集中させるための仕組みの範疇です
まずループカウンタですが、これも「途中状態」の1つです
時間経過や状態変化によって挙動が変わるため、バグを生む可能性があり、テストが難しくなります
次に、Q1でも書いた通り、問題を解くのに「ループ」は必然では無いのですが、「カウンタという状態変化するものを基盤にしたロジックを組む」ということを繰り返すことで、それ無しでは、問題が解けないという思い込みが発生します
これは非常に中毒性・依存性が高く、手続き型思考から、関数型思考やデータフロー型思考に移行できなくなる大きな原因でもあります
少し大きな話になりますが、データフローレベルでの抽象化がされていないコードは、実行性能を高めたり、並列化したりといった最適化が阻害される…つまり、「性能改善が容易で無くなる」ことにも繋がっていきます
もともと、「手軽に使えるカウンタ付きループ」というものは、シングルCPU(かつパイプライン処理等が行われない前提)でのマシンコードをベースとしたプログラミングを容易にするための仕組みだった訳ですが、現代のマルチコアCPUやGPU、並列・分散処理を前提としたプログラミングパラダイムにおいては、害のある考え方です
最後に、カウンタ付きループを書くよりも、EnumやStream、Flowをマスターすれば、より安全で、より目的に沿った処理が、エレガントに書けるからです
カウンタ付きループのような、手段に走った、まどろっこしい書き方は、Elixirのようなモダン言語では不要なのです
今回のまとめ
他言語にあってElixirに無い「ループ中の変数更新」や「手軽に使えるカウンタ付きループ」は、暗黙のバグを混入させ、問題を解くことを阻害する要因だということが、何となく伝わったでしょうか?
今回は、ざっくりしたコード例でしたが、より詳細なコード例は、都度コラム化していこうと思います
また、現代のハードウェア性能を引き出したり、性能改善する上でのバッドパターンを避けるための機構が、Elixirには備わっている、ということも、少しだけですが、お伝えしました
こうしたパラダイムシフトに、Elixirをやっていく中で、ちょくちょく遭遇しますが、これはある意味で「問題を解くためのプログラミング言語の進化」と言っても良い変化です
「変化していく時代」もしくは「世界」に置いていかれないレベルのプログラマを目指すのであれば、こうした変化には、敏感であった方が良いでしょう