tc39/proposal-pipeline-operator の過去と現状

pipeline-operator の ドラフトを振り返る

tc39/proposal-pipeline-operatorリポジトリができた時点の, (厳密には Stage-1 になった時点) pipeline operator の syntax を簡単におさらいしてみましょう.

let result = exclaim(capitalize(doubleSay("hello")));
result //=> "Hello, hello!"

let result = "hello"
  |> doubleSay
  |> capitalize
  |> exclaim

F# や Elixir などに触れているひとは,馴染み深いものかもしれません.その他の,関数型プログラミング言語では,記法や振る舞いに若干の差異はあるものの,類似の pipeline があると思います.

c(b(a)) のような関数呼び出しがネストしたケースで,a |> b |> c と,より直感的に書けることができるようになります.

この仕様としては,このオペレーターは,優先度は , より強く,その他のオペレーターより弱いです.そして,Unary-Function 前提で考えられていました. 上記のような pipeline は,これより F#-style pipeline として扱われていきます.

長い期間 Stage-1 にとどまり続けた理由

pipeline-operator は 2017年に Stage-1 になってから 三年間 Stage-2 に上がることはありませんでした.この記事では,なぜ上がるのが難しかったかに焦点を当てて紹介していきます.

F#-style pipeline における ASI との競合

非同期を pipeline で扱いたいモチベーションから ASI で起きた予期せぬ問題までの流れを コードと順に追っていきます.

function doubleSay (str) {
  return str + ", " + str;
}
async function capitalize (str) {
  let response = await
      capitalizeOnTheServer(str);
  return response.getCapitalized();
}
function exclaim (str) {
  return str + '!';
}

let result = "hello"
  |> doubleSay
  |> capitalize
  |> exclaim;

// result => "[Object Promise]"

普通に pipeline を活用すると, 途中に promise を返す処理があった場合, その promise が解決されずに次の処理に渡されてしまいます. これを解決しようとすると,

let result = await ("hello"
    |> doubleSay
    |> capitalize)
  |> exclaim

この様になってしまいます.しかし,これはあまり直感的ではないです それを解決するために pipeline 下での await を許すとこのように書けるようになります.

let result = "hello"
  |> doubleSay
  |> await capitalize
  |> exclaim;

ここで新たに生じた問題が,上のコードと下のコードで全く別の意味になってしまうということです.

let result = "hello"
  |> doubleSay
  |> (await capitalize)
  |> exclaim;

その問題を回避するために,|> await という構文が提案されました.

let result = "hello"
  |> doubleSay
  |> await
  |> capitalize
  |> exclaim;

|> await という構文を導入しようとしたときに ASI の問題が生じました

let promise = "hello"
  |> doubleSay
  |> await
exclaim(capitalize(promise))

既存の async / await では,await 前の改行が許されており,

await
  asyncFn()

と記述することができます. ここから,

let promise = "hello"
  |> doubleSay
  |> await;
exclaim(capitalize(promise))
let promise = "hello"
  |> doubleSay
  |> await
exclaim(capitalize(promise));

上記のどちらと判断すればよいかわからず,ASI がどこにセミコロンを挿入すべきか判断がつかなくなってしまうという問題が起きました. それらを回避するために await |>|> await |> なども考えられましたが,(|> await で終わる pipeline を許さないための構文) 結果的に,これを解決する画期的な方法がなく,仕様としては,await を含むパターンを後回しにし,それが要因の一つとなり,Stage-2 へ上げる判断はなされませんでした.

await の問題を解決するために,過去に drop された hack style の復活が提案される

pipeline operator 下で,await を扱う良い方法がない現状を打破するために,2015年 に考えられていた hack style復活させる提案がされました.それを受けて,2018年に F# style と hack style を混合させた提案が2つできました.それは, "split-mix pipes""smart-mix pipes" (proposal-smart-pipeline) です.

2つに共通している点は,Placeholder定義し,その token を活用することで,await, yield含んだケースを対応しつつ, 前の処理で評価した結果を次の処理の関数の任意の場所に適用できるというものでした.

split-mix|>|>> (厳密には,どの文字を使うかまでは決まっていない) 2つのオペレーターに分割しているのが特徴です.smart-mix は 関数をそのまま渡すか,Placeholder によってどこに適用するか,文法を限定するというものでした

  • split-mix
// Basic Usage
x |>> f     //-->   f(x)
x |> f(^)  //-->   f(x)

x |>> f(y)     //-->   f(y)(x)
x |> f(y)(^)  //-->   Syntax Error

// 2+ Arity Usage
x |> f(^, 10)   //-->  f(x,10)

// Async solution (does not require special casing)
x |> await f(^)       //-->  await f(x)
x |> await f(^) |> g  //-->  g(await f(x))

// Other expressions
f(x) |> ^.data           //-->  f(x).data
f(x) |> ^[^.length-1]    //-->  let temp=f(x), temp[temp.length-1]
f(x) |> { result: ^ }    //-->  { result: f(x) }

split-mix は,(導入されたときに)各ブラウザが,2つのオペレーターを実装する必要があることから,これに賛同する人はいませんでした.

  • smart-mix
// Basic Usage
x |> f     //-->   f(x)
x |> f(^)  //-->   f(x)

// 2+ Arity Usage
x |> f(y)     //-->   Syntax Error
x |> f(y, ^)  //-->   f(y, x)
x |> f(^, y)  //-->   f(x, y)
x |> f(y)(^)  //-->   f(y)(x)

// Async Solution (Note this would not require special casing)
x |> await f(^)       //-->  await f(x)
x |> await f(^) |> g  //-->  g(await f(x))

// Arbitrary Expressions
f(x) |> ^.data           //-->  f(x).data
f(x) |> ^[^.length-1]    //-->  let temp=f(x), temp[temp.length-1]
f(x) |> { result: ^ }    //-->  { result: f(x) }

pipeline-operator を Stage-2 に上げるために再挑戦

2018年3月に,pipeline-operator を Stage-2 に上げるために F# pipeline と smart-mix pipeline のそれぞれを一緒に再度提案し tc39 に持ち込みました.しかし,どちらの提案も賛同を得ることはなく,一部からは構文上の課題からこれを標準化する価値はないオペレーターだと述べられました.

2018年7月には,pipeline-operator にも 関係がある partial-function-application (PFA) を Stage-2 に進めるべく提案されました. これに関しても,一部から反対され続けられています. Google V8 チームからは,開発者が PFA を通して 容易にたくさんのクロージャ定義することでメモリが逼迫することを懸念していました.

これにより,PFA が stage-2 に進めないこともあって,pipeline-operator の Stage は上がることはありませんでした.

@codehag 氏(Mozilla SpiderMonkey チーム)は,pipe に関するユーザー調査を主導していました. その結果,smart-mix pipeline が若干好まれているようでした.しかし,F# pipeline の方が(実装時の)エラーが少なかったようです.結論としては,この結果はどちらが優位であるかを決めるほどの優位な差はないというものでした.これらを総合的に踏まえて,Mozilla SpiderMonkey チーム はそこまで,pipeline operator の二つの提案に対して前向きではありませんでした.

ここから三年間に渡り,これら二つの pipeline operater が 同意を得られるための最善について オフライン / オンラインで議論されます.

2021年 pipeline-operator が Stage-2 に上がるにあたって起きたこと

State of JS 2020 にて pipeline operator が話題に上がる

2021年1月に State of JS 2020 にて,「最も JavaScript に足りないものはなにか」 という設問において pipeline operator が四位にランクインしたという報告を受けました.

これがきっかけの1つとなったかどうかはわかりませんが,結果として三年間大きな動きがなかったこの proposal に動きが出てきました.

state-of-js-2020 results

https://2020.stateofjs.com/ja-JP/opinions/ より

2021年3月に,これまで チャンピオンを努めていた @littledan 氏 (Igalia) が,他の project で時間が割けない状況であるということから, この proposal のチャンピオンが @tabatkins 氏(Google), @rbuckton 氏 (Microsoft) が 共同チャンピオンとして共に引き継ぐことに同意しました.

また,@rbuckton (Microsoft) 氏 が これまで上がっている F#-style pipeline, hack-style pipeline, smart-mix pipeline, 三つを比較した結果を Gist にまとめ「ほんの些細な違い」と結論づけています. この,Gist での議論もとても白熱しています.

この,Gist に刺激を受けた smart-mix pipeline のドラフトを書いている @js-choi 氏 (Indiana University)は,smart-mix pipeline のドラフトではなく, Hack-style pipeline を書くことにシフトしました.それにより,smart-mix pipeline は Hack-style pipeline と併合された扱いになり 取り下げられました.

再度 Stage-2 に向けて

再度,Stage-2 に向けていくつかの pipeline-operateor の style を提示に向けて議論が行われていました.その結果,Hack-style pipeline を持ち込む事になりました. それにあたり,F#-style がいつまでも行き詰まっていることから,Hack-style pipeline に仮合意することなりました.

8月31日の正式な委員会で,プレナリーが実施されました.@tabatkins 氏(Google)が Hack-style pipeline を現状のチャンピオンの暫定的ななコンセンサスとして提示し,Stage-2 に進めることを提案しました.

それに対して,

  • 他の proposal である bind-operator と将来的に衝突してしまうのではないか
  • @codehag 氏 (Mozilla SpiderMonkey) はいずれにしても pipeline には若干後ろ向き,ただし Stage-2 をブロックするほどではない

という指摘がなされました.

これ以外に,特に指摘もなかったため pipeline-opearator は Stage-2 に進むことになりました.

備考として,以前 Google V8 チームから挙げられていた,pipe と PFA を用いたときに,メモリを逼迫する可能性についての異論はありませんでした.

仕様を固めるために Placeholder の トークンを決める

2021年10月末から議論されているトピックとして,Placeholder の token に何を用いるかというものがあります.

以前は ? が提案されていました, 現在 Bikeshedding the Hack topic token #91 にて 最終的に,Placeholder の token に何を用いるか議論されています.

? が抱えている問題

? token としたときに次のようなコードになります.

value |> fn(10, ?)

これを三項演算子 や Nullish coalescing operator と組み合わせたときに,非常に混乱を招くコードになってしまう問題があります

value |> fn(10, ? ? cond1(?) : defaultValue)
value |> fn(10, ? ?? defaultValue)

また,これを許すと,構文的には下のコードがあり得ることになります

value |> fn(10, ? ?? ? ? ? : ?)
                ~ ~~ ~ ~ ~ ~ ~
                |  |_|_|_|_|_|_______ nullish coalescing operator
                |    | | | | |
                |____._|_._|_.____ placeholder of pipeline
                       |   |
                       |___.___ conditional ternary operator

% が抱えている問題

%? に近い課題を抱えています.% 単体で オペレーターとしてすでにあるので

value |> fn(20 % %)

のようなコードが発生してしまいます,これを文脈から pipline-operator 下の placeholder か,剰余を求める % operator か どうか判断するのは非常に困難です.# でも同様に,クラスのメソッド内で pipeline が使われたとき,private field か placeholder の token か,あるいは 現在上がっている proposal の tuple か 非常に解析が難しくなってしまいます.

これらのことから,独立した token が良いのではないかという意見もあります.

##(), $_, @, ^ など 様々なアイデアが github 上 の issue で 議論されています.そして,まだこれといった token は決まっていません.$, _ 及びそれらの組み合わせは,既存のコードを破壊する可能性があるのでなさそうです)

Stage-3 の条件としては,仕様として完成している必要があるので,まずここが決まらないと pipeline-operator の Stage-3 は難しいでしょう.

現状の Hack-style pipeline の仕様

最後に,これらを経てどのような proposal になったか見てみましょう. Plfaceholder が 仮に % になれば以下のようになります

  • value |> foo(%) for unary function calls,
  • value |> foo(1, %) for n-ary function calls,
  • value |> %.foo() for method calls,
  • value |> % + 1 for arithmetic,
  • value |> [%, 0] for array literals,
  • value |> {foo: %} for object literals,
  • value |> \`${%}\`` for template literals,
  • value |> new Foo(%) for constructing objects,
  • value |> await % for awaiting promises,
  • value |> (yield %) for yielding generator values,
  • value |> import(%) for calling function-like keywords,

おわりに

この記事では,pipeline-operator が Stage-1 から Stage-2 に上がるまでを紹介しました.

東京Node学園祭'2018 にて,tc39 に所属している daniel さんが来日し,発表+参加者とのディスカッションに参加したときに 漠然と,「JavaScript に新しい syntax を導入するのは難しいのだなあ」と感じていたました. 今回,この記事を書くにあたって pipeline-operator について調べ,より強くその難しさを実感できたような気がします.

pipeline-operator や partial-function-application, pattern-matching, bind-operator などの仕様が JavaScript に実装されたときには,とても楽しくプログラミングができそうで楽しみです.

参考文献