Elixirでやばいマクロ（Pattern）を書いた理由【イベント購読】

先日、ペアプロYouTuber第一話のなかで`Pattern`について解説しました。
この記事ではそのとき話した内容をもとに、Cizen/Patternの概要と今の設計になった経緯までを説明します。

## TLDR
- `Pattern`は`Cizen.Filter`をもっと柔軟にして`Cizen`から抽出したもの
- `Pattern.new`はフィルタ関数を前置記法で表現した命令のツリーに変換する
- 柔軟で直感的なクエリで、購読数が100万とかでもスケールする仕組みになった

## Cizenとは
https://hexdocs.pm/cizen/readme.html
"Build highly concurrent, monitorable, and extensible applications with a collection of automata."

めちゃくちゃ雑に説明すると、
1. プロセスをSagaという形式で統一的に管理し
2. Sagaはイベントを購読しイベントを配信することで他のSagaに影響を与える
ような枠組みを提供するためのライブラリです。

イベントの購読時には`Cizen.Filter`を使ってクエリを生成します。
近いうちに`Cizen`から`Cizen.Filter`を抽出した`Pattern`で`Cizen.Filter`を置き換える予定です。

## Cizen.Filterの要件
- 柔軟なクエリでイベントを購読できる
- 購読が数百万あってもスケールする

## 例: ライトが on off イベントを購読  
ライトが複数あって、それぞれのライトが以下のようなイベントを購読します

```elixir
%Event{body: %On{light_id: ^light_id}}
%Event{body: %Off{light_id: ^light_id}}
```

## Cizen.Filter設計考察
現在の設計になるまでの流れを1から4の流れで説明します。
### 1. フィルタ関数で購読
もっとも簡単なのはフィルタ関数をクエリとするものです。

Dispatcherはフィルタ関数がtrueを返すような購読に対してイベントを配信します。

#### コード例
```elixir
query = fn
  %Event{body: %On{light_id: ^light_id}} -> true
  _ -> false
end
```

#### メリット
- 柔軟で直感的なクエリ

#### デメリット
- スケールしない
イベントごとに購読数分の関数を呼ぶ必要があるため

### 2. イベントボディーフィルタ
クエリはevent_type (OnやOffなど) と イベントボディーフィルタのリストを持ちます。

Dispatcherはevent_typeで購読を絞った後、イベントボディーフィルタがすべてtrueを返す購読に対してイベントを配信する。

#### コード例
```elixir
query = %EventQuery{
  event_type: On,
  event_body_filters: [
    On.filter(light_id)
  ]
}
```

#### メリット
- 同じevent_typeに購読が集中しなければスケールする

#### デメリット
- フィルタ関数より煩雑
- イベントボディーフィルタはスケールしない

### 3. パイプライン

パイプラインを指定して購読します。

DispatcherはPipeを木状に解釈（クエリを最適化）してイベントを配信します。

以下は2018年の9月に描いた図です。

![IMG_8932.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57526/38384a4e-bf5e-3210-bafa-d7a1d570675b.jpeg)

![IMG_8934.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57526/653737be-bc06-6fbb-bff1-ecb49513708f.jpeg)

#### コード例
```elixir
query = %Pipeline{
  pipes: [
    Pipe.event_type(On),
    On.light_id(^light_id)
  ]
}
```

#### メリット
- イベントボディーフィルタより柔軟
- スケールしそう

#### デメリット
- 実装によってはDispatcherがスマートになりすぎる
- 実装によってはPipelineの最適化がスケールしない
- フィルタ関数より煩雑
- 最適化の部分がGCみたいにシステムの動作を不安定にしそうでこわい

### 4. 関数を実行時に評価できる形式に変換したものを木構造に挿入して購読
関数を実行時に評価できる形式に変換したものを木構造に挿入して購読、します。
やばいマクロでコンパイル時に頑張るということです。

Dispatcherは木構造からイベントにマッチする葉（購読者）を探してイベントを配信します。

#### コード例
```elixir
query = Cizen.Filter.new(fn %Event{body: %On{light_id: ^light_id}} -> true end)
# コンパイル時にこうなる↓
# query = %Cizen.Filter{
#   code: {:and,
#    [
#      and: [is_map: [access: []], ==: [{:access, [:__struct__]}, Event]],
#      and: [
#        and: [
#          is_map: [access: [:body]],
#          ==: [{:access, [:body, :__struct__]}, On]
#        ],
#        ==: [{:access, [:body, :light_id]}, light_id]
#      ]
#    ]}
# }
```

#### 図
![2020_04_12 19_56 Office Lens_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57526/6b50684f-51e6-9cf4-caa9-1179e89df4a5.jpeg)


#### メリット
- スケールする
- 柔軟かつ直感的に書ける

#### デメリット
- 購読時と解除時のオーバーヘッド

## Pattern
- `Cizen`から`Cizen.Filter`の機能を抽出して、明示的に別で管理するようにしたもの
- リファクタリングを行い、鑑賞に堪えうるものにした
- フィルタ関数だけでなくパターンマッチ的表現も許可

例えば、

```elixir
Cizen.Filter.new(fn %Event{body: %On{light_id: ^light_id}} -> true end)
```
と書いていたところを、

```elixir
Pattern.new(%Event{body: %On{light_id: ^light_id}})
```
と書けるようになりました。

## 関連リンク
Cizen: https://gitlab.com/cizen/cizen
Pattern: https://gitlab.com/cizen/pattern
ペアプロYouTuber第一話: https://youtu.be/WEWZoZkWKmE