Slide 1

Slide 1 text

Kanon #fp_matsuri Kotlin で学ぶ 代数的データ型 ysknsid25 ysknsid25.bsky.social

Slide 2

Slide 2 text

まえがき 2 LTでは本当はこういうのやりたくないが... ● 実はJJUGとほぼ同じ内容の話をする ○ このLTは後編であり、ある意味本編 ○ 「どっちか落ちるやろ」と思って出したら両方通ってしまった ● 初見の人 ○ フツーに聞いてください🙏 ● JJUGでも見たよって人 あるいは Java との違いを見たい人 ○ 構成はほぼ同じだけとコードが違うので手元で比べてみてください 🙌 ○ 個人的には Kotlin はやっぱり、よりスマートに書けてると思う ○ https://speakerdeck.com/ysknsid25/java-dexue-bu-dai-shu-de-detaxing

Slide 3

Slide 3 text

⛳ このセッションのゴール ⛳ 3 代数的データ型完全に理解した となる

Slide 4

Slide 4 text

関数型プログラミングといえば? な キーワードを 1つ想像してください 4

Slide 5

Slide 5 text

5 あなたが想像したのはこれですね? 純粋関数

Slide 6

Slide 6 text

代数的データ型 6

Slide 7

Slide 7 text

代数的データ型 = 直積型 & 直和型 7 また知らないワード…

Slide 8

Slide 8 text

8 直積型 代数的データ型 ● ここでは 星座 × 年齢。A × B × C … 掛け算なので直積 ● Kotlin でいうとdata class ● 要はデータクラス 年齢 星座 (蟹, 20) (双子, 30) (射手, 40) (山羊, 20) data class PersonInfo( val constellation: String, val age: Int )

Slide 9

Slide 9 text

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 // うお座 } 牡羊 牡牛 双子 蟹 乙女 天秤 蠍 射手 山羊 水瓶 魚 獅子

Slide 10

Slide 10 text

代数的データ型 = 直積型 & 直和型 10 改めて

Slide 11

Slide 11 text

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が存在する = 活動終了 と表現できないだろうか? 活動中なのに活動終了と判定され る間違いも起きそう

Slide 12

Slide 12 text

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) } データの意味が一目でわかるようになったし、間違いも起こりにくそう

Slide 13

Slide 13 text

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 }

Slide 14

Slide 14 text

14 継承じゃだめなの? 代数的データ型 sealedではなく継承を使ってしまうと、理論上無限の集合が出来上がる = 直和の性質がない YearsActive StillActive ActiveBetween PreviousLife 知らないところで勝手に前世を定義できてしまう もちろん来世も定義できる

Slide 15

Slide 15 text

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は開始と終了だけ出力 }

Slide 16

Slide 16 text

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 でエラーが 出るので修正漏れが出ない

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Happy Hacking !! 水瀬いのり さんが推し の @ysknsid25 @ysknsid25.bsky.social Presented by Kanon でした