class: center, middle # map / filter などの
高階関数よりも
古典的な for文の方が
読みやすいと感じる
あなたへ BuriKaigi 2025 2025/02/01
@gakuzzzz --- class: left, top ## 自己紹介 * 中村 学/Manabu NAKAMURA * Twitter: [@gakuzzzz](https://twitter.com/gakuzzzz) * [Tech to Value Co.,Ltd.](https://www.t2v.jp/) CEO * [Alp, Inc.](https://thealp.co.jp/) Tech Lead --- class: left, top ## はじめに 昨今のメジャーなプログラミング言語では、 `map` や `filter` , `flatMap` といった高階関数が当たり前のように標準実装されています --- class: left, top ## はじめに 昨今のメジャーなプログラミング言語では、 `map` や `filter` , `flatMap` といった高階関数が当たり前のように標準実装されています しかしながら、SNS上ではこういった高階関数を使ったコードは可読性が低いとか読み辛いといった声もしばしば目にします --- class: left, top ## はじめに 昨今のメジャーなプログラミング言語では、 `map` や `filter` , `flatMap` といった高階関数が当たり前のように標準実装されています しかしながら、SNS上ではこういった高階関数を使ったコードは可読性が低いとか読み辛いといった声もしばしば目にします このセッションでは、高階関数を使ったコードにおいて読みやすくなるケースと読みにくくなるケースについて紹介し、 なぜ読みにくく感じるのか読みやすくするためにはどのようにすれば良いのか、を紹介したいと思います --- class: center, middle # 高階関数への書き換え --- class: left, top ## 高階関数への書き換え まずこの変哲もない TypeScript の for 文から始めましょう ```typescript const products: Product[] = ... let result: Product[] = []; for (const product of products) { if (product.price >= 5000 && product.stockStatus === 'Enough' && product.releaseDate >= sub(today, {months: 2})) { const discountPrice = product.price > 10000 ? product.price * 0.8 : product.price > 8000 ? product.price * 0.9 : product.price * 0.95; result = [...result, {...product, price: discountPrice}]; } } ``` --- class: left, top ## 高階関数への書き換え この for 文を高階関数で書き換えてみます .diff-rm[ ```typescript const products: Product[] = ... let result: Product[] = []; *for (const product of products) { if (product.price >= 5000 && product.stockStatus === 'Enough' && product.releaseDate >= sub(today, {months: 2})) { const discountPrice = product.price > 10000 ? product.price * 0.8 : product.price > 8000 ? product.price * 0.9 : product.price * 0.95; result = [...result, {...product, price: discountPrice}]; } *} ``` ] --- class: left, top ## 高階関数への書き換え この for 文を高階関数で書き換えてみます .diff-add[ ```typescript const products: Product[] = ... let result: Product[] = []; *products.forEach(product => { if (product.price >= 5000 && product.stockStatus === 'Enough' && product.releaseDate >= sub(today, {months: 2})) { const discountPrice = product.price > 10000 ? product.price * 0.8 : product.price > 8000 ? product.price * 0.9 : product.price * 0.95; result = [...result, {...product, price: discountPrice}]; } *}); ``` ] --- class: left, top ## 高階関数への書き換え これで高階関数を使ったので可読性が上がって保守性もあがりました!やったー! ```typescript const products: Product[] = ... let result: Product[] = []; products.forEach(product => { if (product.price >= 5000 && product.stockStatus === 'Enough' && product.releaseDate >= sub(today, {months: 2})) { const discountPrice = product.price > 10000 ? product.price * 0.8 : product.price > 8000 ? product.price * 0.9 : product.price * 0.95; result = [...result, {...product, price: discountPrice}]; } }); ``` --- class: left, top ## 高階関数への書き換え ……そんなわけ無いですよね --- class: left, top ## 高階関数への書き換え もうすこし真面目に書き換えてみましょう ```typescript const products: Product[] = ... let result: Product[] = []; for (const product of products) { if (product.price >= 5000 && product.stockStatus === 'Enough' && product.releaseDate >= sub(today, {months: 2})) { const discountPrice = product.price > 10000 ? product.price * 0.8 : product.price > 8000 ? product.price * 0.9 : product.price * 0.95; result = [...result, {...product, price: discountPrice}]; } } ``` --- class: left, top ## 高階関数への書き換え もうすこし真面目に書き換えてみましょう ```typescript const products: Product[] = ... const result = products.filter(product => { return product.price >= 5000 && product.stockStatus === 'Enough' && product.releaseDate >= sub(today, {months: 2}); }).map(product => { const discountPrice = product.price > 10000 ? product.price * 0.8 : product.price > 8000 ? product.price * 0.9 : product.price * 0.95; return {...product, price: discountPrice}; }); ``` `if` で絞り込んでいる部分を `filter` で、変数 `result` に詰めなおしている部分を `map` で書き換えました --- class: left, top ## 高階関数への書き換え 読みやすくなりました! ……か? ```typescript const products: Product[] = ... const result = products.filter(product => { return product.price >= 5000 && product.stockStatus === 'Enough' && product.releaseDate >= sub(today, {months: 2}); }).map(product => { const discountPrice = product.price > 10000 ? product.price * 0.8 : product.price > 8000 ? product.price * 0.9 : product.price * 0.95; return {...product, price: discountPrice}; }); ``` --- class: left, top ## 高階関数への書き換え このように、ただ単に高階関数を使ったからといって、可読性や保守性が無条件で上がる訳ではありません --- class: left, top ## 高階関数への書き換え このように、ただ単に高階関数を使ったからといって、可読性や保守性が無条件で上がる訳ではありません 逆に高階関数を使った所為で逆に読み難くなったなーと感じる事もあるのではないでしょうか? --- class: left, top ## 高階関数への書き換え このように、ただ単に高階関数を使ったからといって、可読性や保守性が無条件で上がる訳ではありません 逆に高階関数を使った所為で逆に読み難くなったなーと感じる事もあるのではないでしょうか? とは言え世の中のメジャーな言語はこぞって高階関数を取り入れています。言語設計者達が明らかにメリットがあると判断したから取り入れられた訳ですよね --- class: left, top ## 高階関数への書き換え このように、ただ単に高階関数を使ったからといって、可読性や保守性が無条件で上がる訳ではありません 逆に高階関数を使った所為で逆に読み難くなったなーと感じる事もあるのではないでしょうか? とは言え世の中のメジャーな言語はこぞって高階関数を取り入れています。言語設計者達が明らかにメリットがあると判断したから取り入れられた訳ですよね では何故、高階関数を使って読み難く感じるケースと読みやすく感じるケースが出てくるのでしょうか? --- class: left, top ## 高階関数への書き換え このように、ただ単に高階関数を使ったからといって、可読性や保守性が無条件で上がる訳ではありません 逆に高階関数を使った所為で逆に読み難くなったなーと感じる事もあるのではないでしょうか? とは言え世の中のメジャーな言語はこぞって高階関数を取り入れています。言語設計者達が明らかにメリットがあると判断したから取り入れられた訳ですよね では何故、高階関数を使って読み難く感じるケースと読みやすく感じるケースが出てくるのでしょうか? この秘密は「How と What の分離」にあります --- class: center, middle # How と What の分離 --- class: left, top ## How と What の分離 プログラミング言語の進化の歴史は、いかに How を分離して What を端的に表現できるようにするか、の歴史でもあります --- class: left, top ## How と What の分離 プログラミング言語の進化の歴史は、いかに How を分離して What を端的に表現できるようにするか、の歴史でもあります どのようにデータを加工し作用を起こすかという流れのカタマリ(How)を切り出し、 適切な名前をつけてそれが何を表すのか(What)を明示することで、 プログラム全体の構造を把握しやすくするという事は普段から皆さん実施されている事だと思います --- class: left, top ## How と What の分離 プログラミング言語の進化の歴史は、いかに How を分離して What を端的に表現できるようにするか、の歴史でもあります どのようにデータを加工し作用を起こすかという流れのカタマリ(How)を切り出し、 適切な名前をつけてそれが何を表すのか(What)を明示することで、 プログラム全体の構造を把握しやすくするという事は普段から皆さん実施されている事だと思います `map` や `filter` などの集合操作に関する高階関数も、ループという具体的な手続き(How)を隠蔽し、絞り込みや投射といった目的(What)を表現できるように用意されています --- class: left, top ## How と What の分離 それを踏まえて先ほどのコードをもう一度見てみましょう ```typescript const products: Product[] = ... const result = products.filter(product => { return product.price >= 5000 && product.stockStatus === 'Enough' && product.releaseDate >= sub(today, {months: 2}); }).map(product => { const discountPrice = product.price > 10000 ? product.price * 0.8 : product.price > 8000 ? product.price * 0.9 : product.price * 0.95; return {...product, price: discountPrice}; }); ``` --- class: left, top ## How と What の分離 ループという How が隠蔽されたにも関わらず What が端的に表されていないため、中途半端に How が分断された形になっているわけです ```typescript const products: Product[] = ... const result = products.filter(product => { return product.price >= 5000 && product.stockStatus === 'Enough' && product.releaseDate >= sub(today, {months: 2}); }).map(product => { const discountPrice = product.price > 10000 ? product.price * 0.8 : product.price > 8000 ? product.price * 0.9 : product.price * 0.95; return {...product, price: discountPrice}; }); ``` --- class: left, top ## How と What の分離 なので更に How を分離して What を判りやすくしてみましょう ```typescript const products: Product[] = ... const result = products.filter(product => { return product.price >= 5000 && product.stockStatus === 'Enough' && product.releaseDate >= sub(today, {months: 2}); }).map(product => { const discountPrice = product.price > 10000 ? product.price * 0.8 : product.price > 8000 ? product.price * 0.9 : product.price * 0.95; return {...product, price: discountPrice}; }); ``` --- class: left, top ## How と What の分離 割引可能かどうかという判定と、割引を適用する処理にそれぞれ名前を付け What を明示します ```typescript const canDiscount = (product: Product) => product.price >= 5000 && product.stockStatus === 'Enough' && product.releaseDate >= sub(today, {months: 2}); const discount = (product: Product) => { const discountPrice = product.price > 10000 ? product.price * 0.8 : product.price > 8000 ? product.price * 0.9 : product.price * 0.95; return {...product, price: discountPrice}; } ``` --- class: left, top ## How と What の分離 そうすることで利用側は、割引が適用可能な商品に対して割引した一覧を返す、という What が一目瞭然になりました ```typescript const products: Product[] = ... const result = products .filter(canDiscount) .map(discount); ``` --- class: left, top ## How と What の分離 そうすることで利用側は、割引が適用可能な商品に対して割引した一覧を返す、という What が一目瞭然になりました ```typescript const products: Product[] = ... const result = products .filter(canDiscount) .map(discount); ``` 逆に言うと、どのような商品が割引可能なのか、割引はどれだけ値引きされるのかといった詳細は、ここからは直接読み取れなくなります --- class: left, top ## How と What の分離 `map` や `filter` などの集合操作の高階関数は、ループという具体的な How を隠蔽し、そのコードで表したい What を宣言的に書けるようにします --- class: left, top ## How と What の分離 `map` や `filter` などの集合操作の高階関数は、ループという具体的な How を隠蔽し、そのコードで表したい What を宣言的に書けるようにします したがって、What が上手く読み取れない状態のまま高階関数を使ってしまうと、 中途半端に How が隠蔽された上で What もよくわからないコードになってしまい、読み難いという印象になります --- class: left, top ## How と What の分離 `map` や `filter` などの集合操作の高階関数は、ループという具体的な How を隠蔽し、そのコードで表したい What を宣言的に書けるようにします したがって、What が上手く読み取れない状態のまま高階関数を使ってしまうと、 中途半端に How が隠蔽された上で What もよくわからないコードになってしまい、読み難いという印象になります How と What を適切に分離することで、集合操作の高階関数を使ってコードの意図や目的をより判りやすくすることができます --- class: center, middle # おまけ:性能の話 --- class: left, top ## 性能の話 How と What が分離されることで、What を宣言的に記述する事ができるためそのコードの意図や目的は読み取りやすくなりました --- class: left, top ## 性能の話 How と What が分離されることで、What を宣言的に記述する事ができるためそのコードの意図や目的は読み取りやすくなりました しかし逆に言うと、 How が隠蔽されるため、性能特性についてなどは却って読み取り辛くなる場合があります --- class: left, top ## 性能の話 How と What が分離されることで、What を宣言的に記述する事ができるためそのコードの意図や目的は読み取りやすくなりました しかし逆に言うと、 How が隠蔽されるため、性能特性についてなどは却って読み取り辛くなる場合があります 例えば TypeScript で不用意に `Array` の `map` を沢山チェーンさせたりしてしまうと中間状態の `Array` が沢山できてしまい、 for ループで書くよりメモリを多く消費してしまうなんていう事もあります ```typescript const products: Product[] = ... const names = products .map(p => p.name) // 不要な Array が生成される .map(n => n.toUpperCase()); ``` --- class: left, top ## 性能の話 こういった高階関数による集合操作のパフォーマンスチューニングにおいては、for ループでのチューニングとはまた別のノウハウが必要になります --- class: left, top ## 性能の話 こういった高階関数による集合操作のパフォーマンスチューニングにおいては、for ループでのチューニングとはまた別のノウハウが必要になります 多くの場合、こういったケースの性能面はデータ構造に依存します ```typescript const products: Product[] = ... const names = products .map(p => p.name) // 不要な Array が生成される .map(n => n.toUpperCase()); ``` --- class: left, top ## 性能の話 こういった高階関数による集合操作のパフォーマンスチューニングにおいては、for ループでのチューニングとはまた別のノウハウが必要になります 多くの場合、こういったケースの性能面はデータ構造に依存します .diff-add[ ```typescript const products: Product[] = ... const names = products * .values() // Iterator を取得 .map(p => p.name) // Iterator は中間構造を生成しない .map(n => n.toUpperCase()); ``` ] --- class: left, top ## 性能の話 高階関数によってループを分離した事によって、並列化によるパフォーマンスチューニングが簡単になる側面もあります ```scala // Java List
products = ... var result = products .parallelStream() // データ構造として並列Streamを利用 .map(p -> p.heavyCalculation()) // 簡単に重い処理を並列で実行できる .toList(); ``` --- class: left, top ## 性能の話 高階関数によってループを分離した事によって、並列化によるパフォーマンスチューニングが簡単になる側面もあります ```scala // Java List
products = ... var result = products .parallelStream() // データ構造として並列Streamを利用 .map(p -> p.heavyCalculation()) // 簡単に重い処理を並列で実行できる .toList(); ``` より適切なデータ構造や処理の合成方法を選択できるようになりましょう --- class: center, middle # まとめ --- class: left, top ## まとめ - 集合操作の高階関数は使えば必ず良くなるというものではないですよ --- class: left, top ## まとめ - 集合操作の高階関数は使えば必ず良くなるというものではないですよ - How と What の分離を意識しましょう --- class: left, top ## まとめ - 集合操作の高階関数は使えば必ず良くなるというものではないですよ - How と What の分離を意識しましょう - 集合操作の高階関数は What を宣言的に記述するのが大事 --- class: left, top ## まとめ - 集合操作の高階関数は使えば必ず良くなるというものではないですよ - How と What の分離を意識しましょう - 集合操作の高階関数は What を宣言的に記述するのが大事 - 一方でパフォーマンスチューニング等は別のノウハウが必要に --- class: left, top ## まとめ - 集合操作の高階関数は使えば必ず良くなるというものではないですよ - How と What の分離を意識しましょう - 集合操作の高階関数は What を宣言的に記述するのが大事 - 一方でパフォーマンスチューニング等は別のノウハウが必要に - How を分離することでより高度な最適化も可能に --- class: left, top ## まとめ - 集合操作の高階関数は使えば必ず良くなるというものではないですよ - How と What の分離を意識しましょう - 集合操作の高階関数は What を宣言的に記述するのが大事 - 一方でパフォーマンスチューニング等は別のノウハウが必要に - How を分離することでより高度な最適化も可能に - 高階関数を使いこなして保守しやすく性能もいいコードを書いていきましょう --- class: left, top ## 宣伝 - Tech to Value では [Online Scala コードレビュー サービス](https://www.t2v.jp/#service)を提供しています - チームに Scala熟練者が居ない等、ぜひご相談ください - 株式会社アドウェイズさんにて[導入事例を記事にして頂きました](https://blog.engineer.adways.net/entry/2022/08/12/120000) - アルプ株式会社では一緒に働くメンバーを募集しています - 気軽にお声がけください! - [アルプ採用情報](https://alpinc.notion.site/52af90188305440d86acf72968646054) --- class: center, middle ## 質問とか --- class: left, top