┏━━━┳━━━━━━━┳━━┓
┣ヽ  ̄   /  (・ω・)┫
┣━━━━━━╋━━━━╋━━┫
┣、ハ,,、 \(. \ ノ┫ズコープラモ
┗┻━━━━━┻━━━━┻━━┛
|> 関数1(カット)
|> 関数2(組み立て)
|> 関数3(設置)
|> console.log

//           ∧∧
//        ヽ(・ω・)/   ズコー
//     \(.\ ノ
//    、ハ,,、  ̄
//     ̄

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

2018/4/8現在、ようやくStage1に登った所でまだまだ先は長いって感じですね。
the pipeline operator - Node.js ESNEXT Support

JSはES2015でモダンな新構文が押し寄せてきましたので、
既存のコードは押し流されてしまい、まるでレガシー扱いされてしまう程になりましたね。

実はES7に含まれているパイプライン演算子も非常に強力で、
既存のコード全てをレガシー扱いにして押し流す程のパワーを秘めています。
予習しておくことに越したことはないと言っても過言ではないでしょう。

※結論から言うとネイティブで使う分にはダメそうですが、Ramda.jsやawaitとの併用で本当に世界が変わる程の力があると考えました。
※募集: もしES7と同時にビルトイン関数やビルトインオブジェクトで追加される関数やメソッドが分かる方がいらっしゃいましたら教えてください。

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

詳細は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!"

な、ななな何よ!?この括弧の数は!!
こんな糞コードはたたっ斬って、リファクタリングしてやるんだから!!

まぁ、慌てないでください。
これがES7ではこうなります。

es7.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になります。
値を変数に取りたければ下記のようにしてください。

es7-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みたいな処理特化の関数と相性が良いんです。

es7-tap.js
// TODO: 後でカリー化して引数2つでも発火出来るようにする
const tap = 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文は禁止なのか?ポエム版で書かれているように、
メソッドチェーンと同じく状態変数を減らす良い手段として役に立つことでしょう。

また、パイプライン演算子は縦に並べる事も可能です。
アロー関数とセットにすればこのように関数も変数も宣言せずに値を加工していけます。

es7-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

あのさぁ…
こんなもん実装する前に、全部の型にtapthruのプロトタイプメソッド作って下さい。
そっちの方が遥かに有用で使い勝手が良いと思います。

酷評しましたが実際はJSON.stringifyconsole.logみたいな使い勝手の良いビルトインオブジェクトのメソッドがありますので、それなりに採用シーンはあると思います。

実際の利用シーン

ネイティブではちょっとどうしようもないので、
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)とか出来るからファイル出力も捗りますね。
この?を単体で欲しいんですがどうにかなりませんか?

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で試してみましょう。
下記のようにペチペチっと打ち込んで……

1 |> it => it + 2

// repl: Unexpected token (1:5)
// > 1 | 1 |> it => it + 2
//     |      ^

想定どおり構文エラーですね。
しかし、このサイトはproposalのプラグインを導入することが出来ます。
「Plugins」をクリックして「propossal-pipeline-operator」を探して導入すれば……

1 |> it => it + 2

// Requires Babel "^7.0.0-0", but was loaded with "6.26.0".
// If you are sure you have a compatible version of @babel/core,
// it is likely that something in your build process is loading the wrong version.
// Inspect the stack trace of this error to look for the first entry that doesn't mention "@babel/core" or "babel-core" to see what is calling Babel.

ちくしょう!!

まぁ、Babelの7系とpropossal-pipeline-operatorを導入すれば大丈夫らしいですよ。
せっかちな読者の貴方、もし待てないなら試してみてくださいね。

私も気が向いたらセットアップして共有しますね。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.