親記事: なぜfor文は禁止なのか?関数型記述のススメ - @ukiuni@github 氏
@ukiuni@githubさんの上記記事と趣旨は同じです。
でも、読んでてしっくり来なかったので、自分の言葉で表現した方がいいよねって事で今回筆を取りました。
JSのメソッドチェーンは関数型モドキでしかないので、とてもじゃないけど関数型とは呼べない関数型のエッセンスを取り出したリスペクト記事です
まず、for文でやるお馴染みの手法と、
配列のリスト操作系のプロトタイプメソッドを駆使したコードを考えてみます。
(マサカリが飛んでくるので素のJSで関数型というワードは使ってはいけません)
// 0から100未満の偶数のみを累計する。
const main = () => {
let sum = 0
for (let i = 0; i < 100; i++) {
if (i % 2 === 0) {
sum += i
}
}
return sum
}
console.log(main()) // 2450
// 0から100未満の偶数のみを累計する。
const main = () =>
Array(100).fill(0).map((_, i) => i)
.filter(it => it % 2 === 0)
.reduce((a, b) => a + b, 0)
console.log(main()) // 2450
※今後「前者のコード」「後者のコード」というパワーワードが飛び出しますが、これは上記2通りのコードを指し、それぞれ下記のように定義します。
- 前者のコード: if文やfor文を活用したALGOL系の一般的な書き方
- 後者のコード: JSのメソッドチェーンを活用した関数型モドキ
前者のfor文を使ったプログラミングに慣れている人は前者の方が良いかもしれません。
しかし、前者寄りの人が上記のコードを一目で把握できるように、後者のスタイルに慣れた人も上記のコードを一目で把握します。
-
arr.filter(it => it % 2 === 0)
: 配列から偶数の要素だけ抜き出した配列を返す -
arr.reduce((a, b) => a + b, 0)
: 全ての要素を合計した数値を返す
行頭で何がしたいかの目的(filter, reduce)が書かれているので、
行頭だけざっと眺めれば大体何がしたいのか推測することも可能です。
この辺も加味すると前者よりも分かりやすいと感じる人も一定数居ます。
従って、どちらも単に慣れの問題です。
そして前者・後者のコードが両方ある程度書けるエンジニアから見た場合、
後者のコードは下記の主張があります。
- 行数が減る
- ネストが減る・カッコの対応がシンプルになる
- 変数の数が少ない
- sumが宣言した瞬間あるべき値になっている
これらを総合すると可読性が高いといえます。
(因みに前者のfor文を使ったコードは実行速度が速い事、初級者やC言語がメインの人にも扱いやすいという事が主張です。)
1つずつ見ていきましょう。
行数が減る
if文やfor文は終了する時に閉じ波括弧単体の行を作る必要があります。
従ってifやforを1個減らせば1〜2行減ります。
JSはALGOL系の命令型言語ですので、
普通に書くとif文とfor文を多用することになります。
その分だけ波括弧を書く回数が増えます。
人間は面倒なお作法よりも、動作するロジックだけが記述されている方が遥かに読みやすいと感じます。
とは言え、ES2015で増えたといってもJSのメソッドチェーンはとても貧弱です。
無理にJSのメソッドチェーンで頑張ると逆に行数が増えたり可読性が下がるケースがありますので、関数型プログラミングをサポートするライブラリを利用した方が良いと思います。
// 0から100未満の偶数のみを累計する。
const _ = require('lodash')
const main = () =>
_(0).range(100)
.filter(it => it % 2 === 0)
.sum()
console.log(main()) // 2450
// レギュレーション無視なら十分1行に収まる
const _ = require('lodash')
_.chain(0).range(100, 2).sum().tap(console.log).value()
// 2450
Lodashのようにsumメソッドがあると捗りますね。
ちょっとズルしましたが記述量も大幅に減り、ワンライナーで書いても問題なさそうです。
このtapとthruが便利すぎて超卑怯なんですよね…
Lodashは後者の書き方ビギナーさんにもオススメです。
console.log(xxx)
の戻り値は必ずUndefinedです。
従って、後者のスタイルは基本的にメソッドチェーンの途中で値を確認したくなっても確認することが出来ません。
なので暗闇の中を進むような不安を感じることでしょう。
しかし、Lodashなら_.tap
が使えます。
これは内部の関数に値を渡しながらも、戻り値は捨てて引数をそのまま次のメソッドチェーンへ運ぶ挙動をします。
上手く動かない時、.tap(console.log)
を途中に挟んで挙動に影響を与える事無く値を閲覧できます。
デバッグに便利ですね!
ネストが減る・カッコの対応がシンプルになる
これも単純に傾向です。
前者のコードはforやifと共に作られた波括弧が複数の行に跨っています。
後者のコードは括弧を次の行に一切持ち越さずに書けているのが分かります。
インデントのネストや波括弧のネストは、
行数はそうでもありませんが、読み手の負担としてじわじわ効いてきます。
今回は明らかに後者が有利な状況でしたが、
更に難易度が上がると前者寄りの状況も増えるでしょう。
そういう時はRamda.jsを利用すると
Promiseでthenと繋いでいくようにpipeやcomposeを使って表現出来るので、
ネストのコントロールがとても楽になります。
// 0から100未満の偶数のみを累計する。
const R = require('ramda')
const main = () =>
R.pipe(
R.range(0),
R.filter(R.pipe(R.modulo(R.__, 2), R.equals(0))),
R.sum
)(100)
console.log(main()) // 2450
Ramda.jsを使わず自力で書くとこんな感じになります。
前提となるpipeやrange関数はイディオムを多用しているので難解ですが、
それ以降はまぁまぁシンプルに書けていると思います。
// 0から100未満の偶数のみを累計する。
const pipe = (value, ...fns) => fns.reduce((val, fn) => fn(val), value)
const range = (s, e) => Array(e - s + 1).fill(0).map((_, i) => i + s)
const main () =>
pipe(
range(0, 100 - 1),
its => its.filter(it => it % 2 === 0),
its => its.reduce((sum, it) => sum + it, 0)
)
console.log(main) // 2450
変数の数が少ない
変数は少なければ少ないほど良いコードです。
「これは程度問題であり、変数を作るべき場面もある」という反論もあるでしょう。
もちろん何でもかんでも1行で頑張るのが最善とは言いませんし、その反論もわかります。
ですがコードってのは文章なんです。
変数ってのは登場人物なんですよ。
あなたは毎話ヒロインが増える小説を読みたいですか?
すぐにこいつ誰だっけ?ってキャラクターだらけになりますよ。
小説ってのは少ない登場人物で話を深めていく読み物ですからね。
コードの場合も同様で、登場人物(変数)全てを把握していなければ速攻でバグが出ます。
プロダクトってのは開発者全員で書いていくリレー小説みたいなものです。
素人の物書きが1人混じるだけでプロダクト全体が粗悪品になります。
多少の心得のあるエンジニアならば、どうしても宣言しなければならない変数や定数に関して一定の嗅覚や感覚を持ち得てます。
例えばDBやサーバのインスタンスといったものは宣言せざるを得ないものの代表でしょう。
そうして宣言せざるを得ない変数を宣言した結果は、大抵変数が多すぎるのでもっと削るべきという評価を下されます。
「このプロダクトは変数が少なすぎる、もっと増やすべき」
…という場面に遭遇した事はこのエンジニア人生でただの1度もありません。
口では状況次第と言いながらも、現実は変数をより少なくする事に意識を向ける必要があります。
ではどうやって減らせるのでしょうか?
「for文の前後で作れられる状態変数」くらいのものでしょう。
for文の前後で作られる状態変数は少なければ少ない方が良い、0個でも好ましいものです。
sumが宣言した瞬間あるべき値になっている
後者のコードはもはやsum変数すらなくそのまま値を返していますが、
宣言した瞬間に値が決定しているのも後者のコードの良さの一つです。
letは値の加工の宣言に他なりません。
読者さんに対して「この登場人物は刻一刻と成長していきますからよく覚えてて下さいね」と言ってるわけです。
ブロックスコープが閉じるその瞬間まで面倒を見る必要があります。
まぁ、今回のコードはsumという変数1個だからいいんですよ別に。
実践で変数1個で済むような状況はそうそうありません。
2つ以上の状態変数が出てきた時にその真価を問われます。
letで宣言した変数は加工が終わってもletのままで勝手に固定されたりしません。
2つ目の変数を操作している時も気が抜けません。
3つ以上になればどうでしょう?
これでは文節毎に「いっぽうその頃……」を多用する粗悪な小説です。
letを使うなとまでは言いませんが、複数のletを扱うのは絶対やめましょう。
関数やメソッドとして外出しして、使う場面ではconstに束縛してください。
締めの挨拶 (此処からちょっと文章改変)
以上、for文やif文を減らせば状態変数や波括弧地獄が減りますよという私の胸の内を現したポエムでした。
いや、ここからが本当のポエムかもしれません……
私は冒頭でメソッドチェーンを使う側の主張はコードの可読性と謳いました。
その物差しとして行数・変数の数・括弧の数や持ち越した行の数、見事に数字として目に見えるものだけを表現しながら物書きに例えて解説していきました。
何故ならば私にとっての関数型プログラミングはコード量が減るものだったからです。
私は行数が減って簡素に書き表せるものは何でも大好きです!
だからCoffeeScriptやLiveScriptという方向へ舵を切りましたし、Node.jsに戻ってからもLodashやRamda.jsを駆使してプログラミングを行っています。
もし関数型プログラミングとやらで逆にコード量が増えるならば、
きっと私は今でも全く興味無かったと思いますし共感もしなかったでしょう。
今回は命令型で普通に書いてたった9行という短い命題でしたが、
メソッドチェーンを駆使すれば5行で書くことに成功し、短く書けるという証明は達成出来ました。大満足です。
普段命令型で慣れ親しんでいる皆さん。
いざという時、ライブラリやメソッドチェーンを駆使すればコード量を減らせる選択肢があるんだよという事は覚えておいてください。
Lodashくらいならば誰でもすぐその恩恵にあやかれます、最初は「おっ、このsumっての便利やんけ」程度でも全然素敵ですよ。
私はNode.jsやJSを使ってこの数年間お仕事をさせて頂きました。
今まで遭遇してきた全てのビジネスロジックをLodashやRamda.js、Lazy.js等の関数型プログラミング用ライブラリとの併用で時には半分、時にはそれ以下のコード量に圧縮して片付けてきました。
興味が出てきてやろうと思ったけど、無理だと思った時はTeratailにでも書き込んで下さい、
当然実行速度は犠牲になりますが、命題やデータ構造さえちゃんとしていれば描ききってみせます。
Ramda.jsやLodashはよく出来たライブラリで、メソッドが行き届いているのでドキュメントを端から端まで目を通せば必ず突破口になるメソッドが用意されていますからね。
しかし、私はそれ以上を説こうとも強要しようとも思いません。
何故ならば、命令型もまた素敵なものだからです。
なのでタイトルは親記事からパクっただけで内部は全く別物の釣り記事ですし、ポエムです。
(もちろんタダのポエムでは終わらせず、様々なコードを書く事でライブラリの特徴やメソッドチェーンの可能性はお見せ出来たと思っていますけどね)
これらの文章を読んで今まで命令型ばかり書いてたけど、使い分けでバグを減らしたり行数減らせるって素敵じゃないか!と共感する人が暇な時にメソッド一覧を読んでみたり使ってみたよ的な記事を書いてくれると嬉しいなぁ程度に考えています。
【おまけ】両刀になった私の使い分け (追記部分)
冒頭で命令型には速度面での主張があると説きました。
JSはどんなに取り繕ったとしてもバリバリのALGOL系言語であり、
メソッドチェーンや関数型プログラミングのテクニックを利用すれば殆どの場合は処理速度が犠牲となってしまいます。
それを証拠に「俺っち関数型プログラミングのライブラリだぜ」感を醸し出しているLodash、Ramda.jsの中身はwhile文だらけです。
ライブラリレベルではどう取り繕ってもJSという制約がある以上速度が最重要で、内部はゴリゴリの命令型です。
私は普段のコードは関数型プラグラミング用ライブラリを使って値の加工を横着しています。
しかし速度が遅くて改善要求が上がれば迷わずにfor文を駆使して書いていきます。
これがJavaScriptですからね、この緩さが好きですし、この中途半端なスタイルも気に入っています。
お礼
拙い文章にお付き合い下さいましてありがとうございました。
改変・引用ご自由にしてください。
※なお、本文内で何度も取り上げている「粗悪な小説」とは「小説家になろう」や当サイトに寄稿されている小説とは一切関係ありません。コメント欄で具体的なタイトルや作者さんを取り上げてdisるような事はしないでください。