読者です 読者をやめる 読者になる 読者になる

ぱろっと・すたじお

技術メモなどをまったりと / my site : http://parrot-studio.com/

Elixirについて調べたついでに、BrainF**kインタプリタを書いてみた

最近、某D社の事例を含めて、Elixirの話題をよく見かけるようになりまして

http://elixir-lang.org/

もちろん、話題になっているからには、
何らかの「時代の要請」があるはずでして、そのあたりを調べてみたわけです

なぜ、Elixirが出てきたのか?

まず読んでみた本はこちらです(´・ω・)っ

プログラミングElixir

プログラミングElixir

この本は言語の使い方について書いた本ではなく、
「設計思想」について書かれた本でして、
そのこと自体がElixirの出自を象徴しております

歴史の流れを復習してみると、かつてWebはPerlJavaがメインで、
普通のWebであればだいたいPerlCGIで書かれていたわけです

でも、ある程度大きな機能になるとPerlではきつくなってきて、
「もっとちゃんとした言語」が求められ、
RailsをきっかけにしてRubyがブレイクしました

Rubyのポイントは、まつもとさん自身が書いているように・・・

・・・多くの言語が「機能」からの要請で生まれてきたのに対し、
Rubyは「コードをこう書きたい」とか「こう書けると気持ちいい」といった、
「設計思想」を優先して生まれてきた、という経緯があります

その分、(特に1.9より前においては)実行速度に難があったのですが、
システムの大規模化・複雑化や、CPU速度の飛躍的向上により、
メリットがデメリットを大幅に上回るようになったわけです(`・ω・´)

一方で、時代はPCからモバイルへ変わっていき、
「大きなリクエストを素早く」ではなく、
「小さくて大量のリクエストを素早く」という要求に変わっていきました

古いアーキテクチャではこの要請に応えられない*1ため、
Nginxが登場したり、Apacheアーキテクチャが変わり、
さらなる高速化のためWebSoketやそれに対応するNode.jsが出てきたりしたわけです

そして、CPUのアーキテクチャも変化しました
単純な速度ではもはや向上が見込めないため、
マルチコアにするのが当たり前になってきています

その結果、「小さくて軽い処理(をするAPIサーバ)」が求められるようになり、
Rubyではこれらの状況に対応できない・・・ということはないのですが、
Railsもスリム化できるようになり、Node.jsと組み合わせたりするようになりました

(勘違いしないでもらいたいのは、Rubyがダメということではありません
 Rubyの強みは「時代の要請についていくセンス」と、
 「変化を許容し、時に古いものを切り捨てる強さ」にあるので)

一方で、「小さく軽い処理のを組み合わせて一つの大きな処理を構成する」方式は、
まさに関数型言語が得意とする領域です

しかし、関数型言語は難しすぎます
古くはLispHaskellがありますが、これを「普通の人」が扱うのはとても無理*2です(´-ω-)

その領域をScalaが埋めていくのかな・・・と思っていたのですが、
(少なくとも私は)某D社以外に大きな事例も聞かず、
「Play Framework」のWebにおける大きな採用事例も(少なくとも私は)知りません

そもそも、当の某D社がScalaを切った・・・とまではいかずとも、
Scalaで書き直しました!」と大々的にアピールしていたシステムを、
すでにElixirで書き直しているあたりで、だいだい状況は推測できます(´・ω・`)

他の関数型言語だと「F#」あたりなのですが・・・
あれは本を読んでみたものの、難易度的にはScalaと大差はなく、
(少なくとも現状では)Windowsっぽいイメージが強すぎてきついです

難易度を無視すれば、「小さなロジックを大量のプロセスで処理する」*3という、
理想的な設計で事例もある「Erlang」も使えるはずなのですが、
パラダイムのジャンプだけでなく、文法的な難しさがつきまといます

こんな状況の中、「関数型言語をもっとなじみがある文法で」、
より端的にいえば「Rubyが書きやすいんだから、Rubyみたいに書けて、
Erlangのように動く言語
があればいいんじゃない?」と考えた人がいたわけです

それがまさに「Elixir」でして、冒頭でも書いたように、
Elixir最大のポイントは「こう書けるとうれしい」という、
「設計思想」を優先して生まれた言語である、ということなのです(`・ω・´) b

ElixirでBrainF**kインタプリタを書いてみる

先ほどの本を読んだ感じ、設計思想にはものすごく共感できたので、
とりあえず私にとって「関数型言語におけるFizzBuzz」である、
「BrainF**k」の実行系を書いてみました...φ(・ω・`)

以前もScalaで書いたことがありますし・・・

parrot.hatenadiary.jp

・・・それより前にはRubyで「実行系を作成する実行系」を書いたりもΣ(・ω・ノ)ノ

で、今回Elixirで実際に書いてみたのがこちらです(´・ω・)っ

gist.github.com

大きなポイントは二点ありまして、まず一つ目は
「データの処理の流れを記述する」ということを強く意識させる文法です

  def execute(code) do # codeを実行するとは・・・
    code        # 受け取ったcodeを・・・
      |> parse  # 解釈し・・・
      |> eval   # 実行することである
  end

  defp parse(str) do # 解釈するとは・・・
    str  # 受け取った文字列を・・・
      |> String.replace("\n", "")  # 改行を取っ払って・・・
      |> String.codepoints  # 一文字ずつに分離し・・・
      |> List.foldl([], fn c, li -> li ++ [@cmap[c]] end)
        # コマンドのストリームに置き換えることである
  end

この書き方はとてもいいですね(`・ω・´) b

「|>」は前の関数の結果を次の関数の第一引数として渡す、
というだけの構文でしかないのですが、
これだけで「何かしたいのか?」がとても明確になります

それこそが「関数型言語風に宣言的なコードを書く」ことの、
最大の利点であり、それ自体は他の言語でも享受できるのですが、
文法として明確に組み込まれているのがポイントです

もう一点が、「関数の引数自体がパターンマッチになっている」ことです

  defp step(:pinc, coms, jump_map, ind, cind, pind, buf, result) do
    step(coms[cind+1], coms, jump_map, ind+1, cind+1, pind+1, buf, result)
  end

  defp step(:pdec, coms, jump_map, ind, cind, pind, buf, result) do
    step(coms[cind+1], coms, jump_map, ind+1, cind+1, pind-1, buf, result)
  end

# 略

  defp step(_, _coms, _jump_map, _ind, _cind, _pind, _buf, result), do: result

Javaでいうオーバーロードに近いのですが、
ScalaRubyでいうところのcaseやswitchのような、
パターンマッチングを、関数の定義自体でやっています

先ほど挙げた、以前書いたScalaコードを見てもらえばわかるのですが、
stepに相当するところの中でパターンマッチングしていて、
また自身を再帰的に呼び出しています

そのため、stepの処理自体が肥大化しているし、
新しいコマンドを追加する場合、case文のマッチングを増やさないといけません(´-ω-)

その点、Elixirのコードは一つ一つの関数が「定義」として小さくまとまっており、
「何をしたいのか?」が明確になって、見通しが良くなっています

「これだけのこと」ではあるのですが、
それでも「こう書けるととても楽」というのを優先したのがElixirでして、
まさにそれはRubyと同様の設計思想なわけです(`・ω・´)

Elixirは「使える」のか?

こんな感じで、私の好みにあうElixirですが、
今の私の仕事である「アーキテクト」の立場からすると、
少なくとも今は「ない」かな・・・とΣ(゚Д゚)ガーン

弊社CTOとも話をして、「この言語はいいね(`・ω・´)」という話にはなっていますが、
あくまでお仕事として考えると、あまりにもコストが高いかなと思います

「表面的な文法」はRubyに似ているため、
例えばRubyの初期のように「PerlのようにRubyを書く」のと同様、
RubyのようにElixirを書く」のは可能ですし、私もそのように導入します

しかし、Elixirの利点を最大限の享受するには、
「関数型的な設計思想」が必須になります

ElixirがRubyに強く影響を受けているのと同様に、
ElixirのWebフレームワークである「Phoenix Framework」は、
とてもRailsの影響が強いです

www.phoenixframework.org

「ある程度わかっている人ならば」Railsの仕組みをこれに移すことも可能かもしれません

しかし、RubyRailsも最大のポイントは、先ほど書いたように、
「時代の要請についていくセンス」と
「変化を許容し、時に古いものを切り捨てる強さ」です

RubyはどんどんVMが高速化されていますし、
文法的にも「型」を導入しようとしています
(普通の言語なら危険信号なのですが、まつもとさんならまず大丈夫でしょう)

Railsもモジュール化・スリム化(API専用モードなど)が進み、
並行処理を意識したライブラリを組み込んだりもしています

github.com

なにより、個人的な感覚では、「ElixirでRDBにアクセスすること」自体が、
せっかくのメリットを台無しにしていると思いますΣ(・ω・ノ)ノ

Node.jsの仕事をしたときに強く感じたのは、
とにかく「全てを非同期に書かなければならない」ということであり、
「同期的な処理をどう切り離すか?」を常に考えないといけません

こういったことを意識しなければならないのは、
少なくとも「普通のWebシステム」においてはオーバースペックですし、
例えばRubyでもそういった設計思想のシステムは十分組めます*4

あとは人的コストの問題です
Node.jsを書いたことがある人はそこそこ探せますし、
Rubyを書ける人なら(質を問わなければ)十分見つかるでしょう
(なぜ、FBような大規模システムで、PHPが使われているのかを考えてみてください)

今後、Elixirが何らかの形でメインストリームに乗った場合*5は別として、
現時点では「あえてRubyを切り捨てる」メリットがあまりに薄いのかな・・・と思います

メインのWebサーバをRuby+Railsで組み、
リアルタイム性を求められる領域をNode.jsで構築する、というのが、
今の時点では「普通にいけそうなWebシステム(=長く運用できるシステム)」だと思います

逆に「普通じゃないWebシステム」、
例えば「大量の動画をバックエンドでリアルタイムにエンコードし続けるシステム」ならば、
かなり有効に作用すると思います

・・・が、そういうシステムを必要とする会社は限られてますし、
そういう会社はシステムにかけられるコストが桁違いなので、
やはり「普通の会社」では難しいですよね・・・(´・ω・`)


<おまけ>

今回の記事を書く前、本を読み終わった後にTwitterで連投したものをメモしておきます...φ(・ω・`)

*1: いわゆるC10K問題

*2: ここでいう「無理」とは、「扱える人がいない」のではなく、「扱える人を雇うコストが、一般のWeb企業において現実的ではない」くらいの意味で

*3: このプロセスをクラウド上で起動するのが「サーバレスアーキテクチャ」とか話題のあれですね

*4: というか、以前仕事で組みました http://parrot.hatenadiary.jp/entry/2013/09/03/112657

*5: 個人的にはそうあってくれると楽しいのですが・・・(´-ω-)