|>
) (このドキュメントでは、トピック参照のプレースホルダー トークンとして%
を使用します。これが最終的な選択肢になることはほぼ確実ではありません。詳細については、トークンのバイクシェディングの議論を参照してください。)
State of JS 2020 の調査では、「JavaScript に現在何が欠けていると感じますか?」という質問に対する回答が上位 4 位になりました。パイプ演算子でした。なぜ?
JavaScript で値に対して連続操作(関数呼び出しなど) を実行する場合、現在 2 つの基本的なスタイルがあります。
つまり、 three(two(one(value)))
とvalue.one().two().three()
の関係です。ただし、これらのスタイルは、読みやすさ、流暢さ、適用性の点で大きく異なります。
最初のスタイルであるnesting は一般的に適用可能で、関数呼び出し、算術演算、配列/オブジェクト リテラル、 await
とyield
など、あらゆる一連の操作に機能します。
ただし、ネストが深くなると読みにくくなります。実行フローは通常のコードの左から右の読み取りではなく、右から左に移動します。いくつかのレベルで複数の引数がある場合、読み取りは行ったり来たりすることさえあります。つまり、関数名を見つけるために目は左にジャンプし、追加の引数を見つけるために右にジャンプする必要があります。さらに、その後のコードの編集は困難を伴う可能性があります。多くのネストされた括弧の中で新しい引数を挿入するための正しい場所を見つけなければなりません。
React の実際のコードを考えてみましょう。
console . log (
chalk . dim (
`$ ${ Object . keys ( envars )
. map ( envar =>
` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
} ` ,
'node' ,
args . join ( ' ' ) ) ) ;
この現実世界のコードは、深くネストされた式で構成されています。データの流れを読み取るために、人間の目はまず次のことを行う必要があります。
初期データ(最も内側の式envars
) を見つけます。
次に、各データ変換を内側から外側に繰り返しスキャンします。各データ変換は、見逃しやすい左側の接頭辞演算子か、右側の接尾辞演算子のいずれかです。
Object.keys()
(左側)、.map()
(右側)、.join()
(右側)、chalk.dim()
(左側)、その後console.log()
(左側)。多くの式 (前置演算子を使用するもの、後置演算子を使用するもの、接接演算子を使用するもの) が深くネストされているため、各式の先頭を見つけるには左側と右側の両方をチェックする必要があります。
2 番目のスタイルであるメソッド チェーン は、値にそのクラスのメソッドとして指定された関数がある場合にのみ使用できます。これにより、その適用性が制限されます。しかし、これを適用すると、後置構造のおかげで、一般に使いやすくなり、読み書きが容易になります。コードの実行は左から右に流れます。深くネストされた式は解きほぐされます。関数呼び出しのすべての引数は、関数の名前でグループ化されます。また、後でコードを編集してさらにメソッド呼び出しを挿入または削除することは簡単です。カーソルを 1 つの場所に置き、連続する文字を 1 つ入力または削除するだけで済むからです。
実際、メソッド チェーンの利点は非常に魅力的であるため、一部の人気のあるライブラリでは、より多くのメソッド チェーンを可能にするために特にコード構造を変更しています。最も顕著な例はjQueryで、これは依然として世界で最も人気のある JS ライブラリです。 jQuery の中心的な設計は、数十のメソッドを含む単一の über オブジェクトであり、それらはすべて同じオブジェクト タイプを返すため、連鎖を続けることができます。このプログラミング スタイルには、 「流暢なインターフェイス」という名前もあります。
残念ながら、その流暢さにもかかわらず、メソッド チェーンだけでは JavaScript の他の構文 (関数呼び出し、算術演算、配列/オブジェクト リテラル、 await
およびyield
など) に対応することはできません。このように、メソッド チェーンの適用範囲は限られたままです。
パイプ演算子は、メソッド チェーンの利便性と容易性を、式のネストの幅広い適用性と組み合わせようとします。
すべてのパイプ演算子の一般的な構造は、 value |>
e1 |>
e2 |>
e3です。ここで、 e1 、 e2 、 e3はすべて、連続する値をパラメーターとして受け取る式です。 |>
演算子は、ある程度の魔法を実行して、 value
左側から右側に「パイプ」します。
この深くネストされた現実世界のコードを React から継続します。
console . log (
chalk . dim (
`$ ${ Object . keys ( envars )
. map ( envar =>
` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
} ` ,
'node' ,
args . join ( ' ' ) ) ) ;
…パイプ演算子と、前の操作の値の代わりとなるプレースホルダー トークン ( %
) を使用して、このようにもつれを解くことができます。
Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
| > `$ ${ % } `
| > chalk . dim ( % , 'node' , args . join ( ' ' ) )
| > console . log ( % ) ;
これで、人間の読者は初期データ(最も内側の式envars
であったもの) を迅速に見つけて、データの各変換を左から右に線形に読み取ることができます。
一時変数を使用することが、深くネストされたコードを解く唯一の方法であるべきだと主張する人もいるでしょう。すべてのステップの変数に明示的に名前を付けると、メソッド チェーンと同様のことが発生し、コードの読み取りと書き込みに同様の利点が得られます。
たとえば、React から以前に修正した実際の例を使用すると、次のようになります。
Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
| > `$ ${ % } `
| > chalk . dim ( % , 'node' , args . join ( ' ' ) )
| > console . log ( % ) ;
…一時変数を使用するバージョンは次のようになります。
const envarString = Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' ) ;
const consoleText = `$ ${ envarString } ` ;
const coloredConsoleText = chalk . dim ( consoleText , 'node' , args . join ( ' ' ) ) ;
console . log ( coloredConsoleText ) ;
しかし、現実の世界では、一時変数の行ではなく、お互いのコード内で深くネストされた式が常に発生するのには理由があります。そして、jQuery、Mocha などのメソッドチェーンベースの流暢なインターフェイスが依然として人気があるのには理由があります。
多くの場合、一時的な使い捨て変数の長いシーケンスを使用してコードを記述するのは、単純に退屈で冗長です。人間が読むのは間違いなく退屈で、視覚的に煩雑ですらあります。
名前付けがプログラミングで最も難しいタスクの 1 つである場合、プログラマは、変数の利点が比較的小さいと認識すると、必然的に変数に名前を付けることを避けます。
単一の可変変数を短い名前で使用すると、一時変数の冗長さが軽減され、パイプ演算子を使用した場合と同様の結果が得られると主張する人もいます。
たとえば、React から以前に修正した実際の例は、次のように書き直すことができます。
let _ ;
_ = Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' ) ;
_ = `$ ${ _ } ` ;
_ = chalk . dim ( _ , 'node' , args . join ( ' ' ) ) ;
_ = console . log ( _ ) ;
しかし、このようなコードは現実のコードでは一般的ではありません。その理由の 1 つは、可変変数が予期せず変更される可能性があり、見つけにくいサイレント バグを引き起こす可能性があることです。たとえば、変数がクロージャ内で誤って参照される可能性があります。または、式内で誤って再割り当てされた可能性があります。
// setup
function one ( ) { return 1 ; }
function double ( x ) { return x * 2 ; }
let _ ;
_ = one ( ) ; // _ is now 1.
_ = double ( _ ) ; // _ is now 2.
_ = Promise . resolve ( ) . then ( ( ) =>
// This does *not* print 2!
// It prints 1, because `_` is reassigned downstream.
console . log ( _ ) ) ;
// _ becomes 1 before the promise callback.
_ = one ( _ ) ;
この問題はパイプ演算子では発生しません。トピック トークンを再割り当てすることはできず、各ステップの外にあるコードはそのバインディングを変更できません。
let _ ;
_ = one ( )
| > double ( % )
| > Promise . resolve ( ) . then ( ( ) =>
// This prints 2, as intended.
console . log ( % ) ) ;
_ = one ( ) ;
このため、可変変数を含むコードも読みにくくなります。任意の時点で変数が何を表しているかを判断するには、その変数が再割り当てされている場所を直前のスコープ全体で検索する必要があります。
一方、パイプラインのトピック参照の語彙範囲は限られており、そのバインディングはその範囲内で不変です。誤って再割り当てすることはできず、クロージャで安全に使用できます。
トピック値もパイプライン ステップごとに変化しますが、パイプラインの前のステップをスキャンして意味を理解するだけで、コードが読みやすくなります。
代入ステートメントのシーケンス (可変または不変の一時変数を含む) に対するパイプ演算子のもう 1 つの利点は、それらが式であることです。
パイプ式は、直接返すことも、変数に割り当てることも、JSX 式などのコンテキストで使用することもできる式です。
一方、一時変数を使用するには、一連のステートメントが必要です。
パイプライン | 一時変数 |
---|---|
const envVarFormat = vars =>
Object . keys ( vars )
. map ( var => ` ${ var } = ${ vars [ var ] } ` )
. join ( ' ' )
| > chalk . dim ( % , 'node' , args . join ( ' ' ) ) ; | const envVarFormat = ( vars ) => {
let _ = Object . keys ( vars ) ;
_ = _ . map ( var => ` ${ var } = ${ vars [ var ] } ` ) ;
_ = _ . join ( ' ' ) ;
return chalk . dim ( _ , 'node' , args . join ( ' ' ) ) ;
} |
// This example uses JSX.
return (
< ul >
{
values
| > Object . keys ( % )
| > [ ... Array . from ( new Set ( % ) ) ]
| > % . map ( envar => (
< li onClick = {
( ) => doStuff ( values )
} > { envar } < / li >
) )
}
< / ul >
) ; | // This example uses JSX.
let _ = values ;
_ = Object . keys ( _ ) ;
_ = [ ... Array . from ( new Set ( _ ) ) ] ;
_ = _ . map ( envar => (
< li onClick = {
( ) => doStuff ( values )
} > { envar } < / li >
) ) ;
return (
< ul > { _ } < / ul >
) ; |
パイプ オペレーターには、Hack パイプと F# パイプという2 つの競合する提案がありました。 (その前に、最初の 2 つの提案の「スマート ミックス」に関する 3 番目の提案がありましたが、その構文は厳密に提案の 1 つのスーパーセットであるため、撤回されました。)
2 つのパイプ提案は、 |>
使用するときにコードを綴るときの「魔法」が何かという点でわずかに異なります。
どちらの提案も既存の言語概念を再利用しています。ハック パイプは式の概念に基づいており、F# パイプは単項関数の概念に基づいています。
それに応じて、式のパイプ処理と単項関数のパイプ処理には、小さくてほぼ対称的なトレードオフがあります。
Hack 言語のパイプ構文では、パイプの右側は特別なプレースホルダーを含む式であり、左側の式の評価結果にバインドされたプレースホルダーを使用して評価されます。つまり、 value |> one(%) |> two(%) |> three(%)
を記述して、3 つの関数を介してvalue
をパイプします。
長所:右辺には任意の式を指定でき、プレースホルダーは通常の変数識別子が配置できる場所であればどこにでも配置できるため、特別なルールを必要とせずに、必要なコードにパイプ処理できます。
value |> foo(%)
単項関数呼び出しの場合、value |> foo(1, %)
(n 項関数呼び出しの場合)、value |> %.foo()
メソッド呼び出しの場合、value |> % + 1
(算術演算の場合)、value |> [%, 0]
配列リテラルの場合、value |> {foo: %}
オブジェクトリテラルの場合、value |> `${%}`
、value |> new Foo(%)
オブジェクト構築用、value |> await %
約束を待っています。value |> (yield %)
生成ジェネレーターの値を生成します。value |> import(%)
関数のようなキーワードを呼び出すための短所:単項関数によるパイプ処理は、F# パイプよりも Hack パイプの方が若干冗長になります。これには、Ramda のような関数カリー化ライブラリによって作成された単項関数と、引数に対して複雑な構造化を実行する単項アロー関数が含まれます。ハック パイプは、明示的な関数呼び出しサフィックス(%)
を使用すると、もう少し冗長になります。
(式が進行すると、パイプ本体内で変数の代入/構造化を行うことができるため、トピック値の複雑な構造化が容易になります。)
F# 言語のパイプ構文では、パイプの右側は単項関数として評価される必要がある式であり、単項関数は左側の値を唯一の引数として暗黙のうちに呼び出されます。つまり、 value |> one |> two |> three
を記述して、 value
3 つの関数にパイプします。 left |> right
right(left)
になります。これは、暗黙のプログラミングまたはポイントフリー スタイルと呼ばれます。
たとえば、React から以前に修正した実際の例を使用すると、次のようになります。
Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
| > `$ ${ % } `
| > chalk . dim ( % , 'node' , args . join ( ' ' ) )
| > console . log ( % ) ;
…Hack パイプの代わりに F# パイプを使用するバージョンは次のようになります。
Object . keys ( envars )
. map ( envar => ` ${ envar } = ${ envars [ envar ] } ` )
. join ( ' ' )
| > x => `$ ${ x } `
| > x => chalk . dim ( x , 'node' , args . join ( ' ' ) )
| > console . log ;
長所:右辺が単項関数に解決されなければならないという制限により、実行したい操作が単項関数呼び出しである場合に非常に簡潔なパイプを書くことができます。
value |> foo
(単項関数呼び出しの場合)。これには、Ramda などの関数カリー化ライブラリによって作成された単項関数や、引数に対して複雑な構造化を実行する単項アロー関数が含まれます。F# パイプは、暗黙的な関数呼び出し (no (%)
) を使用すると冗長性がわずかに低下します。
短所:この制限は、他の構文で実行される操作は、操作を単項アロー関数でラップすることによって、少し冗長にする必要があることを意味します。
value |> x=> x.foo()
メソッド呼び出しの場合、value |> x=> x + 1
(算術演算の場合)、value |> x=> [x, 0]
配列リテラルの場合、value |> x=> ({foo: x})
オブジェクトリテラルの場合、value |> x=> `${x}`
、value |> x=> new Foo(x)
、value |> x=> import(x)
は関数のようなキーワードを呼び出します。名前付き関数を呼び出す場合でも、複数の引数を渡す必要がある場合はラップする必要があります。
value |> x=> foo(1, x)
(n 項関数呼び出しの場合)。短所: await
操作とyield
操作は、それらを含む functionにスコープが設定されているため、単項関数だけでは処理できません。これらをパイプ式に統合したい場合は、 await
とyield
特別な構文の場合として処理する必要があります。
value |> await
、value |> yield
ジェネレータ値を生成します。Hack パイプと F# パイプはそれぞれ、異なる式に小さな構文税を課します。
パイプをハックすると、単項関数呼び出しのみにわずかな負担がかかります。
F# パイプでは、単項関数呼び出しを除くすべての式にわずかに負担がかかります。
どちらの提案でも、課税される式ごとの構文税は小さいです( (%)
とx=>
は両方とも 3 文字のみです)。ただし、税金には、それぞれの課税対象となる表現の普及率が乗算されます。したがって、一般的ではない表現に税金を課し、より一般的である表現を優先して最適化することが理にかなっている可能性があります。
単項関数呼び出しは、一般に、単項関数を除くすべての式よりも一般的ではありません。特に、メソッド呼び出しとn 項関数呼び出しは常に人気があります。一般に、単項関数の呼び出しは、配列リテラル、オブジェクト リテラル、算術演算などの他のユビキタスな構文はもちろんのこと、これら 2 つの場合だけでも同等かそれを超えています。この説明には、この有病率の違いを示す実際の例がいくつか含まれています。
さらに、拡張呼び出し、 do 式、レコード/タプル リテラルなど、提案されている他のいくつかの新しい構文も、将来的に普及する可能性があります。同様に、TC39 が演算子のオーバーロードを標準化すれば、算術演算もさらに一般的になるでしょう。これらの将来の構文の式のもつれを解くには、F# パイプと比較して Hack パイプを使用した方がスムーズです。
単項関数呼び出しでの Hack パイプの構文 (つまり、右側の単項関数を呼び出す(%)
) は特殊なケースではありません。パイプを使用しない通常の方法で、通常のコードを明示的に記述しているだけです。
一方、 F# パイプでは、「単項関数に解決されるコード」と「その他の式」を区別する必要があり、後者の場合にはアロー関数ラッパーを忘れずに追加する必要があります。
たとえば、Hack パイプでは、 value |> someFunction + 1
は無効な構文であり、早期に失敗します。 someFunction + 1
単項関数として評価されないことを認識する必要はありません。ただし、F# パイプでは、 value |> someFunction + 1
は依然として有効な構文です。 someFunction + 1
呼び出し可能ではないため、実行時に遅く失敗するだけです。
パイプ チャンピオン グループは、ステージ 2 用の F# パイプを TC39 に2 回贈呈しました。 2回ともステージ2進出はならなかった。両方の F# パイプ (および部分関数アプリケーション (PFA)) は、さまざまな懸念により、他の複数の TC39 代表者からの強い反発に直面しています。これらには次のものが含まれます。
await
に関する構文の問題。この反発はパイプチャンピオングループの外側から起きた。詳細については、HISTORY.md を参照してください。
名前付き変数に頼らずに深くネストされた式を簡単に線形化するには、パイプ演算子は何もないよりは良い、というのがパイプチャンピオングループの信念です。チャンピオン グループのメンバーの多くは、Hack パイプが F# パイプよりわずかに優れていると信じており、チャンピオン グループの一部のメンバーは、F# パイプが Hack パイプよりもわずかに優れていると考えています。しかし、チャンピオングループの全員が、F# パイプが予見可能な将来に TC39 に合格するにはあまりにも大きな抵抗に直面していることに同意しています。
強調しておきますが、Hack パイプから F# パイプに戻そうとすると、TC39 がどのパイプにもまったく同意しない可能性が高くなります。 PFA 構文も同様に、TC39 で困難な戦いに直面しています (HISTORY.md を参照)。パイプ チャンピオン グループの多くのメンバーは、これは残念なことだと考えており、後でF# パイプ スプリット ミックスと PFA 構文を求めて再び戦うつもりです。しかし、パイプ チャンピオン グループ以外にも、ハック パイプに関係なく、暗黙のプログラミング (および PFA 構文) の奨励に一般的に反対している代表者 (ブラウザ エンジンの実装者を含む) がかなりの数います。
(正式な仕様草案が入手可能です。)
トピック参照%
null 項演算子です。これはトピック値のプレースホルダーとして機能し、語彙的にスコープが設定され、不変です。
%
最終的な選択ではありません(トピック参照の正確なトークンはFinal ではありません。 %
は、代わりに^
や他の多くのトークンになる可能性があります。ステージ 3 に進む前に、実際に使用するトークンをバイクシェッドする予定です。ただし、 %
構文的に最も問題が少ないと思われます。これは、 printf フォーマット文字列やClojureの#(%)
関数リテラルのプレースホルダーにも似ています。)
パイプ演算子|>
は、パイプ式(パイプラインとも呼ばれます) を形成する中置演算子です。これは左側 (パイプヘッドまたはパイプ入力) を評価し、結果の値 (トピック値) をトピック参照に不変にバインドし、そのバインディングを使用して右側 (パイプ本体) を評価します。右側の結果の値は、パイプ式全体の最終値 (パイプ出力) になります。
パイプ演算子の優先順位は次と同じです。
=>
;=
、 +=
など。yield
とyield *
;これは、カンマ演算子,
のみを使用するよりも厳密です。
他のすべての演算子よりも緩いです。
たとえば、 v => v |> % == null |> foo(%, 0)
v => (v |> (% == null) |> foo(%, 0))
にグループ化されます。
これは、 v => foo(v == null, 0)
と同等です。
パイプ本体はトピック値を少なくとも 1 回使用する必要があります。たとえば、 value |> foo + 1
は、本体にトピック参照が含まれていないため、無効な構文です。この設計は、パイプ式の本体からのトピック参照の省略は、ほぼ間違いなく偶発的なプログラマ エラーであるためです。
同様に、トピック参照はパイプ本体に含まれている必要があります。パイプ本体の外でトピック参照を使用することも無効な構文です。
混乱を招くグループ化を防ぐため、同様の優先順位を持つ他の演算子 (つまり、 arrow =>
、三項条件演算子?
:
、代入演算子、およびyield
演算子) をパイプの head または bodyとして使用することは無効な構文です。これらの演算子とともに|>
使用する場合は、括弧を使用してどのグループ化が正しいかを明示的に示す必要があります。たとえば、 a |> b ? % : c |> %.d
は無効な構文です。 a |> (b ? % : c) |> %.d
またはa |> (b ? % : c |> %.d)
に修正する必要があります。
最後に、動的にコンパイルされたコード ( eval
またはnew Function
など) 内のトピック バインディングは、そのコードの外部では使用できません。たとえば、 v |> eval('% + 1')
実行時にeval
式が評価されるときに構文エラーをスローします。
他に特別なルールはありません。
これらのルールの自然な結果として、パイプ処理されるデータを変更せずにパイプ式のチェーンの途中に副作用を挿入する必要がある場合は、 value |> (sideEffect(), %)
などのコンマ式を使用できます) ということになります。 value |> (sideEffect(), %)
。通常どおり、コンマ式は右側の%
に評価され、基本的にトピック値を変更せずに渡します。これは、素早いデバッグに特に役立ちます: value |> (console.log(%), %)
。
元の例に対する唯一の変更は、デデントとコメントの削除でした。
jquery/build/tasks/sourceMap.js から:
// Status quo
var minLoc = Object . keys ( grunt . config ( "uglify.all.files" ) ) [ 0 ] ;
// With pipes
var minLoc = grunt . config ( 'uglify.all.files' ) | > Object . keys ( % ) [ 0 ] ;
node/deps/npm/lib/unpublish.js から:
// Status quo
const json = await npmFetch . json ( npa ( pkgs [ 0 ] ) . escapedName , opts ) ;
// With pipes
const json = pkgs [ 0 ] | > npa ( % ) . escapedName | > await npmFetch . json ( % , opts ) ;
underscore.js から:
// Status quo
return filter ( obj , negate ( cb ( predicate ) ) , context ) ;
// With pipes
return cb ( predicate ) | > _ . negate ( % ) | > _ . filter ( obj , % , context ) ;
ramda.js より。