JavaScript
ECMAScript
esnext
Pipeline

ES Nextのパイプライン演算子ってどうなの?

const plasticModel = `

┏━━━┳━━━━━━━┳━━┓
┣ヽ  ̄ / (・ω・)┫
┣━━━━━━╋━━━━╋━━┫
┣、ハ,,、 \(. \ ノ┫ズコープラモ
┗┻━━━━━┻━━━━┻━━┛
`
|> functions[カット]
|> functions[組み立て]
|> functions[設置]

plasticModel |> console.log
//         ∧∧
//        ヽ(・ω・)/   ズコー
//     \(.\ ノ
//    、ハ,,、  ̄
//     ̄

ES.nextで定義されている「the pipeline operator」(和訳: パイプライン演算子)を検討してみました。

2018/4/8現在、ようやくStage1に登った所でまだまだ先は長いって感じですね。

the pipeline operator - Node.js ESNEXT Support

JSはES2015でモダンな新構文が押し寄せてきましたので、

既存のコードは押し流されてしまい、まるでレガシー扱いされてしまう程になりましたね。

実はES Nextに含まれているパイプライン演算子も非常に強力で、

既存のコード全てをレガシー扱いにして押し流す程のパワーを秘めていると言っても過言ではありません。

予習しておくことに越したことはないでしょう。

※結論から言うとネイティブで使う分にはダメそうですが、Ramda.jsやawaitとの併用で本当に世界が変わる程の力があると考えました。

※募集: もしES Nextで関数が充実するからRamda.jsとか不要だよという分かる方がいたら教えてください。


パイプライン演算子とはなんぞや?

詳細はES Next用の提案サイトを御覧ください。

https://github.com/tc39/proposal-pipeline-operator

では実際にコードを動かしてみましょうか。

サイトに書いてあるコードはなんか古いのでES6準拠にしてっと…


es6.js

const doubleSay = str => str + ', ' + str

const capitalize = str => str[0].toUpperCase() + str.substring(1)
const exclaim = str => str + '!'

console.log(exclaim(capitalize(doubleSay("hello"))))
// "Hello, hello!"


な、ななな何よ!?この括弧の数は!!

こんな糞コードはたたっ斬って、リファクタリングしてやるんだから!!

まぁ、慌てないでください。

ES Nextのパイプライン演算時が実装されたらこうなります。


esnext.js

const doubleSay = str => str + ', ' + str

const capitalize = str => str[0].toUpperCase() + str.substring(1)
const exclaim = str => str + '!'

"hello" |> doubleSay |> capitalize |> exclaim |> console.log
// "Hello, hello!"


括弧がなくなってスッキリしたわね。

でもイマイチ何やってるか分からないわ。

はい、解説していきます。

パイプライン演算子は値 |> 関数 |> 関数 |> 関数という書式で利用します。

まず、一番左の値が右隣の関数に入り、doubleSay("hello")になります。

戻り値が次の右隣の関数に滑り込み、戻り値を元に更に次の関数を実行という事を繰り返します。


  • "hello" |> doubleSay |> capitalize |> exclaim |> console.log

  • "hello, hello" |> capitalize |> exclaim |> console.log

  • "Hello, hello" |> exclaim |> console.log

  • "Hello, hello!" |> console.log

  • Undefined ("Hello, hello!"を標準出力)

今回は最終的にconsole.logに滑り込んで標準出力されました。

console.logの戻り値は常にundefinedなので式全体はundefinedになります。

値を変数に取りたければ下記のようにしてください。


esnext-dash.js

const doubleSay = str => `${str}, ${str}`

const capitalize = str => str[0].toUpperCase() + str.substring(1)
const exclaim = str => `${str}!`

const hello = "hello" |> doubleSay |> capitalize |> exclaim

console.log(hello) // "Hello, hello!"
hello |> console.log // "Hello, hello!"


Lodashからtap関数を輸入してきても良いですね。

tapは(関数 -> 値)の順で引数を取る関数で、引数の値でコールバック関数を実行させるが、戻り値は捨てるのでconsole.logみたいな処理特化の関数と相性が良いんです。


esnext-tap.js

const curry = (fn, ...args) => (args.length >= fn.length) ? fn(...args) : curry.bind(null, fn, ...args)

const tap = curry((fn, val) => { fn(val); return val })
const doubleSay = str => `${str}, ${str}`
const capitalize = str => str[0].toUpperCase() + str.substring(1)
const exclaim = str => `${str}!`

const hello = "hello" |> doubleSay |> capitalize |> exclaim |> tap(console.log)
// "Hello, hello!"

console.log(hello) // "Hello, hello!"


なぜfor文は禁止なのか?ポエム版で書かれているように、

メソッドチェーンと同じく状態変数を減らす良い手段として役に立つことでしょう。

また、パイプライン演算子は縦に並べる事も可能です。

アロー関数とセットにすればこのように関数も変数も宣言せずに値を加工していけます。


esnext-lambda.js

"hello"

|> (str => `${str}, ${str}`)
|> (str => str[0].toUpperCase() + str.substring(1))
|> (str => `${str}!`)
|> console.log
// "Hello, hello!"

強力ですがあまりに数珠つなぎにし過ぎるのも考えものです。

5個程度を目安にするようにしてみてください。


要するに、何が便利になるの?

まぁ使い捨ての関数、状態変数は減りますよね。

他にも行数を跨ぐ括弧を減らしたり、インデントのネストを回避したり…あれ?

でもJavaScriptって関数と呼べる関数って殆ど無かったような……

ちょっと調べてみましょう。

関数 - MDNによるとわずか12個!

その内訳も絶対使うなと口を酸っぱくして言われるevalに非標準が1個、非推奨が2個……

関数が(8個しか)入ってないやん。どうしてくれんのこれ(憤怒)

他の言語ならパイプライン演算子を活かす為に大量に関数が存在するのですが、

ネイティブのJSだと全部アロー関数で作る事にならない?

ちょっと私の作ったポエムの例題から1問持ってきて解いてみましょう


lambda.js

// 0から100未満の偶数のみを累計する。

const main = () =>
Array(100)
|> (its => its.fill(0))
|> (its => its.map((_, i) => i))
|> (its => its.filter(it => it % 2 === 0))
|> (its => its.reduce((a, b) => a + b, 0))
main() |> console.log // ほぼかくじつに2450

あのさぁ…

JavaScriptはバリバリのオブジェクト指向言語ですから、

既に用意されているメソッドチェーンと用途が用途が被ってます。

こんなもん実装するくらいなら、全部の型にtapthruのプロトタイプメソッド作って下さい。

そっちの方が遥かに有用で使い勝手が良いと思います。


実際の利用シーン

ネイティブではちょっとどうしようもないので、

Ramda.jsと共闘する感じになりそうですね。


ramda+lambda.js

// 0から100未満の偶数のみを累計する。

const R = require('ramda')
const main = () =>
R.range(0, 100)
|> R.filter(R.pipe(R.modulo(R.__, 2), R.equals(0)))
|> R.sum
main() |> console.log // たぶん2450

これだよこれ!私の求めていた挙動そのものです!

Lodashのchainのように.value()で計算開始する必要も無いですし、

ポエムの回答と比べても屈指の出来だと思います。

もしも関数が増えたりすることなく、ぽんとパイプライン演算子だけやってきたら

きっとRamda.js専用構文としてやっていく感じになるのだと思います。

既存の構文押し流すどころか一部の変態が喜んで終わりですね

他にも手作業で作ったJSONやYAMLファイルの評価に使うようなケースでは結構良いかもしれませんね。

何百行の超大作YAMLを作るとたまにパースエラーで死にますからね……


yaml+lambda.js

const YAML = require('yamljs')

`
hoge: 123
piko: 234
`

|> YAML.parse
|> console.log

頭とお尻にこんな感じでおまじない付けて、node hoge.yamlって叩けばファイルをチェックしてくれます。

LiveScript使ってよくやってますが、わりと便利で重宝してます。


これは非常に良さそうと思った箇所


  • ?で部分適用に対応

  • awaitとセットで使える


?で部分適用に対応

ぱっと思いつくのがJSON化してconsole.log出力ですか。

これらの機能は引数を足せば一気に便利になるんですが、この一手間がかなり面倒なんですよね……

パイプライン演算子の紹介ページの下の方で書いてある?を使えば、

パイプライン演算子により注入される引数の場所をコントロール出来るようです。


stringify.js

// 普通に繋いだ場合

{hoge: 123, piko: 234}
|> JSON.stringify
|> console.log
// "{"hoge":123,"piko":234}"

// 部分適用を使ってcute出力
{hoge: 123, piko: 234}
|> JSON.stringify(?, null, 2)
|> console.log('json:', ?)
// "json:" "{
// "hoge": 123,
// "piko": 234
// }"


なにこれ便利!

Node.jsならfs.writeFileSync(?, filename)とか出来るからファイル出力も捗りますね。

この?ってめっちゃ便利なので単体で欲しいんですが何とかなりませんか?

…と思ってたら3歳娘「パパ、関数をカリー化して?」のコメント欄で教えてもらいました。


https://github.com/tc39/proposal-partial-application

まだStage1ですが、すでにBabel7.4.0でサポートされたそうです。


const add = (a, b) => a + b;

const addOne = add(1, ?);
console.log(addOne(2)); // 3

単純に別提案だからパイプライン演算子のページには無かったんですね。

ページ内ではパイプライン演算子の例もセットで載っており、相性の良さをアピールしています。

じゃあパイプライン演算子のメリットじゃないじゃん!!

これなら全ての型にthruとtapをつけろ!(まだ言うか

ま、まぁ相性の良いセットの使い方という事でよしとしましょう。


awaitとセットで使える

これ期待してませんでしたが、ガチの目玉機能に化けそうです。

例えばNode.jsでmysql2パッケージはPromiseを使ったかっこいい書き方が出来て…

https://github.com/sidorares/node-mysql2/blob/master/examples/promise-co-await/await.js

それを応用して、非同期・同期処理を一気にパイプライン演算子で片付けるという事ができそうです。

それではユーザーの一覧をMySQLから抽出、

優秀なスコアのユーザーだけ取り出して名前を標準出力に渡す。

…という想定でコードを書いてみます。


mysql.js

const R = require('ramda')

const mysql = require('mysql2/promise')
const options = {
port: 3306,
user: 'testuser',
namedPlaceholders: true,
password: 'testpassword'
}
const getUsers = async c =>
'select * from users'
|> await c.query // resolve(rows, fields)だがawaitで受け取ると第一引数のみ返ってくる
const main = async () => {
options
|> await mysql.createConnection
|> await getUsers
|> R.filter(R.pipe(R.prop('socore'), R.lte(80)))
|> R.map(R.prop('name'))
|> console.log
}
main()
// ['taro', 'jiro', 'saburo'] <- が出力される予定

おぉ……何となく行けそうですね。

実務でも実用レベルで応えてくれそうです。

同期・非同期処理をひっくるめてポイントフリースタイルで書けるって凄くないですか?

(関数が一切増えない前提であれば)Ramda.jsがないと糞の役にも立たないですが、関数さえ用意されていれば万能感やばいです。


おまけ: 分からなかった所

bind関係が分かりませんでした。

実装されたら試してみたいです。


bind.js

class Animal {

constructor (name) {
this.name = name
}
say (cry) {
return `${(this || {}).name}: ${cry}`
}
}
const cat = new Animal('tama')

'meu'
|> cat.say(?)
|> console.log
// 理想 → tama: meu
// 現実?→ undefined: meu


例えばcat.say自体をどっかに代入してしまったり、引数として保存してしまうと、

thisとの束縛が途絶えるので、パイプライン演算子の中でundefined: meuと捨て猫になってしまう可能性があります。

説明文やBabelの実装を見る限りでは、

LISPよろしくその場でバンバン括弧で包むだけのシンプルな対応になりそうなので、

多分気にする必要はないと思います。

因みに下記のようにすると…


tama.js

class Animal {

constructor (name) {
this.name = name
}
say (cry) {
return `${(this || {}).name}: ${cry}`
}
}
const cat = new Animal('tama')
const say = cat.say
say('meu') // undefined: meu

にゃーん

|>?を使ってこのcat.sayの評価が遅延出来るなら最高ですね。

無理なら無理でbindで実現出来ますからそこまで困りはしないですが気になりますね。


おまけ2: もう待てないんだけど、何時になったら使えるの?

冒頭でも触れましたが、まだStage1ですからねぇ…

しかも2018年になってようやくStage0から1に格上げされたばかりです。

予定通り行ったとしてもChromeやNode.jsで実際に使えるようになるのは2020年頃だと思います。

もしかすると2022年くらいまで待たされる可能性もあります。

そんなに待てない!やだやだ思って調べた所、Babelのproposalsを見ると既に使えるみたいですね。

babel/proposals/issues/29 - GitHub

必要な根回し全て終わって、プルリクも全てmasterブランチに向けてのmergedになってませんか?

あれ、ホントに使えそう……

では早速、try it out - Babelで試してみましょう。

長い事BabelのTryページは6系が使われており、つい先日(2019年2月)にようやく7系に以降されました。

それによりPipelineのオプションも左カラムに設置され、ようやく試せるようになりました!

下記のようにペチペチっと打ち込んで……

123 |> it => it + 2

// /repl: Unexpected token, expected ";" (1:10)
// > 1 | 123 |> it => it + 2
// | ^

はぁ????

どうもアロー関数をそのまま作って投げ込むという事は出来ないようです。

括弧で包んでも同じくエラー……

仕方ないので変数に一度代入して、いけました。

と思ったらなんじゃこの結果は?

const add2 = it => it + 2

123 |> add2

var _;

const add2 = it => it + 2;

_ = 123, add2(_);

大分気持ち悪いですね。

まぁいいです。目玉機能の部分適用さえ使えればなんでも。

では見せてもらおうか、パイプライン演算子の性能とやらを……

const add2 = it => it + 2

123 |> add2 |> console.log("result:", ?)

// /repl: Unexpected token (2:38)

// 1 | const add2 = it => it + 2
// > 2 | 123 |> add2 |> console.log("result:", ?)
// | ^

私のときめきとわくわくを返して!!

どうも部分適用はBabel7.4.0でサポートされたようで、

tryでも利用出来る事は出来るみたいです。

しかしパイプライン演算子と併用したときの相性がイマイチで、

tryのサイト上でオプションをポチポチしましたが正常動作は難しいようです。

なんかプラグインも乱立しててよく分からないですね。

使ってみたい人はお気をつけください。



おまけ3: Minimal proposalF# PipelinesSmart Pipelinesの案に関して

パイプライン演算子のStageが一向に進まない問題に関してどうやら一悶着あったみたいですね。

詳しくはこの記事に良くまとまっています。

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


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

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


ははぁ、BabelでのMinimal proposalというのは、

競合している2つの仕様のどっちを採用するかは分からんけど、

とりあえずMinimal proposalの部分は取り入れられるやろうから先に実装しとくわ

……というニュアンスで先行実装されたものみたいですね。

この辺の解説は @remew さんの記事の方が100倍詳しいので紹介だけしてしめます。