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

Rubyにpipeline operator(以下pipeline演算子)が導入されると話題になっているようです(【速報】Ruby の trunk に pipeline operator がはいった, Twitterでの検索結果、など)。

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


pipeline演算子の現状について

JSのpipeline演算子はまだ正式に導入されたわけではなく、いわゆるStage 1と呼ばれる状態で、まだ仕様自体しっかり固まっていない状態です(Stageについてはこちらがわかりやすいです)。

そのため、将来的に本記事の内容とは違う文法・仕様になっている可能性もあるため、ご注意ください。

更に、pipeline演算子のproposalにも書かれているのですが、仕様にF# PipelinesSmart Pipelinesと呼ばれる2つの案があるようです(違いは後述)。

2つの仕様はOriginal / Minimal Proposal(以下Minimal proposal)と呼ばれるベースの仕様を発展させる形で提案されています。

そのため、まずはMinimal proposalについて解説を行います。


Minimal proposalについて

Minimal proposalでは、pipeline演算子の基本的な仕様が提案されています。

それではpipeline演算子について解説していきます。

pipeline演算子は|>という記号を用いることで左辺の値を引数として右辺の関数を呼び出すことができるというものです。

つまり、以下の2つの式は同じ意味になります。

x |> f

f(x)

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

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

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

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

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

arr

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

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

アロー関数を用いているのは、Minimal proposalではpipeline演算子の右辺に記述する関数のどこに左辺の値(arr)を渡すのか指定することができないためです(Smart Pipelinesだと可能)。

そのため、第一引数にarrを渡せば良いだけの_.sortByではアロー関数を用いていませんし、例えば「2乗する」「10以上のみ取り出す」を関数化してあれば以下のように書けます。

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. アロー関数にカッコが必須


  2. awaitが使えない


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

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

arr

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

(arr => _.map(arr, n => n * n))のように、アロー関数がカッコで囲われています。

これは、文法上の曖昧さが残ってしまうためカッコなしをエラーにする仕様となっているようです。

例えば、

a |> b => c |> b

と書いた際に、

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

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

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

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


awaitが使いづらい

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

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. アロー関数にカッコを付けなかったときの扱いを定義

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


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

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

先程の

a |> b => c |> b

という例は、

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

と解釈される、と決めてしまうということです。

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


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

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

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 styleBare styleに分類し、Topic styleでは#記号を使って式の中の任意の位置に「左辺の値」を埋め込めるようにすることができます。

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


Bare styleの例

識別子と.のみの式はBare styleとして扱われ、Minimal proposalのpipeline演算子と同じことができます。

ただし、式に( )[ ]なども含めることはできません。

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の例にあったエラーはこのエラーです)。

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 fTopic styleにも関わらず#が使われていないためエラーとなり、n |> await f(#)n |> (await f)(#)と書くことができるようになります。


式と変換後の対応表

ここを参考に、pipeline演算子を用いた式がBare styleTopic styleのどちらに分類されるのか、どのような式に変換されるのかをまとめてみました。


style
変換後

value |> f
Bare style
f(value)

value |> x + 1
Topic style
Syntax Error

value |> # + 1
Topic style
value + 1

value |> o.m
Bare style
o.m(value)

value |> o.m(1, 2)
Topic style
Syntax Error

value |> o.m(#, 1, 2)
Topic style
o.m(value, 1, 2)

value |> o.m(1, 2, #)
Topic style
o.m(1, 2, value)

value |> o.m(1, 2)(#)
Topic style
o.m(1, 2)(value)

value |> await o.m(#)
Topic style
await o.m(value)

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


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

F# Pipelinesは現在実装中のようですが、Minimal proposalSmart PipelinesについてはBabelを用いてトランスパイルすることが可能なようです(@babel/plugin-proposal-pipeline-operator · Babel)(Try it out)。


所感

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

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

ただメソッドチェーンと変わらないpipelineはどこが便利なのか正直わかりません。


参考リンク