ことり、穂乃果と一緒に学ぶHaskell(入門)その3「代数的データ型の定義2」

前回

ことり「穂乃果ちゃん、関数の定義方法は授業では習った?」
穂乃果「い…いちおう……。 えーと、こういうのだっけ」

add1 :: Int -> Int
add1 x = x + 1

ことり「:: Int -> Intっていうのは、add1っていう関数にInt -> Intっていう型を付けているんだ Javaに直すとこうかな!」

int add1(int x) { return x + 1; }

ことり「Showは、Showを実装したデータ型であるMemberに対してshow :: Show a => a -> Stringなる関数の実装を与えるよ♪」

data Member = Member String Int
  deriving (Show)

穂乃果「show :: Show a => a -> Stringっていうのは……Showを実装する型aを受け取って、文字列(String)を返す関数show?」
ことり「そのとおりです!」

束の間

ーー ほの邸(お風呂) ーー

穂乃果「ふぅ、一番風呂〜〜! ……んんん〜〜」

湯船バシャーンー

穂乃果「今日はライブ練習に加えて、ことりちゃんのHaskell講座。 楽しかったぁ〜」

穂乃果「なんだっけなー、Showshow :: Show a => a -> Stringを提供する。」
穂乃果「Memberとかのデータ型がShowderivingすると、showを使って値を文字列化できる」
穂乃果「型クラスを実装したデータ型をインスタンスって言って、例えばMemberShowインスタンスって言うんだったよね……」

穂乃果「ふぅ〜〜……」

………

ことり「合ってるよ、穂乃果ちゃん!」
穂乃果「うわぁことりちゃん、なんで窓から出てくるの!!」
ことり「覗いちゃったぁ☆」
穂乃果「覗いちゃったぁ☆ じゃないよ〜!!」


ーー ほの自室 ーー

ことり「あの後、なんだかんだで穂乃果ちゃんとの2人お風呂をしてきた後のことりだよ☆」
穂乃果「も~、なんで窓から覗いてたのー。 言ってくれれば海未ちゃんも呼んでお泊まり会したり、みんなでお風呂入ったりできたのに」
ことり「えっ……♡♡」

穂乃果(みんなでお風呂はさすがに狭いかなぁ……?)
ことり(穂乃果ちゃんと海未ちゃんとお風呂……♡)ハナジブー

穂乃果「うわっ、鼻血鼻血! 大丈夫!? …ことりちゃん、また変なこと考えてたりしたの……?」イブカシゲー
ことり「えっ、あっ、あえーと、えへへ」

ことり「よーし、今夜は型クラスの定義方法までをやっていくよ!」
穂乃果「オッ!?」

代数的データ型の直積型と列挙型、直和型について

ことり「穂乃果ちゃん、前に『代数的データ型の定義』についてやったよね。 実はあれ、まだ途中なんだ」

穂乃果「えっ、あれの他にまだあるの?」
ことり「うん! この前作ったMember……Cの構造体みたいなデータ型、あれは直積型って言うよ!

ことり「その他に列挙型直和型っていうのがあるから、合わせてそれをやっていくよ♪」
穂乃果「わあ、お願いしまーすっ!」

直積型

レコード構文

ことり「まずは直積型のおさらい + αからです」
ことり「前のときに穂乃果ちゃんが『nameとか、ageとかっていう名前は書かないでいいの?』って言ってたよね」


(Haskellでのコード)

data Member = Member String Int

(Javaでのコード)

class Member {
	public String name;
	public int age;
	public Member(String name, int age) {
		this.name = name;
		this.age = age;
	}
}

穂乃果「nameとか、ageっていう名前は書かないでいいの?」
ことり「名前をつけることもできるんだけど、今はまだ置いておいてね」


ことり「Haskellでは名前が必要なときに、レコード構文を使うよ!」

data Member = Member
  { name :: String
  , age :: Int
  }

ことり「値の定義では従来と同じ方法の他に、レコードに直接代入したように見えるような構文が使えるよ」

honoka :: Member
honoka = Member "honoka" 16
honoka :: Member
honoka = Member
  { name = "honoka"
  , age = 16
  }

穂乃果「すごーい! "honoka"とか16が何の値なのかわかりやすくなったー!」
ことり「えへへ。 そして定義したレコードは、データ値からデータ値を取り出す関数として自動実装されるの!」

main :: IO ()
main = do
  putStrLn ("name: " ++ name honoka)
  putStrLn ("age: " ++ show (age honoka))

-- vvv 出力 vvv
-- name: honoka
-- age: 16

ことり「++は、文字列同士を結合する演算子だよ!」

  • (++) :: String -> String -> String
  • ("ほの" ++ "こと") == "ほのこと"

ことり「IntShowインスタンスだから、Int値であるage honokashowされることができるよ」

  • instance Show Int
  • show 10 == "10"

穂乃果「うんーと、name (Member "honoka" 16) == "honoka"ってことかな?」
ことり「うん、その通りです♪」

穂乃果「レコード。 この場合は……nameageが関数になるんだ。 えーと……こう表すことができる?」

  • name :: Member -> String
  • age :: Member -> Int

ことり「さすが穂乃果ちゃん! そう書くことができるよ!」
穂乃果「えへっ! 流石私!」

ことり「でも代数的データ型の面白いところはまだまだこれからだよっー」
穂乃果「おー!」

関数でのパターンマッチ

ことり「代数的データ型の最高最大、一番の武器と言ってもいい。 パターンマッチだよ」

honoka :: Member
honoka = Member
  { name = "honoka"
  , age = 16
  }

main :: IO ()
main = do
  prettyPrintMember honoka

prettyPrintMember :: Member -> IO ()
prettyPrintMember (Member herName herAge) = do
  putStrLn ("name: " ++ herName)
  putStrLn ("age: " ++ show herAge)

穂乃果「prettyPrintMemberMemberを受け取って、それを表示する関数……だよね。 あれ、仮引数ってどこにいっちゃったの?」
ことり「仮引数はちゃんとここにあるよ♪」

--                vvvvvvvv ここ vvvvvvvvv
prettyPrintMember (Member herName herAge) = ...

ことり「これこそがパターンマッチだよ!」
ことり「mainprettyPrintMember (Member "honoka" 16)っていう呼び出しを」
ことり「prettyPrintMember (Member "honoka" 16)っていう風に分解してくれるんだ。  自動的にherName"honoka"herAge16っていう値が束縛してくれます」
穂乃果「束縛?」
ことり「うん。 Haskellでは名前(変数)に値を紐付けることを、束縛って言ったりするよ!

穂乃果「『prettyPrintMember関数が実引数の受け取り時に、"honoka"16herNameherAgeに束縛する』……こんな感じかな」
ことり「そんな感じです♪」

ことり「オブジェクト指向のクラスと比べた代数的データ型の最大の利点は、その網羅性って言われたりするくらいだしね」
ことり「その裏付けをするのがパターンマッチだよ」

穂乃果「も……もうら……せい?」
ことり「えーと、そんなに難しく考える必要はなくって、『データ型からフィールドを簡単に束縛できる』ってくらいでいいよ!」

列挙型

ことり「次は列挙型について」

穂乃果「あれ、Javaにも列挙型ってあったよね。 何か関係があったりするの?」
ことり「関係があるも何も、ほとんど同じものなんだ。 わたしたちμ'sは、こうやって書けるよ」

data Muse = Hanayo | Rin | Maki | Umi | Kotori | Honoka | Eli | Nico | Nozomi

穂乃果「あーっ! 私たちだー!」
ことり「ふふ、あとは適当にShowをderivingして」

data Muse = Hanayo | Rin | Maki | Umi | Kotori | Honoka | Eli | Nico | Nozomi
  deriving (Show)

ことり「簡単に出力できます」

main :: IO ()
main = do
  print Umi
  print Honoka
  print Kotori

-- vvv 出力 vvv
-- Umi
-- Honoka
-- Kotori

ことり「printは、引数をshowしてからputStrLnする関数です」

  • print x = putStrLn (show x)

穂乃果「本当だ。 Javaとおんなじだね」

enum Muse { Hanayo, Rin, Maki, Umi, Kotori, Honoka, Eli, Nico, Nozomi }

public class Program {
	public static void main(String[] args) {
		Muse eli = Muse.Eli;
		System.out.println(eli);
	}
}

// vvv 出力 vvv
// Eli

ことり「これもパターンマッチすることができるよ」

helloTo :: Muse -> IO ()
helloTo Umi    = putStrLn "ウミチャン♡"
helloTo Honoka = putStrLn "ホノカチャン!"
helloTo Kotori = putStrLn "わたしだね"
helloTo x      = putStrLn ("おはようっ♪ > " ++ show x)

main :: IO ()
main = do
  helloTo Umi
  helloTo Honoka
  helloTo Kotori
  helloTo Hanayo
  helloTo Nico
  helloTo Nozomi

-- vvv 出力 vvv
-- ウミチャン♡
-- ホノカチャン!
-- わたしだね
-- おはようっ♪ > Hanayo
-- おはようっ♪ > Nico
-- おはようっ♪ > Nozomi

穂乃果「えっ、helloTo関数の定義がいっぱいあるよ???」
ことり「ふっふっふっ、これもパターンマッチの恩恵なのです」

ことり「Haskellでは具体型の名称、値構築子、代数的データ型の値、型クラスの名称の頭文字は、必ず英字大文字なの」

ことり「値構築子、または値コンストラクタっていうのは、Member型のMember String IntMember ……honokaみたいな値を作るときにStringIntの値を受け取る部分ことだよ」

data Member        -- 具体型の名称
         = Member  -- 値構築子
              String Int

show ::
    a  -- 具体的でない(抽象的な)型の仮名称は頭文字が英字大文字でない
    -> String

data Muse = Hanayo  -- 代数的データ型の値
          | Rin | Maki | Umi | Kotori | Honoka | Eli | Nico | Nozomi
  • Show -- 型クラス名

ことり「そしてこれらを関数の仮引数部に与えたときに、関数でのパターンマッチが起こるんだ」

helloTo :: Muse -> IO ()
helloTo Umi  -- 仮引数部に値が書かれているので、パターンマッチが起こる
    = putStrLn "ウミチャン♡"
helloTo x  -- 仮引数部に名前(変数、具体的な値でないもの)が書かれているので、パターンマッチでない束縛が起こる
    = putStrLn ("おはようっ♪ > " ++ show x)

ことり「直積型でのパターンマッチが起こるのも、同じ理屈だよ♪」

穂乃果「これって……すごいね。 switchじゃないのに、switchみたいに分岐ができてる……」
ことり「でしょ! Haskellはifなどの分岐を極力使わないで書ける言語なんだ!」

直和型

ことり「最後に直和型。 今回はこれで最後だから頑張って☆」
穂乃果「うん! ファイトだよっ!」

ことり「直和型は、列挙型と直積型が合わさったようなもので、列挙子がフィールドを持ちます!」

data Student = OtonokiStudent String Int
             | UranohoshiStudent String Int Bool
  deriving (Show)

ことり「もちろんレコードも使えるよ」

-- 直和型
data Student = OtonokiStudent { otonokiName :: String, otonokiAge :: Int }
             | UranohoshiStudent { uranohoshiName :: String, uranohoshiAge :: Int, likeMikan :: Bool }
  deriving (Show)

ことり「今回のコード例はこちら」
ことり「名前空間がバッティングするので、OtonokiStudentUranohoshiStudentのレコード名は固有のものにしてあるよ」

prettyPrintStudent :: Student -> IO ()
prettyPrintStudent (OtonokiStudent herName herAge) = do
  putStrLn "音ノ木坂学院生徒"
  putStrLn ("name: " ++ herName)
  putStrLn ("age: " ++ show herAge)
prettyPrintStudent (UranohoshiStudent herName herAge sheLikesMikan) = do
  putStrLn "浦の星女学院生徒"
  putStrLn ("name: " ++ herName)
  putStrLn ("age: " ++ show herAge)
  putStrLn ("みかんのこと好き?: " ++ show sheLikesMikan)

main :: IO ()
main = do
  let umi     = OtonokiStudent "園田海未" 16         --
      yoshiko = UranohoshiStudent "ヨハネ" 15 False  -- do式内のletブロックは、複数の名前を定義できるよ
  prettyPrintStudent umi
  prettyPrintStudent yoshiko

-- vvv 出力 vvv
-- 音ノ木坂学院生徒
-- name: 園田海未
-- age: 16
-- 浦の星女学院生徒
-- name: ヨハネ
-- age: 15
-- みかんのこと好き?: False

穂乃果「えーっ! これは……Javaで書くとどうなるんだろう……」
ことり「これはね、ScalaとかKotlinでも使う書き方があって、それで表現できるんだけど……ちょっとJavaだと可読性が悪くなるんだぁ」
ことり「ちょっとコードが長くなるよ……注意してね」

interface Student {}
class OtonokiStudent implements Student {
	public String name;
	public int age;
	public OtonokiStudent(String name, int age) {
		this.name = name;
		this.age = age;
	}
}
class UranohoshiStudent implements Student {
	public String name;
	public int age;
	public boolean likeMikan;
	public UranohoshiStudent(String name, int age, boolean likeMikan) {
		this.name = name;
		this.age = age;
		this.likeMikan = likeMikan;
	}
}

class StudentFunctions {
	public static void prettyPrintStudent(Student she) {
		if (she instanceof OtonokiStudent) {
			OtonokiStudent otonokiGirl = (OtonokiStudent)she;
			System.out.println("音ノ木坂学院生徒");
			System.out.println("name: " + otonokiGirl.name);
			System.out.println("age: " + otonokiGirl.age);
		} else if(she instanceof UranohoshiStudent) {
			UranohoshiStudent uranohoshiGirl = (UranohoshiStudent)she;
			System.out.println("浦の星女学院生徒");
			System.out.println("name: " + uranohoshiGirl.name);
			System.out.println("age: " + uranohoshiGirl.age);
			System.out.println("みかんのこと好き?: " + uranohoshiGirl.likeMikan);
		} else {
			throw new RuntimeException("undefined behavior is detected");
		}
	}
}

public class Program {
	public static void main(String[] args) {
		Student umi = new OtonokiStudent("園田海未", 16);
		Student yoshiko = new UranohoshiStudent("ヨハネ", 15, false);
		StudentFunctions.prettyPrintStudent(umi);
		StudentFunctions.prettyPrintStudent(yoshiko);
	}
}

// vvv 出力 vvv
// 音ノ木坂学院生徒
// name: 園田海未
// age: 16
// 浦の星女学院生徒
// name: ヨハネ
// age: 15
// みかんのこと好き?: false

ことり「表現はHaskellに準拠してるよ」

穂乃果「なるほど……Studentクラスを継承することで、その子としての属性を与えるんだね……」
ことり「うん。 共通フィールドであるnameageStudentに与えてあげてもよあったんだけど、Haskellに合わせて書いてみると、だいたい誰が書いてもこうなるかな」

穂乃果「よしこ……ヨハネ? って誰知り合い?」
ことり「うんん、全然違うよ♪」

おやすみなさい

ーー 就寝(ベッドの中) ーー

穂乃果「代数的データ型ってすごいなあ……そういえば、代数的データ型って何で代数的データ型って言うの?」
ことり「それはことりもわからないの……圏論由来でF代数らしいって聞いたことはあるんだけど……」
穂乃果「圏論?」
ことり「すごい楽しい、数学の学問だよ♪ でも穂乃果ちゃんが当面Haskellをやるに当たって、別に圏論の知識は必要ないから大丈夫だよ! 圏論の知識があったらあったで役に立つけど、なくても大丈夫」

穂乃果「そっかー…………なんかすごそうだね。 パターンマッチもすごかったなぁ……書くのが楽になりそう……」
ことり「パターンマッチは実は関数の仮引数部以外でも、色んなところで使えてね。 それについてはおいおいかな」

穂乃果(Zzz)スピー
ことり(ふふ、おやすみなさい)

…………

(次回に続く)

参考にしたページ


 疑問点があれば、Twitterでリプライくれれば(そのリプライを見逃してなければ)返すよ!
@public_ai000ya - Twitter

筆者プロフィール

my-latest-logo

aiya000(あいや)

せつラボ 〜圏論の基本〜」 「せつラボ2~雲と天使と関手圏~」 「矢澤にこ先輩といっしょに代数!」を書いています!

強い静的型付けとテストを用いて、バグを防ぐのが好き。Haskell・TypeScript。