よく設計された関数というのは、使っていて気持ちの良いものです。逆に、適当に作られた関数は、錆びついた歯車のように開発スピードにとって障害となりうるものです。その関数は、何をしたい関数なのか、本当に明瞭でしょうか。名前から関数の意味は、汲み取れますか。そのインターフェースは、ミスを防ぐことができますか。この章では、意味が明瞭で扱いを間違いないで済むような関数の作り方を見ていきます。
色のある動詞を使おう
関数の命名は動詞で始めるのが慣例ですが、動詞であればなんでも良いというわけではありません。できるだけ意味の曖昧な動詞を避けて、色のある動詞を使うのがポイントです。色のある動詞とは、意味が具体的(限定的)で、情報量が多い動詞です。例えば、”get”という単語だと、どのように”get”するのかという情報が不足しています。
const getSomething = (arg) => {
...?
)
これだと、getSomethingという関数が中でどんな処理を行なっているのか、実際にコードを読んでみないとわかりません。こんな曖昧な命名が至るところにあると、コードを読むのに膨大な時間が取られて、もう作業どころではありません。コードを読んで理解した内容を、一時的に記憶していないと作業が進められないので、脳のワーキングメモリのリソースも食ってしまいます。ワーキングメモリを食うということは、コードを書き出したら、
えっと、たしか、getSomethingは…という処理だったから、ここはgetSomethingを呼び出してやればよくて、こっちの方は……getSomeoneで良いのかな、あれ、getSomeoneはどういう関数だっけ。うーん、思い出せない。またコードを読みにいかなきゃ…
と、なってしまうということです。これでは、プログラミングが甚だしくストレスのかかる行為になります。由々しき問題ですね。どうにか解決することはできないでしょうか。ということで、これをより具体的な命名に変更してみます。次のような命名はどうでしょうか。
① const fetchSomething = (arg) => {
...?
}
or
② const extractSomething = (arg) => ({
...?
})
これだと、関数の名前から、中でどんな処理を行なっているのかが容易に想像できます。①の例では、fetchという単語を使っています。この語彙は、getという単語と同じように「取得する」という意味合いを持っていますが、「何か違うところから取ってくる」のだというニュアンスが加わります。fetchはgetよりも色のある動詞なのです。おそらくはAPIを叩いて何かのデータを取得して返しているのでしょう。そこで行われているのは、次のような処理だと想像できます。
const fetchSomething = (arg) => {
return fetch(`/something?id=${arg.id}`, {method: "GET"})
}
一方、②の例では、extractという単語を使っています。このことから、引数argからsomethingを絞り込んで値を返しているのではないかという、あたりをつけられそうです。すなわち、次のような処理です。
const extractSomething = (arg) => {
return arg.filter(item => item.name === "SOMETHING")
}
ここでは「色のある動詞を使うことが重要」ということを示すために、argやsomethingという名詞の方は敢えて適当に命名しました。しかし、これも本来はいただけない命名です。次のように、名詞もしっかりとつけてあげると良いでしょう。
const fetchUsers = async() => {
const response = await fetch(`/something?id=${arg.id}`, {method: "GET"})
return response.json()
}
const extractEnterpriseUsers = (users) => {
return users.filter(userPlan => userPlan === "enterprise")
}
これなら、この関数を使って別の人がコードを書く場合でも、次のような処理が容易に書けるはずです。
const enterPriseUsers = async() => {
const users = await fetchUsers()
return extractEnterpriseUsers(users)
}
ソースコードが色のない曖昧な動詞で満ちているよりも、ずっとスムーズにコーディングが進みそうですね。このような良い動詞の選定については、次の記事も非常に参考になるのでおすすめです。
プログラミングでよく使う英単語のまとめ【随時更新】 - Qiita
RoRoのパターンを取り入れよう
わかりやすい関数を書くコツとしては、RoRoというパターンもあります。これは”Receive an object, Return an object”の略です。個人的には、”Receive an object”というオブジェクトで受ける(引数をobjectにする)という方が非常に重要だと思います。
例えば、次のような関数があったとします。
const calcBMI = (weight, height) => {
return weight / (height * height)
}
この関数は次のように使うことができます。
const BMI = calcBMI(42, 1.6)
シンプルで良い関数のようにも思えますが、以下のようなケースも考えられます。
const BMI = calcBMI(1.6, 42)
ここでは、うっかり身長と体重を逆に渡してしまっています。こうすると、BMIが0.0009070294784580499という恐ろしい数値になってしまいます。医者ではないのでわかりませんが、即入院というか、もう命が危ないレベルなんじゃないかと思います。
ところで、プログラミング言語の中には、名前付き引数やキーワード引数と呼ばれる言語仕様を持っているものがあります。キーワード引数を使うとこのような引数の渡し間違えのミスを防ぐことができます。例えば、kotlinだと次のように書くことができます。
val BMI: Int = calcBMI(wight = 42, height = 1.6)
残念ながらJavaScriptにはこのような構文がありません。しかし、代わりにRoRoのパターンを採用することを選択できます。次のようにするのはどうでしょうか。
const calcBMI = (arg) => {
return arg.weight / (arg.height * arg.height)
}
const BMI = calcBMI({ weight: 42, height: 1.6 })
今度は、容易には渡し間違えなくなったかと思います。仮に誰かが、calcBMI({ weight: 160, height: 42 })
と書いて、プルリクエストを投げてしまったとしても、レビュワーは彼のミスを逃さす指摘できるのはないでしょうか。
このように、RoRoのパターンを採用するのは、可読性の面でもミスを減らすという面でも、非常に有用であると言えそうです。
関数の言いたいことはただ一つ
読みやすい関数を書くためのコツの1つは、様々な処理を同じ関数の中に詰め込まないことです。関数がその名前通りのことを行うように意識します。名前以上にたくさんのことを行うと、可読性は失われてしまいます。1つの関数の中でなんでもかんでも行おうとするのではなく、別の処理を行いたくなったら、しっかり別の関数に切り出してあげるのが良いでしょう。
具体的にどういうことか、見ていきます。
const updateUser = ({username, email}) => {
if(name > 30) {
throw new Error("ユーザー名が長すぎます")
}
if(!email.includes("@")) {
throw new Error("ユーザー名が長すぎます")
}
return fetch("https://example-api/user", {
method: "PUT",
body: JSON.stringify({ username, email})
})
}
ここでは、関数名が updateUser となっていますが、実際に関数の中でおこなっている処理は、ユーザーの入力した値(usernameとemail)のバリデーションとユーザーのアップデートの2つになってしまっています。これではいけません。関数の命名は「名が体を表す」ようになるのがベストです。
では、validateInputAndUpdateUserとすれば良いのかというと、そういう問題ではありません。本来なら、1つの目的のために1つの関数があるべきです。したがって、命名にAndのついている関数名は、適切に分割されていないものだと疑ってかかった方が良いでしょう。この場合は、以下のように、関数を分けてあげたらどうでしょうか。
const validateUserFormValues = ({username, email}) => {
if(name > 30) {
throw new Error("ユーザー名が長すぎます")
}
if(!email.includes("@")) {
throw new Error("ユーザー名が長すぎます")
}
}
const updateUser = ({username, email}) => {
return fetch("https://example-api/user", {
method: "PUT",
body: JSON.stringify({ username, email})
})
}
どうでしょうか。先ほどより、いくらか関数の担う処理のまとまりが良くなったと感じないでしょうか。
ところで、中には、関数を分割する代わりにコメントを挿入する人もいます。個人的には、これを「コメントの儚い防波堤」と呼んでいます。次のような書き方です。
const updateUser = ({username, email}) => {
// ----- validation ここから -----
if(name > 30) {
throw new Error("ユーザー名が長すぎます")
}
if(!email.includes("@")) {
throw new Error("ユーザー名が長すぎます")
}
// ----- validation ここまで -----
return fetch("https://example-api/user", {
method: "PUT",
body: JSON.stringify({ username, email})
})
}
このようにすると、確かにバリデーションがどこからどこまでなのかは、わかりやすくなります。しかし、コードに降り注ぐ変更の要求の数々は、無慈悲な波のようです。このようなコメントを入れただけでは、大したバリアにはならず、時期にコードの境界は乱れていきます。これが「コメントの儚い防波堤」と呼ぶ理由です。このアンチパターンの仲間には、次のようなものもあります。
// この関数の中で、ユーザーの入力値のバリデーションも行う
const updateUser = ({username, email}) => {
if(name > 30) {
throw new Error("ユーザー名が長すぎます")
}
if(!email.includes("@")) {
throw new Error("ユーザー名が長すぎます")
}
return fetch("https://example-api/user", {
method: "PUT",
body: JSON.stringify({ username, email})
})
}
今度は、関数の先頭にコメントの行を挿入したものです。ここだけを見ると、なるほど、確かに関数が何をやっているのかはわかりやすいのかもしれません。しかし、コメントとは往々にして見過ごされてしまうものです。コメントの持つ効果は、適切な命名のコメントの持つ効果よりも遥かに弱いものだと覚えておいてください。
実装の意図や自分の伝えたいことは、できるだけコードで表現するべきです。コメントよりも、まずはわかりやすい命名を考えることに労力を注いでいきましょう。
関数の箱
(ここから、徐々に抽象的な話をしはじめますので、初学者の方は今は内容が入って来なくても気にしないでください。)
さて、関数の意味の明白さについて長々と語ってきましたが、しかし、そもそも「関数」とは何なのでしょう。今一度、その言葉の成り立ちについて考えてみると、プログラミングの視野は広がるかもしれません。
諸説ありますが、「関数」という漢字は「函数」に由来するもので、「函」は箱を表す言葉です。この「函」という字は言い得て妙というか、一言で関数の性質をうまく表しているように感じます。
関数という言葉を聞いたら、函(箱)を思い浮かべると豊かなイメージが膨らみます。例えば、目の前に、黒い箱が置いてあります。箱の中の様子は見られませんが、とにかく左側からバナナを右側からリンゴが返ってきます。何百回、何千回と繰り返しても常に、同じことが起こります。それは予測がしやすく、守られた世界です。
雨の日も風の日も、Aを入れたらBが返ってくる。今日も明日も明後日も、Aを入れたらBが返ってくる。あなた大阪にいても、東京にいても、クアラルンプールにいてもAを入れたらBが返ってくる。それが、本来の函数、いわゆる純粋関数 (Pure function) と呼ばれるものです。例えば、次のようなイメージです。
const greet1 = (name) => {
return `こんにちは、私の名前は${name}です。`
}
この関数は、greet(”みりん書房”)のように、みりん書房を渡してやれば”こんにちは、私の名前はみりん書房です。”を返します。ジェームズを渡せば、”こんにちは、私の名前はジェームズです。”を返します。いつ、どこで、何をしていようと、引数が同じなら常に同じ結果になります。美しく、安全な世界です。
これに対し、まるで函数の箱の底に穴が開いているかのような、純粋でない関数というものも存在します。常に結果が同じであることが、保証されていないような関数です。それは、例えば次のようなものです。
const person = {
name: "みりん書房"
}
const greet2 = () => {
return `こんにちは、私の名前は${person.name}です。`
}
このサンプルは単純化されたものなので、先ほどの関数greet1とこのgreet2の間には、ほとんど違いがないように感じられるかもしれません。greet(”みりん書房”)とgreet2()の返す値は同じです。
しかし、本当のところ、greet1とgreet2の間には大きな違いがあります。思うに、greet1の方がずっと信頼がおける強固な関数で、greet2は脆く危うい関数です。なぜなら、greet2には、思わぬ値の変更の魔の手が忍び寄る隙があるからです。それは、次のような危険性です。
const person = {
name: "みりん書房"
}
person.name = "ヴィットーリオ・エマヌエーレ2世" // 忍び寄る魔の手
const greet2 = () => {
return `こんにちは、私の名前は${person.name}です。`
}
この例では、personが宣言されてから、greet2を呼び出すまでの間で、こっそり名前を変更してみました。1行を追加しただけなので、今ひとつピンと来ないかもしれないですが、このperson.name = "ヴィットーリオ・エマヌエーレ2世”という処理が、何か複雑で巨大な処理の中にうっかり紛れていたら…と想像すると、バグに気がつくのはかなり困難かもしれません。この点が、greet1に比べたときのgreet2の弱さになります。
関数の外側で宣言された値に依存するということは、関数が値の変更に伴って関数の返す値が変わりうるということです。そうすると、返り値は思わぬ結果になるかもしれません。安全で守られた世界から、危険で不穏な世界へと道を踏み外してしまったのです。
しかし、現実世界では「どのような状況下で実行しても常に結果は同じ」ということは稀です。我々は、様々な外部要因や不測の事態と向き合わなければなりません。ここでいう外部要因とは、例えばデータの取得です。
const greet3 = async() => {
const person = await fetch("https://example-api/person, { method: "GET" }).then(response => response.json())
return `こんにちは、私の名前は${person.name}です。`
}
greet3()
今度はAPIから取得したデータを書こうして、”こんにちは、私の名前は○○です。”といった文字列を返す関数になりました。
この関数greet3は果たして常に同じ結果を返す純粋な関数だと呼ぶことができるでしょうか。いいえ、できません。もし、APIの実装にミスがあれば、person.nameはundefinedになり、”こんにちは、私の名前はundefinedです。”を返してしまうかもしれません。つまり、この例ではAPIの実装という外部要因に依存して、関数の純粋性は損なわれてしまったといえそうです。このような外部要因のことを、以下では「副作用」と名づけて呼んでみます。
では、APIなど使わなければ良いのではないかというと、そうは問屋が卸しません。現代では、APIとデータのやり取りをすることなしに、ユーザーに価値を提供できるプロダクトを開発するのは極めて難しいことです。アプリケーションの開発にAPIは必須で、故に副作用はどこまでもついて回るものです。
結局、我々は副作用の奴隷ということになってしまうのでしょうか。greet3のように実装する代わりに何か他の手段はないのでしょうか。副作用を無くすことはできませんが、副作用の影響を少なくすることならできそうです。次のようにする手があります。
const fetchPerson = async() => fetch("https://example-api/person, { method: "GET" }).then(response => response.json())
const greet4 = (name) => {
return `こんにちは、私の名前は${person.name}です。`
}
const person = await fetchPerson()
greet4(person.name)
この改良バージョンでは、fetchPersonの部分は副作用を孕んだ純粋ではない関数です。しかし、fetchPersonの部分を切り出したおかげで、greet4は純粋な関数になりました。いつ何時どこで実行しようと、渡す値が同じである限りgreet4の返す値は常に同じです。
greet4は、副作用に感染した部分を切り離すことで、関数の純粋性を取り戻しました。このように副作用の影響を最小限に留める、あるいは、「副作用をコントロールする」という考え方が、プログラムを安全にメンテナンスしやすく保つために必要なことの1つだと思います。
四章のまとめ
本章では、読みやすく理解しやすい関数とはどんなものなのか、私見を述べさせていただきました。色のある具体的な動詞を探すこと、RoRoのパターンを取り入れること、副作用をコントロールすることなど明日から意識できそうなTipsもあったのではないかと思います。章後半のピュアな関数についての説明は少々まどろこっしく感じられた方もいるかもしれませんが、さらに読み進めていただければ何となく勘所がわかるはずです。では、五章に進みましょう。