メソッドチェーンとダラー演算子とパイプライン演算子に対する気持ち

tl;dr

ダラー演算子,メソッドチェーン,パイプライン演算子は,「関数を次々に適用するとネストが深くなってしまう」という問題を解決する.

disclaimer

これは個人の気持ち(ポエム)であり,特定の言語を批判することを目的に書いた文章ではない.また,この記事は言語のある側面を切り出して比較しているという点でフェアな比較ではない.言語ごとのベストプラクティスも無視している.

そもそも関数を次々に適用するのであれば,その結果を毎回変数にbindしろという主張もわかる.しかし,中間変数を使わずに関数を次々に適用したいという気持ちになることもあり,その場合はこのようにすると綺麗に書けるということをこの記事は主張している.

私の知識不足で間違ったことを言ってるかもしれないのでそのときは教えてください.

関数を次々に適用するとネストが深くなってしまう問題

「配列の要素を2倍し,10以下の値だけを取り出し,絶対値でソートして,頭の3つを取り出す」という操作を考える.

Pythonでは以下のように書ける.

list(sorted(filter(lambda x: x <= 10, map(lambda x: x*2, [2,1,15,-4,-5,-12])), key=abs))[0:3]
# => [2, 4, -8]

このコードにはネストが深く読みづらいという問題がある.

これを解決するのがメソッドチェーン,ダラー演算子,パイプライン演算子である.

メソッドチェーン,ダラー演算子,パイプライン演算子 の例

メソッドチェーン

先程の操作はRubyでは以下のように書ける.

[2,1,15,-4,-5,-12].map { |n| n * 2 }.select { |n| n <=10 }.sort_by { |n| n.abs }.take(3)
# => [2, 4, -8]

ネストが深く読みづらいという問題が解決されているのがわかる.

ダラー演算子($)

同じ操作をHaskellで書いてみよう.

import Data.List

take 3 $ sortOn abs $ filter (\n -> n <= 10) $ map (\n -> n * 2) [2,1,15,-4,-5,-12]
-- => [2, 4, -8]

これもメソッドチェーンと同様に,ネストの深さを減らすことに成功している.

パイプライン演算子(|>)

同じ操作をElixirで書くと次のようになる.

[2,1,15,-4,-5,-12] |> Enum.map(fn n -> n * 2 end) |> Enum.filter(fn n -> n <= 10 end) |> Enum.sort_by(fn n -> abs(n) end) |> Enum.take(3)

これもネストの深さを減らすことに成功している.

気持ち

このように,ネストの深さを減らす手法を3つ挙げた.これらの手法のうち,私はメソッドチェーンとパイプライン演算子を好んでいる.

私がコードを書くときは,データから始まって順に操作を適用していくというふうに書くほうが楽だと感じる.そのような書き方をするとき,ダラー演算子は書きづらい. というのも,コードを書くときはカーソルが左から右に向かって動くからだ.ダラー演算子で書くときは,1ステップ書き終えるごとに左端までカーソルを動かす必要がある. メソッドチェーンとパイプライン演算子の場合は,データの流れの方向とカーソルが動く方向が一致しているため,そのような操作をする必要がない.

些末な違いだとは思うが,私は「コードの書き心地」を重視する人間なので,メソッドチェーンとパイプライン演算子を好んでいる.

追記: Haskellにおけるパイプライン演算子

Haskellにもパイプライン演算子のようなものがあると教えてもらった.

こういう感じで書ける.

import Data.List
import Data.Function

[2,1,15,-4,-5,-12] & map (*2) & filter (<=10) & sortOn abs & take 3
-- > [2, 4, -8]

これめっちゃいいですね.教えてくれた @ishiy1993 さんありがとうございます :pray: .