- Rubyバックエンド開発者が Elixirを主言語として使う会社に加わったあと、Rubyの影響を受けた構文のおかげで、Elixir/Erlangの経験がなくても比較的すばやく適応できた
- ElixirはRubyのように構文上の楽しさとDSLとの相性の良さを提供しつつ、クラス・インスタンス・継承のない 不変な関数型モデルによって、状態管理の負担を減らす
- RubyのDSLがランタイムでオブジェクトとメソッドを動的に定義することに依存する一方、ElixirのDSLは コンパイル時マクロでランタイムコードを生成し、エラーをより早い段階で明らかにする
- パイプライン構文、
with式、{:ok, result}形式の Resultタプルは、正常系の流れと復旧可能なエラー処理を分離するのに有用
- Rubyで例外や
nil/false、オブジェクト内部のエラー状態に頼っていた方法より、Elixirの明示的なok/error返却のほうが満足度が高く、3か月使ったあとでも書く体験は楽しいと見ている
Ruby経験者がElixirに適応した方法
- 新しい職場ではバックエンドの主言語として Elixir を使っており、始める前はElixirやErlangのコードを書いた経験がなかった
- ErlangはPrologを少し触った経験があったため馴染みがなくはなく、ElixirはRubyから強い影響を受けた構文のおかげで素早く習得できた
- まだベストプラクティス、アーキテクチャ、より低レベルのErlang概念に精通しているわけではないが、こうした点がElixirへの入り口を妨げることはなかった
- ElixirはRubyがもたらすプログラミングの楽しさを保ちながら、OOP言語でよくある状態関連の落とし穴を減らす言語として受け止められている
- クラス、インスタンス、継承がない
- 不変性 と関数型スタイルを基盤にしている
- 静的型システムに縛られない
- 可変状態を持つコードは保守がはるかに難しく、状態が関数ローカルなのか、インスタンスなのか、クラスなのかが常に明確とは限らない
- Rubyではすべてがオブジェクトであるため各オブジェクトが状態を持てるうえ、直感的なDSLを作ろうとする過程で、関数・インスタンス・クラスレベルの状態が混ざりやすい
- Rubyの Eigenclass はクラスのクラスのような概念であり、あるインスタンスがeigenclassを変更することで、ほかのインスタンスの状態にも影響を与え得る
DSLとコンパイル時マクロの違い
- ElixirもRubyのようにDSLをサポートし、構文も馴染みやすいが、核心的な違いはDSLが作られるタイミングにある
- ElixirのDSLは コンパイル時マクロ であり、ランタイムコードを生成する
- RubyのDSLはランタイムを変更し、オブジェクトとメソッドを動的に定義する方式に依存する
- Elixirのマクロを誤って使うとライブラリでエラーが起き、生成されたマクロコードが間違っていればコンパイラエラーが発生する
- こうしたエラーはコンパイル時に明らかになるため、テストは実際のアプリケーションロジックにより集中できる
パイプラインとwithが作る流れ
- Elixirのパイプライン演算子
|>は便利だが、Clojureのthreading演算子にも利点がある
- Clojureの
->はElixirの|>のように、前の式の結果を次の関数の第1引数に入れる
- Clojureの
->>は前の結果を次の関数の 最後の引数 に入れる
- この違いは小さく見えても、匿名関数を使わずに相互運用コードを書くときに役立つ
- Elixirの
with式はHaskellやLispのletを思い起こさせつつ、otherwiseを内蔵しているかのように動作する
- 複雑な関数を、上側の正常経路と下側のエラー処理に分けられる
{:ok, result}にパターンマッチできなければ復旧可能なエラーとして扱えるため、Resultタプル と相性が良い
明示的なエラー返却がもたらす利点
- Rubyでは制御フローに例外を使うことがよくあり、明示的なエラー型の代わりに、成功結果、
nil、false、またはオブジェクト内部のエラー状態に依存することが多い
- たとえば
model.update(params)は失敗時にfalseを返し、model.errorsに失敗理由を保存する
- これは再び 可変状態 の問題につながる
Result / Eitherモナドのように、ok/errorまたはleft/rightを返り値として明示する方式のほうが、Ruby式の例外フローより満足感がある
- 3か月にわたってElixirを使ったあとでも、ElixirはRubyが示した「プログラミングは楽しい」という感覚を受け継ぐ言語であり続けている
1件のコメント
Hacker Newsの意見
ここ数週間、ほぼ毎日 ElixirのYouTube動画を見ている
少し前にElixirのカンファレンスがあったようで、いくつか見たらYouTubeがElixir関連のコンテンツを延々とおすすめしてくるようになった
Erlangのアイデアは好きだが、以前ejabberdのチャットサーバーのようなものを扱ったときは、あまりに独特だと感じた
Joe Armstrongの動画を見るたびに、過小評価された天才だったと思うし、隔離されたプロセスとメッセージパッシング、つまりアクターモデルは分散プログラミングの未来の候補として良さそうに見える
Crockfordの新しいMisty言語の動画 https://www.youtube.com/watch?v=R2idkNdKqpQ でも、質問がElixir/Erlang方面に向かったのが印象的だった
ただ、LiveViewとLiveBookの「魔法」を見ると、まだ100%確信は持てない。あまりに良さそうに見えるものにはたいてい落とし穴があったし、GenServerのような聞き慣れない用語やプラットフォーム固有の知識は、一方通行のポータルに入っていくような感覚で、他へ移転できる知識なのか分からない
Goにもかなり良い並行処理の話がある状況で、小さなコミュニティの言語とプラットフォームは、採用・ライブラリ・資料の面でリスクが大きい。それでもElixirコミュニティが成し遂げたことは本当に印象的で、型システムの実装がどうなるのかも楽しみだ
sendがどう動くかだけ見ればいいGenServerは、何度も自分で書くことになるパターンを自然に一般化したもので、アクターの知識は他にも持ち運べる
Goにもアクター抽象を実装したライブラリがあるし、プロセスのメールボックスはGoのチャネルのようなメッセージキューだと考えればいい。違いはインタプリタとプロセッサの譲渡がどう動くかという点にある
一方でLiveViewが魔法のように見えるのはその通り。実際、多くの可動部分が噛み合っていて、複数のコミュニティでこの10年積み上げられてきた作業の成果なので、抽象化は多くてもアイデア自体は成熟している
Riakのようなものは時代を先取りしていたし、GenServerを作るように堅牢な分散システムを作れるというアイデアを、ほぼ備えていた
ChrisもJoséも初期のElixir圏の外で露出を増やそうとかなり動いているし、コミュニティ内の人たちもそれぞれのレベルで同じことをしている
採用は思うほど複雑ではない。この言語で働きたい人はかなり多く、優秀な開発者ならかなり早く追いつく
それでもニッチな言語という側面は飼いならす必要がある。言語自体にとってはリスクだが、短期的にはニッチ技術に熟達していることへの報酬にもなる
ほとんどのものは何をしているのか見て理解でき、コードも読める。JavaScriptではそうでないことが多い
採用やチュートリアル探しが難しいという点はその通り。例えばElixirで自分がやろうとしていることを既にやった人を見つけるのは、JavaScriptよりずっと難しい
ライブラリは大きな問題ではなく、かなり多い。APIを扱うとき、JavaScriptやPython向けのクライアントライブラリはあってもElixir向けはない場合があるが、普通はHTTPクライアントだけでAPIを使うのはそれほど難しくない
今Node.jsを使っているのは、使いたいからではなく使わなければならないからだ。使ってみて、新しいプロジェクトをNode.jsで始めることは絶対にないと思った
Node.jsの並行処理制御、スケーラビリティ、信頼性・可観測性ツールは、BEAMエコシステムよりかなり遅れている
人々がElixirを「あまりに良すぎて信じがたい」と感じるのは、レジリエンスのために設計されていないプラットフォームに慣れすぎているから、という場合があると思う
LiveBookは、差分計算やデータ圧縮の正確な詳細を除けば、正しいメンタルモデルを持つのはかなり簡単だ。急いでいればWebSocketメッセージを覗いても、何が起きているのかかなり把握できる
LiveViewは驚くほど直感的だ
Elixir/Erlangの大きな魅力は、Webサーバーの文脈でバックグラウンドジョブを本当に簡単に実行でき、ブロッキングI/Oがサーバー全体を止める心配がないことだ
前職ではWebhookハンドラの中でHTTPリクエストを大量に行う必要があったが、同時に十分な数が発生するとOSプロセスがすべて忙しくなり、サイト全体が落ちることがあった
そんなとき、ElixirならTaskを1つ起動して先に進めばよかったのに、と切実に思った
Tokioの非同期プロセス間でオブジェクトを共有するという考え方は、Elixirの第一級機能に比べると難しく感じた。不可能とか本質的に難しいというわけではないが、正しく作るのがより難しそうに見えた
バックグラウンドジョブはHTTPリクエストとは別のコンピュート容量の上に置きたい。リソース利用のパターンが違うし、リトライ・エラー処理・バックプレッシャーを明確にするために、状態やキューを前段に置きたいからだ
もちろん、こうした複雑さがなく同じフレームワークが全部処理してくれればいいが、プログラミング言語やランタイムだけでそこまで行くのは難しいと思う。モニタリングと運用手順までが解決策の一部であるべきだ
Sasa JuricのThe Soul of Erlang and Elixirの発表は、この言語がどれほど強力になり得るかをよく示している
https://www.youtube.com/watch?v=JvBT4XBdoUE&t=4
「失敗させよ」とか「BEAMは並行処理を正しく扱う」と聞いてもまだピンと来ていない人に強くおすすめする。本当にすごい発表だ
1991年から毎日 Erlang でプログラミングしてきており、今後もそうするつもりだった。
その間に Haskell、Rust、Elixir なども試した。強い型付けは非常に価値があるが、結局はどこでいちばん楽しさを感じるかに帰着する。
Erlang は最も大きな楽しみを与えてくれるプログラミング言語で、数年後に引退するまでは、少なくとも Erlang をハックし続けるつもりだ。
Elixir を知ってから1年少し経ったが、学んでいろいろな構成要素に組み込んでみる過程はかなり楽しかった。
最大の楽しさは、BEAM/OTP だけで標準搭載されているものが多いところから来ている。
一緒に働いていたチームの誰かが「BEAM/OTP は複雑な部分を取り除いた k8s みたいなものだ」と言ったことがある。
数年間 Elixir を学んできたが、最近自分が設計した OpenShift、つまり k8s へのマイグレーションのせいで、その利点はいくらか薄まった。
k8s がなければ、はるかに魅力的な提案だっただろう。代わりに既存の開発スタックを維持し、OTP でやっていたであろうことを k8s の概念で実現した。もちろんトレードオフは異なっていた。
「Elixir は Ruby のような楽しい言語を提供しつつ、オブジェクト指向言語の状態にまつわる落とし穴を取り除いてくれる。クラスも、インスタンスも、継承もなく、不変・関数型であり、静的型システムに縛られない」
これが欲しいが、型 もあってほしい。大きなコードベースでは強い型付けが正しいと確信している。多くのマジックを明らかにしてくれ、推論しやすくなるからだ。
関数の パターンマッチング/単一化 のおかげで、データが行き来するときにどんな形なのかかなりよく分かる。それでも不安なら Dialyzer がある。
Dialyzer は Erlang/Elixir 向けの静的解析ツールだ。標準の Erlang リリースに含まれており、「DIscrepancy AnaLYZer for ERlang programs」の略である。
単一の Erlang モジュールやアプリケーション群から、型エラー、デッドコード、不要なテストのような不整合を見つけ出す。正しいプログラムを基に型を推論し、型注釈は必須ではないが、あればより良い警告を提供する。
https://fly.io/phoenix-files/adding-dialyzer-without-the-pai...
https://elixir-lang.org/blog/2023/06/22/type-system-updates-...
GenServer はオブジェクトに近く、単に関連関数が付いた構造体ではなく、メッセージを通じて相互作用する。
プロジェクトの GenServer に共通の登録、メモリ設定、ロギングのような基本テンプレートを置き、それを3種類の特定の作業に合わせて特殊化できるとよい。
Elixir はこの問題をマクロによるコード生成で埋めているが、あまり良くはない。自分の GenServer たちは些細な繰り返しでいっぱいだ。
「Ruby では制御フローに例外を使うことが一般的だ」という話は、単に間違っていると思う。
その段落の後の例は Rails の
updateメソッドだが、標準的な Rails の例やジェネレーターで使われる方法は、例外を使わないupdate版だ。ある程度の規模があるライブラリやアプリケーションは、十分に複雑になると説明的なエラークラスへ移行する。ActiveRecord のような状態ベースのエラー処理はつらいからだ。
Ruby には今やある程度 パターンマッチング があるので、制御フロー用の例外を置き換えられると思う。例外を投げる代わりにクラス自体を返し、そこにマッチさせればよい。
多くの gem が制御フローに例外を使っており、自分が書いたり保守したりしたアプリやライブラリでも一般的だった。
Rails にはエラー時に例外を投げる
bang!メソッドが多い。しかし一般に Ruby は Python と同じように、制御フローに例外を使うことが実際によくある。F# から来て、Elixir は少しだけ触ってみたのですが、残念ながら言語としてはやや混乱しやすいです。
たとえば関数シグネチャは名前と引数の数だけを示し、型は示さないので、大きなライブラリでは何がどこに入るのかを頭の中で把握するのがかなり大変です。
間違った引数を渡していたことも、コードを実行してデバッグしてからようやく分かることがよくあります。F# のような厳格な型付き言語では、こうした問題はコーディング中にすぐ捕捉されるため、時間と労力を大きく減らせます。
なので、Erlang 環境を除くと、Elixir の主な魅力は何なのか気になります。
https://elixir-lang.org/blog/2023/09/20/strong-arrows-gradua...
現時点では動的言語であることは確かで、そのため言語自体から型情報を多く得ることはできません。まだ静的型付け言語ではありません。
ただし、自分が使ってきた他の言語より Elixir を優位にしているのは、本当に BEAM/OTP です。
経験上、他の言語ならライブラリとして取り込む必要がある多くの機能が標準で含まれています。そうしたライブラリは複雑さを大きく増やし、時には学習曲線も急になります。非同期関連の機能がよい例です。
Elixir をしばらく使い、こうした組み込みツールや抽象化に慣れると、エコシステム全体の力が理解できるようになると思います。
主な魅力は、おそらく Ruby 風の構文で BEAM 上で動くことです。BEAM はかなり大きな利点だと思います。
F# の型とはうまく比較できませんが、typespec は使えます: https://hexdocs.pm/elixir/1.15.7/typespecs.html
たとえば Axon、Nx、BumbleBee とともに 機械学習 がネイティブに統合され、GPU コンパイルまで可能な流れがあります: https://www.youtube.com/watch?v=HK38-HIK6NA
コミュニティはこの方面を実際にかなり強く推しています。
LiveView の動作方式も魅力的です。ただし .NET には Blazor があります。
Elixir は組み込み向けの Nerves やスクリプティングなど、さまざまな文脈で使えます。F# もスクリプティングには使えると理解しています。
全体として BEAM とそれに関連する構造が強みで、パッケージマネージャの Hex も本当に優れています。もちろん .NET もツールは良いです。
typespec、パターンマッチ、ガードでしのぐことはできますが、かなり面倒で、少なくともコミュニティ主導の JetBrains プラグインでは IDE/IntelliSense に優しくありません。
始めたときではなく、2年使ってみた後 に書いてみるのがよさそうです。
新しいものは最初のうちは、いつも欠点のない輝かしい万能解決策のように見えるものです。
第一印象にもそれなりの位置づけがあり、私が提供しているのもそれです。対象は Elixir/Erlang の専門家ではなく Ruby の専門家たちだからです。
Elixir は当時も今も宝石のような存在です。完璧ではありませんが、驚くほどよく持ちこたえます。
2007年ごろから Ruby を使ってきました。
次の「かっこいい」ものに乗り換えようとするたびに、結局 Ruby に戻ってきました。Ruby が好きで、Ruby で最も速く仕事ができ、それがすべてだったからです。
ところが最近 Elixir の話をずっと多く見かけるようになり、見た目や雰囲気が気に入っています。近いうちに新しいサイドプロジェクトを始めて、一度使ってみるべきだと思っています。