Rubyにpipeline operator(以下pipeline演算子)が導入されると話題になっているようです(【速報】Ruby の trunk に pipeline operator がはいった, Twitterでの検索結果、など)。
残念ながら私はRubyがほとんどわからないので、代わりにJavaScript(この場合ECMAScriptと呼ぶべきですが)に提案されているpipeline演算子についてまとめてみようと思います。
pipeline演算子の現状について
JSのpipeline演算子はまだ正式に導入されたわけではなく、いわゆるStage 1
と呼ばれる状態で、まだ仕様自体しっかり固まっていない状態です(Stageについてはこちらがわかりやすいです)。
そのため、将来的に本記事の内容とは違う文法・仕様になっている可能性もあるため、ご注意ください。
更に、pipeline演算子のproposalにも書かれているのですが、仕様にF# Pipelines
とSmart 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つの問題点があります。
- アロー関数にカッコが必須
-
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つの問題点を以下のように解決するようです。
- アロー関数にカッコを付けなかったときの扱いを定義
- 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 style
とBare 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 f
はTopic style
にも関わらず#
が使われていないためエラーとなり、n |> await f(#)
やn |> (await f)(#)
と書くことができるようになります。
式と変換後の対応表
ここを参考に、pipeline演算子を用いた式がBare style
とTopic 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 proposal
とSmart Pipelines
についてはBabelを用いてトランスパイルすることが可能なようです(@babel/plugin-proposal-pipeline-operator · Babel)(Try it out)。
所感
Smart Pipelines
の#
を使った独特な記法は最初は気持ち悪いかもしれませんが、この記事を書くために眺めていたら慣れました()
普段JSしか書いてないのでpipeline演算子がある言語を使ったことがなく旨味がまだちゃんとわかっていませんが、関数の結果を他の関数に渡すことはそれなりの頻度であるので、正式採用されれば便利なんだろうなぁとほんのりと感じました。
ただメソッドチェーンと変わらないpipelineはどこが便利なのか正直わかりません。