Taku’s Teckブログ

Teckはわざとです。

【メモ】私が愛する Elixir/Erlang の楽しさと辛さ

元資料が素晴らしいためまとめる必要など全くないのですが、自己の整理のため、箇条書きにて書き起こししているものです


Elixirのメリット

  •  Web開発のための優秀なツール完備
  •  サーバ構成をシンプルに保てる
  •  コードをシンプルに保てる
  •  夜眠れる


なぜ流行らないのか?

  • 学習コストが高い(と思われれている) × 納期が短い = 採用されない
  • 学習コストが高い(と思われれている)・・並行処理、関数型、マクロと難しそうな概念を学ぶ必要がありそう


伝えたいこと

  • Webシステムの開発ならば、学習コストは高くも低くもない
  • 加えて、安全性が高く、生産性が高く、それ以外の性質は程良い
  • →Elixirは学習コストのわりには恩恵が大きい


難しいのであれば学ぶことを取捨選択して学習コストを下げる

以下、選択して学習することで学習コストを下げてElixirの恩恵を受けることができる

  • 並行処理、マクロ、ビヘイビア、プロトコルは最後に学ぶ
  • ここで紹介した以外はリスト操作に習熟してから学ぶ

 
EVMの並行処理について

巷の良い話
  • アクターモデル →?
  • メッセージパッシング →?
  • メモリを共有しないからシンプル
巷の悪い話
  • 概念自体の学習にコストが必要
  • (共有メモリ+スレッドより楽だけど)メッセージパッシングですら人類には早かった

        → 前提条件を変更する

           PhoenixWebサービスを開発するなら、逐次処理を書くだけで、軽量プロセスの恩恵を享受できる

            →よってしばらく並行処理は忘れて良い、学習コストを払うのも後回しで良い

 

以下のように考えるとわかりやすい

  • EVM(Erlang VM) = OS
  • Cowboy(Phoenixが依存しているWebサーバライブラリ) = WebServer
  • iex = Shell


Erlangで作られたWebサーバは一つの接続ごとに一つのプロセスを生成する。10万個のチャットのセッションが必要なら10万個のWebサーバが起動する。仮にその中の一つが落ちたとしても、誰も気にしない」

 

f:id:taku-exs:20190505215042p:plain

 


EVM=OSと似たプロセスの仕組みを持つ

プロセスとは
  • 独立したCPUを持つ
  • 独立したメモリを持つ
  • ネットワーク上の一意な住所を持つ
どういうことか
  • EVMは各軽量プロセスにCPUを使う権限を与える
  • 軽量プロセスが一定量処理すると別の軽量プロセスに権限が移る
 メリット
  • 一つの軽量プロセスがCPUを占有しづらい
  • (極論)軽量プロセスないで無限ループしてもシステム全体に影響が出にくい
  • 軽量プロセス内はシンプルな逐次処理となる
  • Elixirであれば同じ処理を簡単な逐次処理として記述できる

         →「どこでコンテキストが切り替わるのか」考慮不要

 

f:id:taku-exs:20190505215211p:plain

  • プロセスを終了するとメモリを解放できる
  • システム全体でメモリリークしづらい
  • プロセス毎にGCが可能
  • GC中にコンテキストを切り替え可能
  • (結果)GCでシステム全体が停止しない
ネットワーク上の一意な住所を持つとは?
  • EVMは各軽量プロセスに一意なアドレスを割り当てる
  • PIDは複数のEVMで構成されたクラスタ上で一意

 (利点)

  • 軽量プロセス同士がメッセージを送受信し、共有メモリを使わずに連動できる
  • 軽量プロセス同士がシグナルを送受信し監視や管理(再起動など)ができる

 

 (注意点)

  • とはいえ、高頻度に再起動するとCPU負荷が上がるため注意
  • プロセスを使いまわすと、プロセス停止によるメモリ解放が行われないため、メモリリークしないように気を配る必要がある

 

速さはおまけ
  • 軽量プロセスは耐障害性の向上するための仕組みであり、速さはおまけ

         => OSと似たことをしているのに早いわけがない

  • Node.jsは他の動的言語と比較すると圧倒的に速い(V8というものが頑張っているらしい)
  • 結果として、Elixir => 並行処理の学習コスト不要で、ほぼほぼに遅くないとても楽で安全な言語


Elixirの関数型について

巷の良い話
  • 副作用少ない
  • データ処理に集中
  • gotoは処理フローの迷子、○○は状態の迷子
巷の悪い話
  • 概念自体の学習にコストが必要
  • 敷居が高すぎる
  • しかも何が嬉しいか不明
  • 世間一般には関数型言語が難しいとされている
Elixirは純粋な関数型ではない
  • そのため関数型由来の仕組みが少ない
  • ○○由来の仕組みが少ない
  • 複雑さが排除されていて、理解するための文脈が少ない
  • 他者の記述したコードを読むのが楽

 (注意)マクロ自体やマクロ由来の仕組みは複雑さを招きやすい


純粋だと言われる条件
  • 参照透過な関数で表現できることが多い → ?
  • e.g. 演算子、例外、IOなどが関数で表現されている

オススメの学習方法

  • プログラミングElixir:p11-157を読む
  • Elixir入門:11プロセス、16プロトコル、20ビヘイビア以外
  • ElixirSchool:基礎の章だけ読む
  • Phoenix GUIDES:Up and Runningから始める(始めはChannel,Presenceを読み飛ばす)
プログラミングElixirを読む前に
  • プログラミングとはデータを変換することであり、リスト,Enum,モジュール,パイプライン演算子はデータ変換の道具であるため、ここに学習コストを払う
活用の鍵
  • 構造と再帰
  • 再帰を使いこなすと表現の自由度が広がるが、可読性が落ちる
  • for や Enumで要件を満たせる場合、そちらを使う
Elixirのリスト
  • リストとはペアで作ったリンクリスト
  • [1]というのは[1 | ]の糖衣構文
   ※糖衣構文:プログラミング言語において、読み書きのしやすさのために導入される書き方であり、複雑でわかりにくい書き方と全く同じ意味になるものを、よりシンプルでわかりやすい書き方で書くことができるもののこと
 語源は「取り扱いやすい」を意味するsweetの第一義が「(砂糖のように)甘い」であることから
 https://eow.alc.co.jp/search?q=syntactic+sugar

 例)[1, 2, 3] = [1 | [2 | [3 | ]]]
  再帰=headとtailの繰り返し(プログラミングElixir参照)
Enum
  • Enumモジュールで定義されている関数は様々な言語で類似する名前と機能が提供されているため、一度覚えると他の言語でも応用が効きやすい
  • よく使われるものから覚える
  • mapが多い
  • (Enumよりforの可読性が高いことがある(forを使えという理由でfilter_mapという関数がEnumから削除された))
パイプライン
  • Shellでいう | (パイプ)みたいなもの
  • パイプラインの存在意義=可読性の上昇
  • 関数定義に一貫性が生じる
  • パイプラインが使えるように関数を定義する癖がつく
  • 第一引数 = 操作前データ
  • 戻り値 = 操作後データ
(JavaScriptにもパイプラインが導入されるかも?)
* http://tc39.github.io/proposal-pipeline-operator/
do
  • doの法則
  • do ブロックを引数に取るものは、変数のスコープを持つ値(戻り値)を持つ
  • 例) defmodule, def, case, if, forなど
  • ブロックにスコープと戻り値があるため、ブロックネストの深いところで、上位ブロックの変数を再束縛(する必要がない|できない)
  • よって「この値はどこから来たのか?」を調べる手間が減り、可読性が上がる

 

マクロ、ビヘイビア、プロトコルを後回しにして良い理由
  • Webシステムの開発に限定すると、新たに自作のこれらを定義しなくとも開発できるから


Elixir/Erlang の辛さ
OTPについて

  • OTPは高性能もWebシステム開発ではほぼ必要ない
  • OTPは高性能だが断捨離する
  • モジュールで設計(抽象化)するなら関数中心で考える
  • Elixirの用途を割り切る
  • APIサーバをElixirで書く
  • APIサーバはステートレスにしてオートスケーリングに対応する
  • キャッシュやDBなどは既存品を使う

断捨離して良いもの

  • ETS
  • オンメモリDB

         =>キャッシュ用途に便利だが、共有メモリでスレッドプログラミングする世界に逆戻りする

  • DETS

          =>オンディスクDB

  • Mnesia

         => ETSとDETSの上に構築された分散DB

              APIサーバにDBのロールを担わせるため、サーバのオートスケーリングの難易度を上げる

          =>末尾再帰する関数をフルネームで評価すると発生する
               * プロセスを越境してコードホットスワップがシステム全体に適用されている時せいは見たことない
                  https://blog.stenmans.org/theBeamBook/

               * 起動しているプロセス全てに対して、完全にコードホットスワップに対応していると保証するコストより、再起動コストの方が低い
               * サービスの無停止を実現するならば、Blue-Green Deploy(※)で良い
                ※デプロイをより安心して行うために、サーバの内容を変更する際には既存のサーバに手を加えるのではなく、新規に作り直して切り替える、という方法
                  https://www.publickey1.jp/blog/14/blue-green_deployment.html

 

  • これらの良さは「すごいErlangゆかいに学ぼう!」を参照

           →APIサーバを作るだけなら、無駄な複雑性をシステムに呼び込まないよう、利用を避ける

  • ETSはEcto内でPrepareしたDB Queryを一ノード内でキャッシュして使いまわすなどに利用されている
  • Elixir入門直後はこのようなチューニングを実績あるライブラリに任せる

 

混線する関数型と○○

 

モジュールの2つの役割

  • 関数をまとめる
  • 構造体や型を定義する
  • →クラス定義に見える
  • クラス定義に見えると何がいけないのか?
  • Elixir で OO ができる!と勘違いして, 無駄な抽象化を持ち込み, コードが複雑化する事案が発生しやすい
  • 例えば, Ecto で repo という単語を使っているため, リポジトリ パターンによる抽象化のベストプラクティスを持ち込む話しなど
  • プロトコルとビヘイビアの混合
  • Trait などの知識があると, プロトコルやビヘイビアは, いつ, ど こで使うのか?という判断が OO に紐付いてしまう
  • プロトコルはデータが主役
  • ビヘイビアは関数が主役
  • Elixir は, プロトコルとビヘイビアの定義がモジュールに紐付いて いる。プロトコルはデータに, ビヘイビアは関数に紐づけて考えると良い

 

マクロは高性能
  • マクロは言語作者が楽をする機能
  • あまりに強力すぎる

         → Elixir は, あらゆるものがマクロで実装されている

  • マクロの理解は, Web システム開発においては必須ではないが、マクロが読めないと正しい理解に時間がかかる機能もあるため、Elixir に慣れてからゆっくり学習するとよい