JavaScriptのpipeline operatorについてまとめてみた

Rubyにpipeline operator(以下pipeline演算子)が導入されると話題になっているようです([【速報】Ruby の trunk に pipeline operator がはいった](https://qiita.com/pink_bangbi/items/cef6a73f8c580356b927), [Twitterでの検索結果](https://twitter.com/search?vertical=default&q=ruby%20%E3%83%91%E3%82%A4%E3%83%97%E3%83%A9%E3%82%A4%E3%83%B3%20since%3A2019-6-13%20until%3A2019-6-17&src=typd)、など)。

残念ながら私はRubyがほとんどわからないので、代わりにJavaScript(この場合ECMAScriptと呼ぶべきですが)に提案されているpipeline演算子についてまとめてみようと思います。

## pipeline演算子の現状について

JSのpipeline演算子はまだ正式に導入されたわけではなく、いわゆる`Stage 1`と呼ばれる状態で、まだ仕様自体しっかり固まっていない状態です(Stageについては[こちら](https://azu.github.io/slide-what-is-ecmascript/slide/12.html)がわかりやすいです)。
そのため、将来的に本記事の内容とは違う文法・仕様になっている可能性もあるため、ご注意ください。

更に、[pipeline演算子のproposal](https://github.com/tc39/proposal-pipeline-operator)にも書かれているのですが、仕様に`F# Pipelines`と`Smart Pipelines`と呼ばれる2つの案があるようです(違いは後述)。

2つの仕様は`Original / Minimal Proposal`(以下`Minimal proposal`)と呼ばれるベースの仕様を発展させる形で提案されています。
そのため、まずは`Minimal proposal`について解説を行います。

### `Minimal proposal`について

`Minimal proposal`では、pipeline演算子の基本的な仕様が提案されています。
それではpipeline演算子について解説していきます。

pipeline演算子は`|>`という記号を用いることで左辺の値を引数として右辺の関数を呼び出すことができるというものです。
つまり、以下の2つの式は同じ意味になります。

```js
x |> f
f(x)
```

これだけでは何が嬉しいのかわからないと思いますが、`lodash`などを用いて「関数の結果を別の関数に引数として渡すのを繰り返す」ときに可読性が高まります。

例えば、「配列のそれぞれの値を2乗し、値が10以上のものをソートして取り出す」という処理はpipeline演算子無しだと以下のように書けると思います。

```js
_.sortBy(_.filter(_.map(arr, n => n * n), n => n >= 10))
```

上記のコードの問題点として、行いたい処理の順番(2乗する→10以上のみ取り出す→ソートする)と、コード上の記述順序(`_.sortBy`, `_.filter`, `_.map`)が逆というものがあり、コードを読む際の認知負荷が高くなっています。

pipeline演算子を使うことで、上記の処理を以下のように書くことができます。

```js
arr
|> (arr => _.map(arr, n => n * n))
|> (arr => _.filter(arr, n => n >= 10))
|> _.sortBy
```

ご覧の通り、行いたい処理の順番とコード上の記述順序が一致しています。

アロー関数を用いているのは、`Minimal proposal`ではpipeline演算子の右辺に記述する関数のどこに左辺の値(`arr`)を渡すのか指定することができないためです(`Smart Pipelines`だと可能)。
そのため、第一引数にarrを渡せば良いだけの`_.sortBy`ではアロー関数を用いていませんし、例えば「2乗する」「10以上のみ取り出す」を関数化してあれば以下のように書けます。

```js
const pow2 = arr => _.map(arr, n => n * n);
const filterGreaterThan10 = arr => _.filter(arr, n => n >= 10)

arr
|> pow2
|> filterGreaterThan10
|> _.sortBy
```

## `Minimal proposal`の問題点

`Minimal proposal`には以下の2つの問題点があります。

1. アロー関数にカッコが必須
1. `await`が使えない

### アロー関数にカッコが必要

先程も記載したコード例を改めて提示します。

```js
arr
|> (arr => _.map(arr, n => n * n))
|> (arr => _.filter(arr, n => n >= 10))
|> _.sortBy
```

`(arr => _.map(arr, n => n * n))`のように、アロー関数がカッコで囲われています。
これは、文法上の曖昧さが残ってしまうためカッコなしをエラーにする仕様となっているようです。

例えば、

```js
a |> b => c |> b
```

と書いた際に、

```js
a |> b => (c |> b) // (b => b(c))(a)
a |> (b => c) |> b // b((b => c)(a))
```

のどちらと解釈されるのかがわかりづらいということです。

そのため、カッコのないアロー関数は構文エラーとする、というのが`Minimal proposal`での仕様のようです。

### `await`が使いづらい

`await`が使いづらいとはどういうことかというと、以下のコードがエラーになるということです。

```js
async function f(n) {
  return n
    |> generateUrlWithQuery(n) // 引数nを元に、クエリ付きURLを生成する関数のイメージ
    |> await fetch // エラー!!
}
```

こちらもアロー関数のときと同じように曖昧さによるものだと説明されており、`n |> await f`というコードが`(await f)(n)`と`await f(n)`のどちらの意味なのかがわかりづらいため、仕様としてエラーになるようになっているのです。

逆にいうと、曖昧さのない構文なら問題ないため、`n |> (await f)`や`n |> (async n => f(n))`のように書けばエラーではなくなります。

## `F# Pipelines`について

`F# Pipelines`では、前述の`Minimal proposal`の2つの問題点を以下のように解決するようです。

1. アロー関数にカッコを付けなかったときの扱いを定義
1. pipeline演算子の右辺に`await`のみの記述可能にする

### アロー関数にカッコを付けなかったときの扱いを定義

pipeline演算子の右側でアロー関数を定義した際に、後続のpipeline演算子はアロー関数のボディに含めないように定義してしまう、というものです。

先程の

```js
a |> b => c |> b
```

という例は、

```js
a |> (b => c) |> b
```

と解釈される、と決めてしまうということです。
もし`a |> b => (c |> b)`と解釈させたい場合は、そのようにカッコつきで書いてあげれば良いです。

### pipeline演算子の右辺に`await`のみの記述可能にする

以下のように、`await`のみを書くことで、左辺のPromiseが解決するのを待つという案のようです。

```js
async function f(n) {
  return n
    |> generateUrlWithQuery(n) // 引数nを元に、クエリ付きURLを生成する関数のイメージ
    |> fetch
    |> await // ここで上段のfetchが解決されるのを待つ
}
```

これによって、`n |> (async n => fetch(n))`という若干面倒な記述を用いる必要がなくなります。

以上が、`F# Pipelines`の説明です。

## `Smart Pipelines`

`Smart Pipelines`はpipeline演算子の右辺を`Topic style`と`Bare style`に分類し、`Topic style`では`#`記号を使って式の中の任意の位置に「左辺の値」を埋め込めるようにすることができます。

言葉だけだとよくわからないと思うので、コード例を見てみましょう。

### `Bare style`の例

識別子と`.`のみの式は`Bare style`として扱われ、`Minimal proposal`のpipeline演算子と同じことができます。
ただし、式に`( )`や`[ ]`なども含めることはできません。

```js
v |> o.fn // o.fn(v) と同じ
v |> f // f(v) と同じ
v |> console.log // console.log(v)と同じ
v |> obj[prop] // エラー(ただし、エラーの内容はTopic styleに関連するエラーです)
```

`Bare style`以外の全ての式は`Topic style`として扱われます。

### `Topic style`の例

`Topic style`の右辺には、任意の式を書くことができ、式の中の`#`というトークンは左辺の値に置き換えられます。
また、必ず`#`を1つ以上含める必要があり、無かった場合はエラーになります(`Bare style`の例にあったエラーはこのエラーです)。

```js
v |> # + 1 // v + 1 と同じ
v |> # / (1 + #) // v / (1 + v) と同じ
v |> obj[prop] // エラー #を使っていない
v |> obj[prop](#) // obj[prop](v) と同じ

arr
|> _.map(#, n => n * n) // _.map(arr, n => n * n)
|> _.filter(#, n => n >= 10) // _.filter(中略, n => n >= 10)
|> _.sortBy // (Bare style) _.sortBy(中略)
```

`Smart Pipelines`では、pipeline演算子の右辺部分にアロー関数を使うことは(おそらく)無いため、アロー関数にカッコが必須という制約も問題なくなります。

また、awaitが出てくる例ですが、`n |> await f`は`Topic style`にも関わらず`#`が使われていないためエラーとなり、`n |> await f(#)`や`n |> (await f)(#)`と書くことができるようになります。

### 式と変換後の対応表

[ここ](https://docs.google.com/presentation/d/1eFFRK1wLIazIuK0F6fY974OIDvvWXS890XAMB59PUBA/edit?ts=5aa931f1#slide=id.g34fce93f9f_0_208)を参考に、pipeline演算子を用いた式が`Bare style`と`Topic style`のどちらに分類されるのか、どのような式に変換されるのかをまとめてみました。

|式|style|変換後
|:--|:-:|:-:|
|<code>value &#x7c;> f</code>|`Bare style`|`f(value)`|
|<code>value &#x7c;> x + 1</code>|`Topic style`|**Syntax Error**|
|<code>value &#x7c;> # + 1</code>|`Topic style`|`value + 1`|
|<code>value &#x7c;> o.m</code>|`Bare style`|`o.m(value)`|
|<code>value &#x7c;> o.m(1, 2)</code>|`Topic style`|**Syntax Error**|
|<code>value &#x7c;> o.m(#, 1, 2)</code>|`Topic style`|`o.m(value, 1, 2)`|
|<code>value &#x7c;> o.m(1, 2, #)</code>|`Topic style`|`o.m(1, 2, value)`|
|<code>value &#x7c;> o.m(1, 2)(#)</code>|`Topic style`|`o.m(1, 2)(value)`|
|<code>value &#x7c;> await o.m(#)</code>|`Topic style`|`await o.m(value)`|

以上が、`Smart Pipelines`の説明です。

## pipeline演算子を試してみるには

`F# Pipelines`は現在実装中のようですが、`Minimal proposal`と`Smart Pipelines`についてはBabelを用いてトランスパイルすることが可能なようです([@babel/plugin-proposal-pipeline-operator · Babel](https://babeljs.io/docs/en/babel-plugin-proposal-pipeline-operator))([Try it out](https://babeljs.io/repl#?babili=false&browsers=&build=&builtIns=false&spec=false&loose=false&code_lz=MYewdgzgLgBGMF4YGYDcAoUlYAcQHdEYBbRAPjnJgFkBDKACwDo98AKMAGhIEpNxoMVgCYirNsJ4Z-kEABsApkzkgA5hxgAfCiJ5A&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=react%2Cstage-1&prettier=false&targets=&version=7.4.5&externalPlugins=))。

## 所感

`Smart Pipelines`の`#`を使った独特な記法は最初は気持ち悪いかもしれませんが、この記事を書くために眺めていたら慣れました()

普段JSしか書いてないのでpipeline演算子がある言語を使ったことがなく旨味がまだちゃんとわかっていませんが、関数の結果を他の関数に渡すことはそれなりの頻度であるので、正式採用されれば便利なんだろうなぁとほんのりと感じました。
~~ただメソッドチェーンと変わらないpipelineはどこが便利なのか正直わかりません。~~

## 参考リンク

- [tc39/proposal-pipeline-operator](https://github.com/tc39/proposal-pipeline-operator)
- [tc39/proposal-pipeline-operator Wiki](https://github.com/tc39/proposal-pipeline-operator/wiki)
- [Pipeline operator: Stage 1 update
](https://docs.google.com/presentation/d/1eFFRK1wLIazIuK0F6fY974OIDvvWXS890XAMB59PUBA/edit?ts=5aa931f1#slide=id.p)
- [パイプライン演算子の歴史](https://mametter.hatenablog.com/entry/2019/06/15/192311)