Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlinで学ぶ 代数的データ型

Avatar for Kanon Kanon
June 15, 2025

Kotlinで学ぶ 代数的データ型

関数型まつり(#fp_matsuri)の登壇資料です。

Avatar for Kanon

Kanon

June 15, 2025
Tweet

More Decks by Kanon

Other Decks in Technology

Transcript

  1. まえがき 2 LTでは本当はこういうのやりたくないが... • 実はJJUGとほぼ同じ内容の話をする ◦ このLTは後編であり、ある意味本編 ◦ 「どっちか落ちるやろ」と思って出したら両方通ってしまった •

    初見の人 ◦ フツーに聞いてください🙏 • JJUGでも見たよって人 あるいは Java との違いを見たい人 ◦ 構成はほぼ同じだけとコードが違うので手元で比べてみてください 🙌 ◦ 個人的には Kotlin はやっぱり、よりスマートに書けてると思う ◦ https://speakerdeck.com/ysknsid25/java-dexue-bu-dai-shu-de-detaxing
  2. 8 直積型 代数的データ型 • ここでは 星座 × 年齢。A × B

    × C … 掛け算なので直積 • Kotlin でいうとdata class • 要はデータクラス 年齢 星座 (蟹, 20) (双子, 30) (射手, 40) (山羊, 20) data class PersonInfo( val constellation: String, val age: Int )
  3. 9 直和型 代数的データ型 • ここでは 星座 は必ず12個で、牡牛座であり牡牛座であることはありえない。 • 和集合 A

    ∪ B ただし A ∩ B = φ … 直和 • Kotlinでいうとenumやsealed class, sealed interface 星座 enum class Constellation { ARIES, // おひつじ座 TAURUS, // おうし座 GEMINI, // ふたご座 CANCER, // かに座 LEO, // しし座 VIRGO, // おとめ座 LIBRA, // てんびん座 SCORPIO, // さそり座 SAGITTARIUS, // いて座 CAPRICORN, // やぎ座 AQUARIUS, // みずがめ座 PISCES // うお座 } 牡羊 牡牛 双子 蟹 乙女 天秤 蠍 射手 山羊 水瓶 魚 獅子
  4. 11 代数的データ型になっていないコード 代数的データ型 data class PeriodInYears( val start: Int, val

    end: Int? ) fun main() { val p1 = PeriodInYears(1981, null) val p2 = PeriodInYears(1968, 1980) println(p1) // PeriodInYears(start=1981, end=null) println(p2) // PeriodInYears(start=1968, end=1980) } 直積の概念のみが反映された状態のコード 活動期間を表現したい endが存在しない = 活動中 endが存在する = 活動終了 と表現できないだろうか? 活動中なのに活動終了と判定され る間違いも起きそう
  5. 12 代数的データ型になったコード 代数的データ型 // 直和型 sealed interface YearsActive // record(直積型)×

    sealed(直和型)の代数的データ型 data class StillActive(val since: Int) : YearsActive data class ActiveBetween(val start: Int, val end: Int) : YearsActive fun main() { val y1: YearsActive = StillActive(2005) val y2: YearsActive = ActiveBetween(1990, 2000) println(y1) // StillActive(since=2005) println(y2) // ActiveBetween(start=1990, end=2000) } データの意味が一目でわかるようになったし、間違いも起こりにくそう
  6. 13 sealedではなくenumじゃだめなの? 代数的データ型 enum class YearsActive { STILL_ACTIVE { var

    since: Int = -1 override fun setData(a: Int, b: Int) { since = a } }, ACTIVE_BETWEEN { var start: Int = -1 var end: Int = -1 override fun setData(a: Int, b: Int) { start = a end = b } }; abstract fun setData(a: Int, b: Int) } enumで直積の性質を表現するのは無理がある。 以下のようにだいぶ無理があるコードができあがる。var使っとるわ int b を無理やり入れないとだわで、どう考えてもスマートじゃない fun main() { val y1 = YearsActive.STILL_ACTIVE y1.setData(2005, 9999) // 9999ってなんだ・・・ val y2 = YearsActive.ACTIVE_BETWEEN y2.setData(1990, 2000) println(y1) // STILL_ACTIVE println(y2) // ACTIVE_BETWEEN }
  7. 15 さらなる恩恵を受ける 代数的データ型 println()の内容をデータによって切り分ける関数を作るとする // 直和型 sealed interface YearsActive //

    record(直積型)× sealed(直和型)の代数的データ型 data class StillActive(val since: Int) : YearsActive data class ActiveBetween(val start: Int, val end: Int) : YearsActive fun main() { val y1: YearsActive = StillActive(2005) val y2: YearsActive = ActiveBetween(1990, 2000) println(y1) // StillActive(since=2005) // StillActiveならsinceだけ出力 println(y2) // ActiveBetween(start=1990, end=2000) // ActiveBetweenは開始と終了だけ出力 }
  8. 16 代数的データ型の威力 代数的データ型 sealed interface YearsActive data class StillActive(val since:

    Int) : YearsActive data class ActiveBetween(val start: Int, val end: Int) : YearsActive fun getYearsMessage(ya: YearsActive): String = when (ya) { is StillActive -> "Still active since ${ya.since}" is ActiveBetween -> "Active between ${ya.start} and ${ya.end}" } fun main() { val y1: YearsActive = StillActive(2005) val y2: YearsActive = ActiveBetween(1990, 2000) println(getYearsMessage(y1)) println(getYearsMessage(y2)) } • データの不整合が起こり得ない ◦ 先のenumでみた9999とか • YearsActiveでとりうる値の範囲が限 定される ことで、switchにデフォルトが 生えない ◦ コンパイル時点でデータの整合 性が担保される • 逆にYearsActiveに新たな値が増えた 場合は、getYearsMessage でエラーが 出るので修正漏れが出ない
  9. 17 まとめ • 代数的データ型 = 直積型 & 直和型 ◦ enumは直積の性質がない

    ◦ 継承は直和の性質がない • 代数的データ型を使うことでより明示的なシグネチャを定義できる • 代数的データ型を使うことで取りうるデータの範囲を型で表現できる • 代数的データ型はコンパイル時点での品質保証に寄与する • Kotlinは後継だけあって、同じことがより短いコードで出来てる