class: center, middle # コードレビューサービスで
多くの現場に伝えたこと 第四回関数型プログラミング(仮)の会 2023/03/25
@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 ## 今日話す事 Tech to Value では オンライン Scala コードレビュー というサービスを提供しています。 様々なプロジェクトのコードレビューをする中で、おおよそ共通でしている話があります。 今日はそのなかからいくつかピックアップしてご紹介したいと思います。 関数型のパラダイムに不慣れな方に対して教育する際の参考にして頂ければ幸いです。 --- class: left, top ## 今日話す事 - データの加工と副作用を分離しよう - 制約を型やデータ構造で表現しよう - 網羅性検査を活用しよう --- class: left, top ## データの加工と副作用を分離しよう ```scala def updateLatestVisit(targets: List[UserId], now: Instant):Unit = { val users = UserRepository.findByIds(targets) for (user <- users) { if (user.status == UserStatus.Active) { val updated = user.withLastVisit(now) UserRepository.store(updated) } } } ``` --- class: left, top ## データの加工と副作用を分離しよう .diff-rm[ ```scala def updateLatestVisit(targets: List[UserId], now: Instant):Unit = { val users = UserRepository.findByIds(targets) * for (user <- users) { * if (user.status == UserStatus.Active) { * val updated = user.withLastVisit(now) * UserRepository.store(updated) * } * } } ``` ] --- class: left, top ## データの加工と副作用を分離しよう .diff-add[ ```scala def updateLatestVisit(targets: List[UserId], now: Instant):Unit = { val users = UserRepository.findByIds(targets) * val updates = users.collect { * case u @ User(_, _, UserStatus.Active) => u.withLastVisit(now) * } * UserRepository.store(updates) } ``` ] --- class: left, top ## データの加工と副作用を分離しよう .diff-rm[ ```scala def updateLatestVisit(targets: List[UserId], now: Instant):Unit = { val users = UserRepository.findByIds(targets) * val updates = users.collect { * case u @ User(_, _, UserStatus.Active) => u.withLastVisit(now) * } UserRepository.store(updates) } ``` ] --- class: left, top ## データの加工と副作用を分離しよう .diff-add[ ```scala *private def newLastVisitUsers( * users: List[User], now: Instant *): List[User] = { * users.collect { * case u @ User(_, _, UserStatus.Active) => u.withLastVisit(now) * } *} def updateLatestVisit(targets: List[UserId], now: Instant):Unit = { val users = UserRepository.findByIds(targets) * val updates = `newLastVisitUsers(users, now)` UserRepository.store(updates) } ``` ] --- class: left, top ## 制約を型やデータ構造で表現しよう |範囲 |1アカウントあたり| |----------|-----| |50人未満 |120円| |100人未満 |140円| |150人未満 |210円| |250人未満 |250円| |上限なし |300円| --- class: left, top ## 制約を型やデータ構造で表現しよう ```scala case class Tier( upperLimit: Option[Int] money: Money ) ``` ```scala case class Tiers(values: List[Tier]) { assert(values.nonEmpty, "ティアは必ず一つ以上") assert(upperLimits.distinct == upperLimits, "上限値は重複しない") assert(upperLimits.sorted == upperLimits, "上限値でソートされてる") assert(values.last.upperLimit.isEmpty, "最後は上限値無し") assert(values.count(_.isEmpty) == 1, "上限値無しは1つだけ") def upperLimits = values.flatMap(_.upperLimit) ... } ``` --- class: left, top ## 制約を型やデータ構造で表現しよう .diff-rm[ ```scala *case class Tiers(`values: List[Tier]`) { * assert(values.nonEmpty, "ティアは必ず一つ以上") assert(upperLimits.distinct == upperLimits, "上限値は重複しない") assert(upperLimits.sorted == upperLimits, "上限値でソートされてる") assert(values.last.upperLimit.isEmpty, "最後は上限値無し") assert(values.count(_.isEmpty) == 1, "上限値無しは1つだけ") def upperLimits = values.flatMap(_.upperLimit) ... } ``` ] --- class: left, top ## 制約を型やデータ構造で表現しよう .diff-add[ ```scala *case class Tiers(`values: NonEmptyList[Tier]`) { * assert(upperLimits.distinct == upperLimits, "上限値は重複しない") assert(upperLimits.sorted == upperLimits, "上限値でソートされてる") assert(values.last.upperLimit.isEmpty, "最後は上限値無し") assert(values.count(_.isEmpty) == 1, "上限値無しは1つだけ") def upperLimits = values.flatMap(_.upperLimit) ... } ``` ] --- class: left, top ## 制約を型やデータ構造で表現しよう .diff-rm[ ```scala *case class Tiers(`values: NonEmptyList[Tier]`) { * assert(upperLimits.distinct == upperLimits, "上限値は重複しない") * assert(upperLimits.sorted == upperLimits, "上限値でソートされてる") * assert(values.last.upperLimit.isEmpty, "最後は上限値無し") * assert(values.count(_.isEmpty) == 1, "上限値無しは1つだけ") def upperLimits = values.flatMap(_.upperLimit) ... } ``` ] --- class: left, top ## 制約を型やデータ構造で表現しよう .diff-add[ ```scala *case class Tiers(`ranges: SortedMap[Int, Money], last: Money`) { def upperLimits = ranges.keys ... } ``` ] --- class: left, top ## 制約を型やデータ構造で表現しよう .diff-rm[ ```scala *case class Tiers(`values: NonEmptyList[Tier]`) { * assert(upperLimits.distinct == upperLimits, "上限値は重複しない") * assert(upperLimits.sorted == upperLimits, "上限値でソートされてる") * assert(values.last.upperLimit.isEmpty, "最後は上限値無し") * assert(values.count(_.isEmpty) == 1, "上限値無しは1つだけ") def upperLimits = values.flatMap(_.upperLimit) ... } ``` ] .diff-add[ ```scala *case class Tiers(`ranges: SortedMap[Int, Money], last: Money`) { def upperLimits = ranges.keys ... } ``` ] --- class: left, top ## 網羅性検査を活用しよう ```scala enum UserStatus { case Active case Inactive } sealed trait User object User { case object Guest extends User case class Regular(name: Name, status: UserStatus) extends User case class Admin(name: Name, status: UserStatus) extends User } ``` --- class: left, top ## 網羅性検査を活用しよう ```scala val users: List[User] = ... user.map { case Regular(name, status) if status == UserStatus.Active => ??? case Admin(name, status) if status == UserStatus.Active => ??? case _ => ??? } ``` --- class: left, top ## 網羅性検査を活用しよう .diff-rm[ ```scala val users: List[User] = ... user.map { * case Regular(name, status) `if status == UserStatus.Active` => ??? * case Admin(name, status) `if status == UserStatus.Active` => ??? case _ => ??? } ``` ] --- class: left, top ## 網羅性検査を活用しよう .diff-add[ ```scala val users: List[User] = ... user.map { * case Regular(name, UserStatus.Active) => ??? * case Admin(name, UserStatus.Active) => ??? case _ => ??? } ``` ] --- class: left, top ## 網羅性検査を活用しよう .diff-rm[ ```scala val users: List[User] = ... user.map { case Regular(name, UserStatus.Active) => ??? case Admin(name, UserStatus.Active) => ??? * case _ => ??? } ``` ] --- class: left, top ## 網羅性検査を活用しよう .diff-add[ ```scala val users: List[User] = ... user.map { case Regular(name, UserStatus.Active) => ??? * case Regular(name, UserStatus.Inctive) => ??? case Admin(name, UserStatus.Active) => ??? * case Admin(name, UserStatus.Inctive) => ??? * case Guest => ??? } ``` ] --- class: left, top ## まとめ - データの加工と副作用を分離しよう - 制約を型やデータ構造で表現しよう - 網羅性検査を活用しよう --- class: left, top ## 宣伝 - Tech to Value では [Online Scala コードレビュー サービス](https://www.t2v.jp/#service)を提供しています。 - チームに Scala熟練者が居ない等、ぜひご相談ください。 - 株式会社アドウェイズさんにて[導入事例を記事にして頂きました。](https://blog.engineer.adways.net/entry/2022/08/12/120000) - アルプ株式会社 では [サブスクリプション事業の販売・請求管理を支援する Scalebase](https://scalebase.com/) というサービスを提供しています。 - SaaS ビジネス等、複雑になりがちな様々な契約情報を正しく管理し、継続課金ビジネスの成長を支えます。 - [今すぐ資料ダウンロード](https://scalebase.com/resources/service_document) --- class: center, middle ## 質問とか --- class: left, top