Ruby gem を Elixir で書き直してみよう

こんにちは！
この記事は 3/11 に福岡で開催される Elixir オフラインイベントの LT 記事です。

https://fukuokaex.connpass.com/event/272529/

# Elixir 書きたい！しかし題材は？
私は現在、家計簿アプリでおなじみ [Money Forward](https://corp.moneyforward.com/) に所属して ToB 向けの SaaS を Ruby で開発しています。
最近ではテックブログも書くようにしており、 Ruby バージョンアップの知見をまとめたりしています。 

https://moneyforward-dev.jp/entry/2023/02/28/100000

とはいえ私もアルケミスト。 [^1]
日々 Elixir を書きたい気持ちを抑えつつ（？）仕事をしています。
・・・というのは冗談ですが（笑）、せっかく Ruby を仕事でやっているのでそれを活かしつつ、 Elixir コミュニティに貢献しつつ、コード書けないかなぁとずっと思っていました。

Ruby は Web 開発で長く使われてきたこともあり、便利な gem （ライブラリ）がたくさんあります。
そんな時「そうだ、これを Elixir で書き直してみたら面白いんじゃないか！？」と思いついてやってみたのが今回の記事となります。

# Ruby gem を Elixir で書き直してみよう
## Ruby gem 探し
ということで Elixir で実装しなおす参照元とする gem を探してみます。その際に以下の条件を意識しました。

- コードがそこまで大きくないこと
- （せっかくなので）実際によく使いそうなもの

ということで、見つけたのが以下の gem です。
以前ライブラリの中を読んだことがあり、中の実装は比較的シンプルな割に、便利な gem というのが決め手でした。

https://github.com/syguer/macrow

この Macrow という gem は継承すると文字列の置換ルールを定義する DSL を提供してくれます。
単純に正規表現で実装してもいいのですが、大量の文字列変換パターンがある場合に煩雑になるのと、実行時に実行コンテキストを指定できるので状況に応じて文字列変換の方法を変えたいときに便利です（例えば DB 内の値に応じて変換方法を変えたいときなど）。
また、Elixir は文字列操作に少しクセがあるので、こういうライブラリは便利だろうと思いました。

## 作成開始！
ということで無事に決まったので次は制作です。
作成する Elixir ライブラリの名前は `MacrowEx` と決めて作成開始しました！

```bash
mix new macrow_ex
```

以下、苦労した点を書いてみます笑

## 言語仕様の違い
### Elixir には継承がない
本家の macrow のインターフェースは以下です。
（[README](https://github.com/syguer/macrow#usage) より引用）

```ruby
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 を定義する手法があるようです。

https://qiita.com/uasi/items/a96db4c6c50f0b1027be

また Ruby だとブロック引数で定義されていますが Elixir にはブロック引数はないので、関数型らしく無名関数で代用してみます。

ということで、以下のようなインターフェースを考えました。

```elixir
defmodule MyMacrowEx do
  use MacrowEx

  rules "hoge", fn ->
    "ほげ"
  end

  rules "len", fn array ->
    length(array) |> Integer.to_string()
  end
end
```

### メタプログラミングの差異をどのように吸収して実装するか
インターフェースも決まったので実装です。

本家の [macrow の実装](https://github.com/syguer/macrow/blob/d038717eb787a50803629d6d562570ca1cdbd246/lib/macrow.rb#L29) を見てみると継承時に [instance_variable_set](https://docs.ruby-lang.org/ja/latest/method/Object/i/instance_variable_set.html) で動的にインスタンス変数をセットしています。
また [define_method](https://docs.ruby-lang.org/ja/latest/method/Module/i/define_method.html) を使ってインスタンスに動的メソッド定義をしています。

Elixir はインスタンスがないためこの方法では難しいです。一方で Ruby と違ってコンパイルのある言語です。
コンパイル時に関数定義することで、 define_method の代用とします。

なので、どうするかというと [Module.register_attribute/3](https://hexdocs.pm/elixir/1.14.3/Module.html#register_attribute/3) を使用してコンパイル前に DSL から値をセットしてもらった後に [before_complile](https://hexdocs.pm/elixir/1.14/Module.html#module-before_compile) フックにて use 先のモジュールのコンパイル前にそれらを使用して関数を定義します。
before_complile の挙動についてはドキュメントが詳しいです。

https://hexdocs.pm/elixir/1.14/Module.html#module-before_compile-1

実装を見てみた方が分かりやすいかと思うので、ぜひ見てみてください。

https://github.com/koyo-miyamura/macrow_ex/blob/f882041337bb35a85de99d5f26b7bba9db406270/lib/macrow_ex.ex#L53-L112

マクロの実装は quote や unquote を駆使するのですが、これらは下記の書籍が参考になりました。

https://www.amazon.co.jp/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0-Elixir-%E7%AC%AC2%E7%89%88-Dave-Thomas/dp/4274226379

こちらのドキュメントもわかりやすいです。

https://elixirschool.com/ja/lessons/advanced/metaprogramming

なお DSL の実装には以下の記事を参考にさせていただきました！（感謝！）

https://qiita.com/uasi/items/a96db4c6c50f0b1027be

## DSL を mix format の対象にしない
その他小ネタを。
何もしないと `mix format` した時に以下のように `()` 付きでフォーマットされます。

```elixir
    rules("hoge", fn ->
      "ほげ"
    end)
```

ただ DSL なので `()` は付けたくないと思いました。
そういう時は .formatter.exs に `locals_without_parens` をセットすることで `()` フォーマット対象から除外できます。
また `export` を設定することで、インストール先のライブラリでも `import_deps: [:macrow_ex]` とすることでフォーマット対象から外すことができます。
地味に便利なので DSL 作る方はぜひ参考にしてみてください。

```elixir:.formatter.exs
# 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 化！
ということで完成したライブラリが以下になります！

https://github.com/koyo-miyamura/macrow_ex

せっかくなので OSS 化しよう！ということでドキュメントを書いたり Elixir のパッケージマネージャーである [hex](https://hex.pm/) に公開することにしました。


### ドキュメント
ドキュメントは ex_doc を使うことで生成できます。
コード中にルールに沿ってモジュールや関数の説明を書くだけでリッチなドキュメントを作ることができます。
Elixir 自身のドキュメントもこちらで作られています。

https://github.com/elixir-lang/ex_doc

MacrowEx では以下のようにコメントを書いています。

https://hexdocs.pm/macrow_ex/MacrowEx.html#content

### mix hex.publish

後は OSS として公開です。公開の方法は以下の記事が詳しいです。
なおすでに登録済みの場合は `mix hex.user auth` でログインできます。

https://qiita.com/ma2ge/items/0e19bf3f03078f589096

公式ドキュメントもあるので詳細はこちらを確認してみてください！

https://hex.pm/docs/publish

### その他 OSS としてやっておきたいこと
あとは以下のようにタグをつけたり [Github Release](https://github.com/koyo-miyamura/macrow_ex/releases) を書いたりするとより OSS として親切な形になりますね :thumbsup: 

```elixir
git tag -a 0.1.0 -m "v0.1.0"
git push origin 0.1.0
```

[github actions](https://github.com/koyo-miyamura/macrow_ex/blob/master/.github/workflows/run-test.yml) で自動で CI が回るようにするのもおすすめです。

### 完成！
ということで MacrowEx を OSS として公開しました。

https://hexdocs.pm/macrow_ex/MacrowEx.html#content

## 使ってみよう
早速使ってみましょう。
ちなみに Elixir には Ruby の irb のような、 iex というインタラクティブシェルが付属されているのですが、そのシェルの中で `Mix.install/2` を使うことで動的にライブラリをインストールできます。

https://hexdocs.pm/mix/1.14/Mix.html#install/2

iex を起動してぜひ使ってみましょう！

```elixir
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 コミュニティとしては便利なライブラリが増えるということで歓迎！といいことづくめですね！
（もちろん参照元ライブラリの作者には敬意を払って開発しましょう :pray: ）

ちょっと手を動かしたい時にも最適なので、皆さんぜひやってみてください。
そして、その制作過程や制作結果はぜひ Elixir コミュニティでシェアしましょう :thumbsup: 

https://discord.com/invite/k7PHppmy53


[^1]:Elixir を書く人のことをこう読んだりします