こんにちは!
この記事は 3/11 に福岡で開催される Elixir オフラインイベントの LT 記事です。
Elixir 書きたい!しかし題材は?
私は現在、家計簿アプリでおなじみ Money Forward に所属して ToB 向けの SaaS を Ruby で開発しています。
最近ではテックブログも書くようにしており、 Ruby バージョンアップの知見をまとめたりしています。
とはいえ私もアルケミスト。 1
日々 Elixir を書きたい気持ちを抑えつつ(?)仕事をしています。
・・・というのは冗談ですが(笑)、せっかく Ruby を仕事でやっているのでそれを活かしつつ、 Elixir コミュニティに貢献しつつ、コード書けないかなぁとずっと思っていました。
Ruby は Web 開発で長く使われてきたこともあり、便利な gem (ライブラリ)がたくさんあります。
そんな時「そうだ、これを Elixir で書き直してみたら面白いんじゃないか!?」と思いついてやってみたのが今回の記事となります。
Ruby gem を Elixir で書き直してみよう
Ruby gem 探し
ということで Elixir で実装しなおす参照元とする gem を探してみます。その際に以下の条件を意識しました。
- コードがそこまで大きくないこと
- (せっかくなので)実際によく使いそうなもの
ということで、見つけたのが以下の gem です。
以前ライブラリの中を読んだことがあり、中の実装は比較的シンプルな割に、便利な gem というのが決め手でした。
この Macrow という gem は継承すると文字列の置換ルールを定義する DSL を提供してくれます。
単純に正規表現で実装してもいいのですが、大量の文字列変換パターンがある場合に煩雑になるのと、実行時に実行コンテキストを指定できるので状況に応じて文字列変換の方法を変えたいときに便利です(例えば DB 内の値に応じて変換方法を変えたいときなど)。
また、Elixir は文字列操作に少しクセがあるので、こういうライブラリは便利だろうと思いました。
作成開始!
ということで無事に決まったので次は制作です。
作成する Elixir ライブラリの名前は MacrowEx
と決めて作成開始しました!
mix new macrow_ex
以下、苦労した点を書いてみます笑
言語仕様の違い
Elixir には継承がない
本家の macrow のインターフェースは以下です。
(README より引用)
class HogeMacro < Macrow
rules do
# this rule means '${something}' -> 'trouble'
rule 'something' do
'trouble'
end
# you can access an object
rule 'length' do |object|
object.length
end
end
end
macro = HogeMacro.new
macro.apply_all_rules("${something} happened")
# => "trouble happened"
macro.apply("${something} happened") # you can use short alias
# => "trouble happened"
array = [1, 2]
macro.apply("object length is ${length}", array) # you can pass an object
# => "object length is 2"
gem が提供する Macrow
クラスを継承することで DSL が使えるようになるので、それによって文字列変換ルールを定義するインターフェースになっています。
・・・しかし Elixir には継承がありません。そもそもクラスがないです。
とはいえ DSL を定義する方法はあります。plug などメジャーどころのライブラリの実装を見ると use
キーワードを使って DSL を定義する手法があるようです。
また Ruby だとブロック引数で定義されていますが Elixir にはブロック引数はないので、関数型らしく無名関数で代用してみます。
ということで、以下のようなインターフェースを考えました。
defmodule MyMacrowEx do
use MacrowEx
rules "hoge", fn ->
"ほげ"
end
rules "len", fn array ->
length(array) |> Integer.to_string()
end
end
メタプログラミングの差異をどのように吸収して実装するか
インターフェースも決まったので実装です。
本家の macrow の実装 を見てみると継承時に instance_variable_set で動的にインスタンス変数をセットしています。
また define_method を使ってインスタンスに動的メソッド定義をしています。
Elixir はインスタンスがないためこの方法では難しいです。一方で Ruby と違ってコンパイルのある言語です。
コンパイル時に関数定義することで、 define_method の代用とします。
なので、どうするかというと Module.register_attribute/3 を使用してコンパイル前に DSL から値をセットしてもらった後に before_complile フックにて use 先のモジュールのコンパイル前にそれらを使用して関数を定義します。
before_complile の挙動についてはドキュメントが詳しいです。
実装を見てみた方が分かりやすいかと思うので、ぜひ見てみてください。
マクロの実装は quote や unquote を駆使するのですが、これらは下記の書籍が参考になりました。
こちらのドキュメントもわかりやすいです。
なお DSL の実装には以下の記事を参考にさせていただきました!(感謝!)
DSL を mix format の対象にしない
その他小ネタを。
何もしないと mix format
した時に以下のように ()
付きでフォーマットされます。
rules("hoge", fn ->
"ほげ"
end)
ただ DSL なので ()
は付けたくないと思いました。
そういう時は .formatter.exs に locals_without_parens
をセットすることで ()
フォーマット対象から除外できます。
また export
を設定することで、インストール先のライブラリでも import_deps: [:macrow_ex]
とすることでフォーマット対象から外すことができます。
地味に便利なので DSL 作る方はぜひ参考にしてみてください。
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
locals_without_parens: [rules: 2, macro_prefix: 1, macro_suffix: 1],
export: [
locals_without_parens: [rules: 2, macro_prefix: 1, macro_suffix: 1]
]
]
完成!そして OSS 化!
ということで完成したライブラリが以下になります!
せっかくなので OSS 化しよう!ということでドキュメントを書いたり Elixir のパッケージマネージャーである hex に公開することにしました。
ドキュメント
ドキュメントは ex_doc を使うことで生成できます。
コード中にルールに沿ってモジュールや関数の説明を書くだけでリッチなドキュメントを作ることができます。
Elixir 自身のドキュメントもこちらで作られています。
MacrowEx では以下のようにコメントを書いています。
mix hex.publish
後は OSS として公開です。公開の方法は以下の記事が詳しいです。
なおすでに登録済みの場合は mix hex.user auth
でログインできます。
公式ドキュメントもあるので詳細はこちらを確認してみてください!
その他 OSS としてやっておきたいこと
あとは以下のようにタグをつけたり Github Release を書いたりするとより OSS として親切な形になりますね
git tag -a 0.1.0 -m "v0.1.0"
git push origin 0.1.0
github actions で自動で CI が回るようにするのもおすすめです。
完成!
ということで MacrowEx を OSS として公開しました。
使ってみよう
早速使ってみましょう。
ちなみに Elixir には Ruby の irb のような、 iex というインタラクティブシェルが付属されているのですが、そのシェルの中で Mix.install/2
を使うことで動的にライブラリをインストールできます。
iex を起動してぜひ使ってみましょう!
iex(1)> Mix.install([:macrow_ex])
.....
iex(2)> defmodule MyMacrowEx do
...(2)> use MacrowEx
...(2)>
...(2)> rules "Elixir を愛する人", fn ->
...(2)> "アルケミスト"
...(2)> end
...(2)> end
iex(3)> MyMacrowEx.apply("${Elixir を愛する人}は最高だ!")
"アルケミストは最高だ!"
最後に
ということで Ruby の gem を Elixir で書き直して OSS 化してみた話でした。
言語間の差異を学びつつ、有用なライブラリを作ることができて満足でした。
私は Ruby のライブラリを Elixir に書き直してみましたが、他の言語のライブラリでも同じことはできると思います。
いつもは Elixir じゃなくて別の言語書いてるよーって人も、お使いの言語の便利なライブラリを Elixir に書き直してみてはいかがでしょうか。
別の言語で使われているライブラリを Elxiir で開発しなおすことは、実装するものが明確なので開発しやすく、ある程度使われている実績のあるライブラリであれば需要もあるし、Elixir コミュニティとしては便利なライブラリが増えるということで歓迎!といいことづくめですね!
(もちろん参照元ライブラリの作者には敬意を払って開発しましょう )
ちょっと手を動かしたい時にも最適なので、皆さんぜひやってみてください。
そして、その制作過程や制作結果はぜひ Elixir コミュニティでシェアしましょう
-
Elixir を書く人のことをこう読んだりします ↩