RubyからElixirに移動する上で

Adways Advent Calendar 8日目の記事です。

http://blog.engineer.adways.net/entry/advent_calendar/archive


こんにちは、渡部です。

Advent Calendar用にあれこれ考えたのですが、進捗が出せなかったので、今行っている Elixir での Push API用のマイクロサービス作りの作業を書き出します。

目録

  • Ruby から Elixir へ移動する上で困ったこと
    • ログイン機能 in Elixir on Phoenix
      • CurrentUser Plug の作成
      • ユーザーデータの格納
    • Firebase Cloud Messaging(FCM)
  • 開発環境
    • Google Cloud Platform
      • Elixir のノード間通信
  • 感想
    • オブジェクト指向と関数型
    • Elixir を始めた理由
      • vs Node.js(JavaScript)
      • vs 他言語

RubyからElixirへ移動する上で困ったこと

業務では Ruby を使っているので、そこで好きな部分が Elixir ではないとなると困ります。

よく使う機能は以下の表のようなものです。

Feature Ruby Elixir
Debugger binding.pry require IEx; IEx.pry
Mix-in include use (マクロ)
Meta Programming o o (マクロ)
Deploy Capistrano Distillery

最初に、Elixir でも僕が一番お世話になっているであろう binding.pry のようなものが使えました。

require IEx; IEx.pry

を関数の中に挿入してアプリを起動するときに、 iex -S mix

Phoenixサーバーの場合iex -S mix phoenix.server とするだけ、該当箇所で止まってくれます。

個人的には動かしてデバッグするのが楽なので、これがなかったら、Elixirをやめていたかもしれません。

また、Rubyの表現力を高めてくれているメタプログラミングは、Elixirのマクロを使って似たようなことができました。

デプロイもすでに Distillery というモジュールがあり、簡単にホットスワッピング(サーバーを止めずにコードをリロードする)が可能です。

ログイン機能 in Elixir on Phoenix

CurrentUser Plug の作成

次に Phoenix でサーバーを書く上で困ったことは、どのように現在ログインしているユーザーを取得するかといったものでした。

最近のサービスでは当たり前のようにログイン機能がありますので、なんとかしなければと、作成したのが以下の自作Plugです。

defmodule PushApiServer.Plugs.CurrentUser do
  import Plug.Conn
  require Ecto
  require Ecto.Query
  alias PushApiServer.Repo
  alias PushApiServer.User

  defmacro __using__(_) do
    quote do
      plug PushApiServer.Plugs.CurrentUser

      defmacro current_user do
        quote do
          var!(conn).assigns[:current_user]
        end
      end
    end
  end

  def init(default) do
    default
  end

  def call(conn, _) do
    current_user_id = get_session(conn, :current_user_id)
    current_user =
      if current_user_id do
        User
        |> Ecto.Query.where(id: ^current_user_id)
        |> Repo.one
      else
        nil
      end

    case current_user do
      %User{} ->
        conn |> assign(:current_user, current_user)
      nil ->
        url = PushApiServer.Router.Helpers.signin_path(conn, :new, refer: conn.request_path)
        conn
        |> Phoenix.Controller.redirect(to: url)
        |> halt
    end
  end
end

ログインしているユーザーのIDをセッションに入れておけば、あとはコントローラーのほうで、

use PushApiServer.Plugs.CurrentUser

とすれば、current_userマクロが定義され、ログインしているユーザーのIDは

current_user.id

とすれば取得できます。とても簡単です。

詳細は省きますが、ElixirのPhoenixはPlugの連鎖です。

自分でPlugを書けば、HTTPリクエストがPlugを通るようにすることができます。

僕の環境では web/plugs/current_user.ex に配置してあります。

(lib/ 以下でも良いかと思ったのですが、ライブリロードが走らないとのことだったのでやめました。簡単なユーティリティ関数は lib に置いています)

Plug の作成は init/1call/2 を実装するだけなので、僕のようななんちゃって Elixirer でも簡単に作成できます。

ユーザーデータの格納

パスワードなどをDBに格納するときは、こちらを参考に

:crypto.hash(:sha256, password) |> Base.encode16(case: :lower)

としてハッシュ化して格納することができました。

Elixirは登場して間もないと思うのですが、Erlang の肩の上に乗っているので、モジュールが足りなくて困るといったことがほとんどなく、素直にすごいと思いました。(crypto は Erlang のモジュールです)

開発環境

Google Cloud Platform

Google Cloud Platform を使っています。

とにかく、デフォルトで機能がとても使いやすいです。

  • デフォルトでブラウザでSSHできるので、インスタンスを立てた瞬間内部にログインできる
  • デフォルトでリージョンごとに VPC というローカルエリアネットワークのようなものになっており、ファイアウォールに阻まれることなく開発できる
  • チュートリアルやクイックスタートが充実しており、それに則ってやるとほとんどのことが実現できてしまう
  • インターフェースがとてもきれいで好き

個人的に Google Cloud は神ってる気がします。

Elixir のノード間通信

リージョンごとに VPC がデフォルトでなっていたので、同じリージョンであれば、Elixir のノード間通信も簡単に実現できました。

  • instance-1
$ docker run -p 4369:4369 -p 9001:9001 -it --rm -h instance-1 elixir iex --sname foo --cookie hoge --erl '-kernel inet_dist_listen_min 9001 inet_dist_listen_max 9001'
  • instance-2
$ docker run -p 4369:4369 -p 9001:9001 -it --rm -h instance-2 elixir iex --sname bar --cookie hoge --erl '-kernel inet_dist_listen_min 9001 inet_dist_listen_max 9001'
> Node.connect :"foo@instance-1"
#=> true
> Node.list
#=> [:"foo@instance-1"]
> Node.spawn :"foo@instance-1", fn -> IO.puts("Hello, world!") end # foo@instance-1で実行される
#=> #PID<0000.00.0>
Hello, world!

感想

オブジェクト指向と関数型

オブジェクト指向ではインスタンス変数などを用いて、状態をオブジェクトにカプセル化する工夫だと思います(本質は違うかもしれません)。

オブジェクト指向のプロジェクトでよく成功しているのは小さくオブジェクトを作っていっているプロジェクトと聞きますが、大きいクラスになって使い方を誤ると、途端にインスタンス変数はグローバル変数のような厄介さを持つと思います。

どこで何が変更されているかわからないので、関数の分離や移動などがすごくしづらくなってしまいます。

そこの点では、Elixir は関数型なのでそれをしにくい言語になっているように思います。Elixir もプロセスに状態を持たせることはできるので、「しにくい」だけですがそれはとても大切なことだと思います。

始めて数ヶ月経ちますが、Elixirは状態をほとんど持たないので、とてもリファクタリングしやすいです。

僕はとりあえず作って、気に入らないものを後からリファクタリングしていくようなプロトタイプモデルのような開発手法が好きです。(事前にあれこれ考えるのが苦手です)

状態を持たないというのは一度作った関数をどこへでも持っていけるので、名前空間の整理などにとても便利です。

Elixir を始めた理由

Elixir をやり始めた理由はチームで動くならばどのような言語がいいだろう、と考え始めたことがきっかけでした。

vs Node.js(JavaScript)

始めは、Node.js で何かできればと思っていました。サーバーサイドの JavaScript 環境だということで、フロントとバックエンドの統一言語としてもてはやされています。

しかし、最近ホットなものだということもあり、バージョンアップが盛んで、Babel や AltJS など選択肢が多岐にわたり、事前に決めなければならないことがとても多いように思いました。

それと同時にベストプラクティス的なものがなく、個人でやるにはともかくチーム開発に向かないような気がしました。

Elixir は Phoenix という Rails ライクなフレームワークがでてきており、Elixir が乗っている Erlang は歴史があって、ある程度枯れています。Elixir 自体はまだまだ新しい言語ですが、JavaScript よりも環境依存(ブラウザなど)やモジュール依存が少ないように思えました。

vs 他言語

また、Python や Go など、その他色々な言語がありますが、それぞれ文化があり特長があります。

Python ならば機械学習や科学系が盛んで、Go ならばハード一台をふんだんに使い尽くすということもできると思います。

Elixir/Erlang の良いところは、プロセスを分離できるところだと思います。

それならば、他言語は別プロセスとして立ち上げて、後で結べば良いと思いました(それには落とし穴もあるかもしれませんが)。

プログラミングの領域はフロントエンド、バックエンド、機械学習、インフラ、etc、様々なものがありますが、どれも専門的な知識が必要です。

  • フロントエンドであれば、Reactjs、jQuery3、Angular2、HTML5、JavaScript(ES6、ES.next, ....)
  • バックエンドであれば、DBチューニング、クエリチューニング、RESTFul API、AWS、Google Cloud
  • 機械学習であれば、数学、統計・解析、ディープラーニング、Watson API や Google 機械学習API

今なおバージョンアップを続けており全部を一人で追い続けるのは相当な体力が必要でしょう。

それならば専門家同士をつなぐような立ち回りが必要に思いました。マイクロサービスのような概念はそれを可能にする気がします。

そしてマイクロサービスの作成には、分散処理を得意とする Elixir が適している気がします。 (「気がします」ばかりですね)

もちろん、全部が全部必要なものではありませんから、作りたいものによっては、Elixirは適さないものをあると思います。

そこの部分は判断が難しいところですが、もう少し、Elixir を触って知見を深めていけたらと思っています。

まとめ

色々と書いてしまいましたが、結局ツールは使う人次第だと思います。

これからも何か作れるようにこれからも日々精進していきたいと思います。

間違っている箇所も多々あると思いますが、ご容赦のほどよろしくお願いします。


次は石丸さんの記事です!

http://blog.engineer.adways.net/entry/advent_calendar/09