「すごいHaskellたのしく学ぼう!」第7章

型や型クラスを
         自分で作ろう
    第3回 スタートHaskell2
       2012年8月19日
        @a_hisame
自己紹介

• あひさめ(@a_hisame)
• 社会人2年目
  • 今のところはSIerさんのお仕事メイン
• Haskell学習は3度目の正直
• プログラミング、アナログゲーム全般、料理・お酒など
7.1 データ型

• 様々なデータ型
• Bool, Int, Char, Maybe, ... etc
• dataキーワードで新しいデータ型を定義


-- Bool型を定義
data Bool = False | True


                                      新しいデータ型「Bool」を定義
                          このBool型はFalse、または、Trueの値を取り得る
                                    (これらは値コンストラクタでもある)
                データ型名、値コンストラクタは大文字で始まる必要がある
7.2 形づくる

-- フィールドの必要ないデータ型
data Bool = False | True


-- フィールドを持つデータ型
data Shape = Circle Float Float Float
  | Rectangle Float Float Float Float


ghci > :t Rectangle
Rectangle :: Float -> Float -> Float -> Float -> Shape

                                    Circle, Rectangleは値コンストラクタ
                  その後に渡す値を持つ、定義したデータ型の値を返す関数
7.2 形づくる


• 図形(Shape)の面積を求める関数を定義


area :: Shape -> Float
area ( Circle _ _ r ) = pi * r ^ 2
area ( Rectangle x1 y1 x2 y2 ) =
                       (abs $ x2-x1) * (abs $ y2 - y1)



               area関数はShape型を受け取り、その面積(Float)を返す関数
                 引数のShape型を受け取る部分でパターンマッチを行い
                                 型ごとに適切な実装を行っている
                 (この関数は任意のShape型の値を引数として受け取れる)
7.2 形づくる
• ghci上で次のコードを実行する
data Shape = Circle Float Float Float
  | Rectangle Float Float Float Float
Circle 10 20 5

        <interactive>:19:1:
           No instance for (Show Shape)
            arising from a use of `print'
           Possible fix: add an instance declaration for (Show Shape)
           In a stmt of an interactive GHCi command: print it




      Shapeというデータ型の値を文字列として表示する方法が分かりません
       (ghci上で実行した場合、その値の文字列表現を結果として表示する)
7.2 形づくる
• Shape型の定義に次の一文を追加

data Shape = Circle Float Float Float
  | Rectangle Float Float Float Float
  deriving (Show)
Circle 10 20 5




                              deriving (Show) をつけることで、
              Shapeデータ型の各値を文字列でどのように表現するかを
                          自動的に実装してくれる(詳しくは後述)
7.2 形づくる
• ところで・・・
data Shape = Circle Float Float Float
  | Rectangle Float Float Float Float
  deriving (Show)
Circle 10 20 5

        Circleの10, 20, 5というのは何を表している?
        Circle [中心のX座標] [中心のY座標] [円の半径] を想定
        しているが、この順番を間違えてしまうかも。



      Shapeというデータ型の値を文字列として表示する方法が分かりません
       (ghci上で実行した場合、その値の文字列表現を結果として表示する)
7.2 形づくる
• Point型を定義
data Point = Point Float Float deriving (Show)
data Shape = Circle Point Float
  | Rectangle Point Point deriving (Show)
Circle (Point 10 20) 5

  Circle [中心座標] [円の半径] ということを型で表現できる




                                  データ型を表現するために
                   新たなデータ型を定義して使用することができる
7.2 形づくる


• 図形(Shape)の面積を求める関数を修正


area :: Shape -> Float
area ( Circle _ r ) = pi * r ^ 2
area ( Rectangle (Point x1 y1) (Point x2 y2) ) =
                       (abs $ x2-x1) * (abs $ y2 - y1)




          area関数はShape型を受け取り、その面積(Float)を返す関数(修正版)
              データ型の中のデータ型もパターンマッチの対象にできる。
7.2 形づくる
• モジュールとしてエクスポート
module Shapes (
  Point(..), Shape(Circle, Rectangle),
  area, baseCircle
) where

        データ型、値コンストラクタ(関数)、関数のエクスポート


-- 値コンストラクタをエクスポートしない場合
module Shapes (Point, Shape) where


      モジュールとしてデータ型、値コンストラクタを個別にエクスポートできる
                  値コンストラクタを提供せず、生成用の関数を提供することで
                    値コンストラクタを使ったパターンマッチはできなくなるが
                          既存のプログラムを壊さずに型の内部表現が変更可能
7.3 レコード構文

• 名前付きのフィールドを備えたデータ型の作成
data Person = Person {
  firstName :: String
, lastName :: String
, age :: Int
, height :: Float
, phoneNumber :: String
, flavor :: String } deriving (Show)

   フィールド名 :: フィールドの型

                                  作成したデータ型のフィールドに
                        それぞれ名前を持たせたデータ型を作成できる
7.3 レコード構文

• 値の作成と自動生成される関数
-- フィールド名の関数(データ型 -> フィールド)を自動生成する

ghci> :t flavor
flavor :: Person -> String

-- レコード型の値を生成する(カンマ区切りでフィールドに値を設定)

ghci> let p = Person { firstName = “Buddy”,
 lastName = “Finklestein”, age = 43, height = 184.2,
 phoneNumber = “526-2928”, flavor = “Chocolate” }


           レコード型のパラメータを名前付きで全て指定する(順序は自由)
モジュールで値コンストラクタの指定をしなくてもレコード型の値を生成できる
 Q. レコード型の値をエクスポートするときにそれを公開しないようにできる?
7.4 型引数

• 値コンストラクタと型コンストラクタ

data Point = Point Float Float deriving (Show)
data Maybe a = Nothing | Just a
                      このaが型引数

ghci> Point 5 10      (型を与えることで使用する具体型が決定される)

ghci> Point “x” “y”    -- ERROR
ghci> Just 10          -- (Num a) => Maybe a
ghci> Just “string”    -- Maybe [Char]

       値コンストラクタ:値を受け取って新しい値(Ex. Point 5.0 10.0)を作る
          型コンストラクタ:型を受け取って新しい型(Ex. Maybe Int)を作る
                 (この例では単なるMaybeという型の値は存在できないが
                                Maybe Intという型の値は存在できる)
7.4 型引数

• 様々な型を格納するデータ型が作れる

data Maybe a = Nothing | Just a

-- もしも型引数がなければ...

data IntMaybe = INothing | IJust Int
data StrMaybe = SNothing | SJust String

ghci> Just 10 :: Maybe Int
ghci> Just “string” :: Maybe String



                    異なるデータ型ごとにデータを作る必要がなくなる
7.4 型引数
• 多相的
functionA :: Maybe Int -> String
functionB :: Maybe String -> String


ghci> :t []
[] :: [a] -- ([] a)と意味は同じ
ghci> :t (++)
(++) :: [a] -> [a] -> [a]

       MaybeのNothingはfunctionA, functionBのどちらの引数としても扱える
                               Nothingの型はMaybe aであるため
                     (aの型の値を持たないので、aを決定する必要がない)
                 同様に空リストは任意の型引数を取るリストとして扱える
7.4 型引数
• 型引数の使いどころ
どんな型をそこに持ってきても変わらず
動作するようなデータ構造
Ex. Maybe, List, Map, Stack, Queue など


• データ型宣言時のルール
データ型宣言には型クラス制約をつけない
-- 順序型クラスの制約を持つ型を型引数に取るMaybe

data (Ord a) => Maybe' a = Nothing' | Just' a

             不必要に型引数を使いすぎると返って煩雑にしすぎてしまう
                     データに制約をつけず、関数の方に制約をつける
          (データにつけるとオプションが無い限りコンパイル時エラー)
7.5 インスタンスの自動導出
• 型クラスの自動導出
データ型の宣言 deriving (自動導出する型クラス)


• 自動導出可能な型クラス(P.28-32)
Eq(等価判定), Ord(順序付け),
Enum(列挙可能), Bounded(上限下限あり),
Show(文字列に変換可能),
Read(文字列から変換可能)
7.5 インスタンスの自動導出
• 自動導出全部入り
data Day = Monday | Tuesday | Wednesday |
 Thursday | Friday | Saturday | Sunday
 deriving
  (Eq, Ord, Show, Read, Bounded, Enum)


ghci> Monday == Monday    -- Eq
True
ghci> Friday < Sunday     -- Ord
True

                         書いた値で、最初に書いた値ほど小さい
7.5 インスタンスの自動導出
• 動作の確認
ghci> show Monday            -- Show
“Monday”
ghci> read “Monday” :: Day   -- Read
True

ghci> [minBound .. maxBound] :: [Day]
[Monday | Tuesday | Wednesday | Thursday |
 Friday | Saturday | Sunday]

                             最後の例はBoundedとEnumの複合
                  最小の要素(Bounded)と最大の要素(Bounded)を含む
                              Day型の値を列挙していく(Enum)
7.6 型シノニム
• 既存データ型に別の名前を与える
type String = [Char]
type IntMaybe = Maybe Int
type Pair a b = (a, b)
type MyStr = String
testStr :: MyStr -> String
testStr s = s

                   コンパイルエラー
ghci> Pair 1 2
ghci> testStr (“abc” :: [Char])

                                  別の名前を与えるだけの機能であり
                        値コンストラクタを作ってくれるわけではない
        別名であるので、同じものを指すのであれば関数の引数に引き渡せる
7.6 型シノニム
• 型引数を2つ取る型の例
data Either a b = Left a | Right b
 deriving (Eq, Ord, Read, Show)

ghci> :t Left True
Left True :: Either True b
ghci> :t Right ‘a’
Right ‘a’ :: Either a Char



                      Leftは後の型引数bを多相のまま残して失敗を表し
                     Rightは先の型引数aを多相のまま残して成功を表す
         Q. なぜLeftを失敗に使い、Rightを成功に使うのか?(Rightだから?)
7.6 型シノニム

• 失敗した時になぜ失敗したのかを返せる
on1to10 :: Int -> Either Ordering Int
on1to10 x
 | x < 1 = Left LT -- value less than [1,10]
 | x > 10 = Left GT -- value greater than [1,10]
 | otherwise = Right x -- value on [1,10]




                   整数が範囲内であるかどうかを判定する関数on1to10
                    範囲外である場合に、引数が小さいor大きいという
                        型情報Orderingの値を失敗値Leftと一緒に返す
7.7 再帰的なデータ構造
• フィールドに自分自身の型を持つデータ型
data List’ a = Empty | Cons a (List’ a)
  deriving (Show, Read, Eq, Ord)

ghci> Empty
Empty
ghci> 4 `Cons` (5 `Cons` Empty)
Cons 4 (Cons 5 Empty)
ghci> :t Cons
Cons :: a -> List' a -> List' a

             List’はEmpty、もしくはCons値コンストラクタで作成された値
             ConsコンストラクタはaとList’ aを取ってList’ aの値を作成する
7.7 再帰的なデータ構造
• イメージでつかもう
data List’ a = Empty | Cons a (List’ a)
  deriving (Show, Read, Eq, Ord)


 List’
            Empty
                       |       Cons a (List’ a)


                                           1 : 2 : 3 : []


1 `Cons` 2 `Cons` 3 `Cons`           Empty
7.7 再帰的なデータ構造
• 中置記号で書き直す              コロン(:)から始まる記号のみの値コ
                         ンストラクタは中置関数になる
infixr 5 :-:
data List’ a = Empty | a :-: (List’ a)
   deriving (Show, Read, Eq, Ord)

ghci> 1 :-: 2 :-: Empty
ghci> (1 :-: ( 2 :-: Empty))



            infixl or infixrで右結合か左結合かを指定する(省略すると右結合)
         また、結合性の優先順位を指定する(数字が大きいほど優先して結合)
                     省略した場合はinfixl 9が指定されていることと同じ
             右結合を指定しない場合は下のように括弧をつける必要がある
7.7 再帰的なデータ構造
• リストを連結する関数を実装
infixr 5 ^++
(^++) :: List' a -> List' a -> List' a
(^++) Empty ys = ys
(^++) (x :-: xs) ys = x :-: (xs ^++ ys)



                  記号だけで関数を実装すると、自動的に中置記法になる
                          関数の実装にパターンマッチを使用している
     パターンマッチとは値コンストラクタをマッチさせることに他ならない!
                               (Emptyも:-:も値コンストラクタである)
             Q. 組み込みのリストでは[]を値コンストラクタとして使えるが
                このように記号を値コンストラクタにできる実装はある?
7.7 再帰的なデータ構造
• 木構造(2分探索木)
data Tree a =
  EmptyTree | Node a (Tree a) (Tree a)
  deriving (Show)

Tree
        EmptyTree       Node a (Tree a) (Tree b)



 Nodeは値aと左の木、右の木を持つ
7.7 再帰的なデータ構造

Tree
       EmptyTree   Node a (Tree a) (Tree b)

                            P.139に描かれている木の例


                   5

         3                     7

   1           4        6             8
7.7 再帰的なデータ構造

• 木構造(2分[探索]木)
data Tree a =
  EmptyTree | Node a (Tree a) (Tree a)
  deriving (Show)




            この定義だけでは、ただの2分木を表現しているに過ぎない
       そのため、上記のデータ型では2分探索木では無いTreeも表現できる
          Q. 型引数に型クラス制約をつけないというルールはあったが、
     データ構造上の制約をデータ型の定義の段階で入れることはあるのか?
7.7 再帰的なデータ構造
• 2分探索木を作成するための関数を定義
singleton :: a -> Tree a
singleton x = Node x EmptyTree EmptyTree

treeInsert :: (Ord a) => a -> Tree a -> Tree a
treeInsert :: x EmptyTree = singleton x
treeInsert :: x (Node v left right)
  | x == v = Node v left right
  | x < v = Node v (treeInsert x left) right
  | x > v = Node v left (treeInsert x right)

                             木が空なら、新たなノードを作成する
木に値がある場合、挿入する値が小さければ左へ、値が大きければ右へ挿入する
7.7 再帰的なデータ構造

• 2分探索木に特定の値があるかを探索する関数
treeElem :: (Ord a) => a -> Tree a -> Bool
treeElem :: x EmptyTree = False
treeElem :: x (Node v left right)
  | x == v = True
  | x < v = treeElem x left
  | x > v = treeElem x right




               第2引数のTree aが2分探索木であれば正しい答えを返す
                       Q. このような時にInvalidな値を渡された場合
                            Haskellではどのように対処するのか?
7.7 再帰的なデータ構造

• 2分探索木の作成
let nums = [8,6,4,1,7,3,5]
let tree = foldr treeInsert EmptyTree nums

*Main> :t foldr
foldr :: (a -> b -> b) -> b -> [a] -> b

                       foldrにはa,bの2匹数を受け取りbを返す関数(第1引数)と
                                           その関数に与える初期値(第2引数)
                           および関数に順に与える値のリスト(第3引数)を渡す
                                ここでは、まずtreeInsert 5 EmptyTreeが行われ
                            その戻り値(Node 5 EmptyTree EmptyTree)に対して
                            treeInsert 3 (Node 5 EmptyTree EmptyTree) が行われ
                    ...と続いていき最終的にP.139の2分探索木を最終的に得る
7.8 型クラス中級講座

• WARNING!

この先に進む前に
型クラスという言葉に対して
手続き型言語にでてくる「クラス」については
とりあえず忘れて欲しいな!




                 型クラスは特定の振る舞いを表現するものであり、
             JavaやPythonのクラスではなくインタフェースのようなもの
        型クラスの振る舞いは、型クラス関数を定義することで得られる
7.8 型クラス中級講座

• Eq型クラスの実装               Eq型クラスの定義の始まり
                          a: Eqのインスタンスになる型を表す
class Eq a where
 (==) :: a -> a -> Bool
 (/=) :: a -> a -> Bool
                           Eq型クラスのインスタンスであれば
 x == y = not (x /= y)     使用できる関数 ( == と /= )
 x /= y = not (x == y)
   型クラス関数のデフォルト実装
   (省略可能)


                 ある型をEq型クラスのインスタンスにしようとする場合
                      定義されている全ての関数を実装する必要がある
              Eq型クラスの場合はデフォルト実装が相互再帰しているので
                              どちらか一方を書き換えるだけでよい
7.8 型クラス中級講座
• Eq型クラスのインスタンスの実装
data TrafficLight = Red | Yellow | Green
instance Eq TrafficLight where
  Red == Red = True
  Yellow == Yellow = True
  Green == Green = True
  _ == _ = False            Eq型クラスの関数(==)の実装

-- x /= y = not (x == y)    (パターンマッチを利用)



                    関数(/=)は関数(==)を用いて定義されているので
                   関数(==)の実装を上書きすることでEq型クラスで
                      必要とされる関数の実装を行うことができる
                                (型クラスの最小完全性定義)
7.8 型クラス中級講座
• 型クラスのサブクラス
class (Eq a) => Num a where
 ...

                    2つのインスタンス関係のイメージ

 Eq型クラスの
 インスタンス
                              Num型クラスの
                              インスタンス


                        型クラスNumのインスタンスとなる型は
                      型クラスEqのインスタンスである必要がある
7.8 型クラス中級講座
• 多層型(Maybe)のインスタンス

instance (Eq m) => Eq (Maybe m) where
  Just x == Just y = x == y
  Nothing == Nothing = True
  _ == _ = False

                      Eq型クラスのインスタンスである型mを
                      持つ(Maybe m)の具体型に対して
                      Eq型クラスのインスタンスを定義する
                      (これはmに対して==を使用しているため)



      逐一型ごとに(Maybe Int, Maybe Char, ...)インスタンスを実装せずとも
      型を単に変数と記述してインスタンスを実装することが許されている
7.8 型クラス中級講座
• 型クラスについての情報
ghci> :info Num
class Num a where
  (+) :: a -> a -> a
  (*) :: a -> a -> a
  (-) :: a -> a -> a
  negate :: a -> a
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a
       -- Defined in `GHC.Num'
instance Num Integer -- Defined in `GHC.Num'
instance Num Int -- Defined in `GHC.Num'
instance Num Float -- Defined in `GHC.Float'
instance Num Double -- Defined in `GHC.Float'


                            :info [型クラス名]で、型クラスの性質を示す関数と
                     実装されているインスタンス一覧を表示することができる
7.9 YesとNoの型クラス
• 型クラスを自作する
class YesNo a where
 yesno :: a -> Bool

• 自作の型クラスを独自実装する(例: Maybe)
instance (YesNo m) => YesNo (Maybe m) where
  yesno (Just x) = yesno x
  yesno _ = False

                       C言語やJavaScriptの条件判定がtrueとなる
          (ifの中に記述するとtrueと判断されるもの)を表現する型クラス
                        使用する時は型指定が必要なこともある
                      (yesno 0だけではIntと推論してくれなかった)
7.9 YesとNoの型クラス
• If式(っぽいもの)を自作する

yesnoIf :: (YesNo y) => y -> a -> a -> a
yesnoIf yesnoVal tValue fValue
 | yesno yesnoVal = tValue
 | otherwise = fValue




                                 Haskellでは遅延評価であるため
                     tValue, fValueの値が必要になるまで評価されない
                                     一方で正格評価な言語では
                     この関数が実行される段階でtValue, fValueともに
                               評価が終わっていなければならない
7.10 Functor型クラス
• 全体を写せる(map over)ものの型クラス

  class Functor something where
   fmap (a -> b) -> something a -> something b


       1, 2, 3, ..., 26
                                                       型クラス
  型クラス                    Int -> Char              something Char
something Int
                                        ‘A’, ‘B’, ‘C’, ..., ‘Z’

         ある型がFunctorであるならば、その型に対するfmapが提供されている
           fと書くと単なる関数のように見えたのでsomethingで書き直してある
7.10 Functor型クラス

• Maybe, List, TreeなどはFunctor型クラスのインスタンス

 instance Functor Maybe where
   fmap f (Just x) = Just (f x)
   fmap f Nothing = Nothing


class Eq a where             class Functor something where
 (==) :: a -> a -> Bool       fmap (a -> b) ->
 (/=) :: a -> a -> Bool         something a -> something b



                          Functor型クラスのインスタンスとして指定する型は
                           型を1つ取る型コンストラクタを持つ必要がある
                (Functorは具体型では無く、型コンストラクタを要求している)
7.10 Functor型クラス

• 1つの型コンストラクタを持てば良いのでEitherでもOK

instance Functor (Either a) where
 fmap f (Right rx) = Right $ f rx
 fmap f (Left lx) = Left lx       Either aのaは型変数
                                  任意のaに対してのインスタ
                                  ンスを実装
ghci> fmap (*2) (Right 10)
Right 20
ghci> fmap (*2) (Left “ABC”)
Left “ABC”

               fmapの第1匹数はRightの値の型bを別の型に変換する関数
                 Leftに使われる型aに対しては写像関数fの対象ではない
7.11 型を司るもの、種類
• :kコマンドで型の種類を知ることができる
ghci> :k Int
Int :: *
ghci> :k Maybe
Maybe :: * -> *
ghci> :k Maybe Int
Maybe Int :: *
ghci> :k Either String
Either String :: * -> *

                                       *は具体型 (Intなど)
       * -> *は具体型を1つ取って具体型を返す型コンストラクタ (Maybeなど)
     * -> * -> *は具体型を2つ取って具体型を返す型コンストラクタ (Eitherなど)
                              なお、部分適用を行うことも可能
演習問題1
• 木の最大の深さを求める関数を実装せよ。ただし、空の木
 の深さは0とし、P.139の木の深さを3とする

getMaxDepth :: Tree a -> Int
(以下を実装する)


• 2分探索木の中から最大の値を取得する関数を実装せよ

getMaxNode :: (Ord a) => Tree a -> Maybe a
(以下を実装する)


引数に渡されるTree aは正しい2分探索木であると
仮定してよい
演習問題2
• 型クラスMyStackを次のように定義する

class MyStack s where
 push :: a -> s a -> s a
 pop :: s a -> Maybe (a, s a)
 peek :: s a -> Maybe a


この時、標準で提供されるリストをMyStackのインスタンス
として定義せよ

instance MyStack [] where
  (以下を実装する)
演習問題1: 解答例

getMaxDepth :: Tree a -> Int
getMaxDepth EmptyTree = 0
getMaxDepth (Node _ left right) =
 1 + max (getMaxDepth left) (getMaxDepth right)


  左と右の木の深い方の木に、1を加えたものが自分の深さ


getMaxNode :: (Ord a) => Tree a -> Maybe a
getMaxNode EmptyTree = Nothing
getMaxNode (Node x _ EmptyTree) = Just x
getMaxNode (Node _ _ right) = getMaxNode right


2分探索木なので、最も右側にあるノードが最大の値を保持する
演習問題2: 解答例

instance MyStack [] where
  push x xs = x:xs
  pop [] = Nothing
  pop (x:xs) = Just (x, xs)
  peek [] = Nothing
  peek (x:_) = Just x


• push: 要素を先頭に追加したリストを返す
• pop: 要素があれば、先頭から取り出した値と先頭を取り除
 いたリストを返す

• peek: 要素があれば、先頭の値を返す
演習問題2: 動作例


ghci> let stack = foldr push [] [1,2,3,4,5]
ghci> stack
[1,2,3,4,5]
ghci> peek stack
Just 1
ghci> pop stack
Just (1,[2,3,4,5])
ghci> pop []
Nothing

 

  • 1.
    「すごいHaskellたのしく学ぼう!」第7章 型や型クラスを 自分で作ろう 第3回 スタートHaskell2 2012年8月19日 @a_hisame
  • 2.
    自己紹介 • あひさめ(@a_hisame) • 社会人2年目 • 今のところはSIerさんのお仕事メイン • Haskell学習は3度目の正直 • プログラミング、アナログゲーム全般、料理・お酒など
  • 3.
    7.1 データ型 • 様々なデータ型 •Bool, Int, Char, Maybe, ... etc • dataキーワードで新しいデータ型を定義 -- Bool型を定義 data Bool = False | True 新しいデータ型「Bool」を定義 このBool型はFalse、または、Trueの値を取り得る (これらは値コンストラクタでもある) データ型名、値コンストラクタは大文字で始まる必要がある
  • 4.
    7.2 形づくる -- フィールドの必要ないデータ型 dataBool = False | True -- フィールドを持つデータ型 data Shape = Circle Float Float Float | Rectangle Float Float Float Float ghci > :t Rectangle Rectangle :: Float -> Float -> Float -> Float -> Shape Circle, Rectangleは値コンストラクタ その後に渡す値を持つ、定義したデータ型の値を返す関数
  • 5.
    7.2 形づくる • 図形(Shape)の面積を求める関数を定義 area:: Shape -> Float area ( Circle _ _ r ) = pi * r ^ 2 area ( Rectangle x1 y1 x2 y2 ) = (abs $ x2-x1) * (abs $ y2 - y1) area関数はShape型を受け取り、その面積(Float)を返す関数 引数のShape型を受け取る部分でパターンマッチを行い 型ごとに適切な実装を行っている (この関数は任意のShape型の値を引数として受け取れる)
  • 6.
    7.2 形づくる • ghci上で次のコードを実行する dataShape = Circle Float Float Float | Rectangle Float Float Float Float Circle 10 20 5 <interactive>:19:1: No instance for (Show Shape) arising from a use of `print' Possible fix: add an instance declaration for (Show Shape) In a stmt of an interactive GHCi command: print it Shapeというデータ型の値を文字列として表示する方法が分かりません (ghci上で実行した場合、その値の文字列表現を結果として表示する)
  • 7.
    7.2 形づくる • Shape型の定義に次の一文を追加 dataShape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show) Circle 10 20 5 deriving (Show) をつけることで、 Shapeデータ型の各値を文字列でどのように表現するかを 自動的に実装してくれる(詳しくは後述)
  • 8.
    7.2 形づくる • ところで・・・ dataShape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show) Circle 10 20 5 Circleの10, 20, 5というのは何を表している? Circle [中心のX座標] [中心のY座標] [円の半径] を想定 しているが、この順番を間違えてしまうかも。 Shapeというデータ型の値を文字列として表示する方法が分かりません (ghci上で実行した場合、その値の文字列表現を結果として表示する)
  • 9.
    7.2 形づくる • Point型を定義 dataPoint = Point Float Float deriving (Show) data Shape = Circle Point Float | Rectangle Point Point deriving (Show) Circle (Point 10 20) 5 Circle [中心座標] [円の半径] ということを型で表現できる データ型を表現するために 新たなデータ型を定義して使用することができる
  • 10.
    7.2 形づくる • 図形(Shape)の面積を求める関数を修正 area:: Shape -> Float area ( Circle _ r ) = pi * r ^ 2 area ( Rectangle (Point x1 y1) (Point x2 y2) ) = (abs $ x2-x1) * (abs $ y2 - y1) area関数はShape型を受け取り、その面積(Float)を返す関数(修正版) データ型の中のデータ型もパターンマッチの対象にできる。
  • 11.
    7.2 形づくる • モジュールとしてエクスポート moduleShapes ( Point(..), Shape(Circle, Rectangle), area, baseCircle ) where データ型、値コンストラクタ(関数)、関数のエクスポート -- 値コンストラクタをエクスポートしない場合 module Shapes (Point, Shape) where モジュールとしてデータ型、値コンストラクタを個別にエクスポートできる 値コンストラクタを提供せず、生成用の関数を提供することで 値コンストラクタを使ったパターンマッチはできなくなるが 既存のプログラムを壊さずに型の内部表現が変更可能
  • 12.
    7.3 レコード構文 • 名前付きのフィールドを備えたデータ型の作成 dataPerson = Person { firstName :: String , lastName :: String , age :: Int , height :: Float , phoneNumber :: String , flavor :: String } deriving (Show) フィールド名 :: フィールドの型 作成したデータ型のフィールドに それぞれ名前を持たせたデータ型を作成できる
  • 13.
    7.3 レコード構文 • 値の作成と自動生成される関数 --フィールド名の関数(データ型 -> フィールド)を自動生成する ghci> :t flavor flavor :: Person -> String -- レコード型の値を生成する(カンマ区切りでフィールドに値を設定) ghci> let p = Person { firstName = “Buddy”, lastName = “Finklestein”, age = 43, height = 184.2, phoneNumber = “526-2928”, flavor = “Chocolate” } レコード型のパラメータを名前付きで全て指定する(順序は自由) モジュールで値コンストラクタの指定をしなくてもレコード型の値を生成できる Q. レコード型の値をエクスポートするときにそれを公開しないようにできる?
  • 14.
    7.4 型引数 • 値コンストラクタと型コンストラクタ dataPoint = Point Float Float deriving (Show) data Maybe a = Nothing | Just a このaが型引数 ghci> Point 5 10 (型を与えることで使用する具体型が決定される) ghci> Point “x” “y” -- ERROR ghci> Just 10 -- (Num a) => Maybe a ghci> Just “string” -- Maybe [Char] 値コンストラクタ:値を受け取って新しい値(Ex. Point 5.0 10.0)を作る 型コンストラクタ:型を受け取って新しい型(Ex. Maybe Int)を作る (この例では単なるMaybeという型の値は存在できないが Maybe Intという型の値は存在できる)
  • 15.
    7.4 型引数 • 様々な型を格納するデータ型が作れる dataMaybe a = Nothing | Just a -- もしも型引数がなければ... data IntMaybe = INothing | IJust Int data StrMaybe = SNothing | SJust String ghci> Just 10 :: Maybe Int ghci> Just “string” :: Maybe String 異なるデータ型ごとにデータを作る必要がなくなる
  • 16.
    7.4 型引数 • 多相的 functionA:: Maybe Int -> String functionB :: Maybe String -> String ghci> :t [] [] :: [a] -- ([] a)と意味は同じ ghci> :t (++) (++) :: [a] -> [a] -> [a] MaybeのNothingはfunctionA, functionBのどちらの引数としても扱える Nothingの型はMaybe aであるため (aの型の値を持たないので、aを決定する必要がない) 同様に空リストは任意の型引数を取るリストとして扱える
  • 17.
    7.4 型引数 • 型引数の使いどころ どんな型をそこに持ってきても変わらず 動作するようなデータ構造 Ex.Maybe, List, Map, Stack, Queue など • データ型宣言時のルール データ型宣言には型クラス制約をつけない -- 順序型クラスの制約を持つ型を型引数に取るMaybe data (Ord a) => Maybe' a = Nothing' | Just' a 不必要に型引数を使いすぎると返って煩雑にしすぎてしまう データに制約をつけず、関数の方に制約をつける (データにつけるとオプションが無い限りコンパイル時エラー)
  • 18.
    7.5 インスタンスの自動導出 • 型クラスの自動導出 データ型の宣言deriving (自動導出する型クラス) • 自動導出可能な型クラス(P.28-32) Eq(等価判定), Ord(順序付け), Enum(列挙可能), Bounded(上限下限あり), Show(文字列に変換可能), Read(文字列から変換可能)
  • 19.
    7.5 インスタンスの自動導出 • 自動導出全部入り dataDay = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday deriving (Eq, Ord, Show, Read, Bounded, Enum) ghci> Monday == Monday -- Eq True ghci> Friday < Sunday -- Ord True 書いた値で、最初に書いた値ほど小さい
  • 20.
    7.5 インスタンスの自動導出 • 動作の確認 ghci>show Monday -- Show “Monday” ghci> read “Monday” :: Day -- Read True ghci> [minBound .. maxBound] :: [Day] [Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday] 最後の例はBoundedとEnumの複合 最小の要素(Bounded)と最大の要素(Bounded)を含む Day型の値を列挙していく(Enum)
  • 21.
    7.6 型シノニム • 既存データ型に別の名前を与える typeString = [Char] type IntMaybe = Maybe Int type Pair a b = (a, b) type MyStr = String testStr :: MyStr -> String testStr s = s コンパイルエラー ghci> Pair 1 2 ghci> testStr (“abc” :: [Char]) 別の名前を与えるだけの機能であり 値コンストラクタを作ってくれるわけではない 別名であるので、同じものを指すのであれば関数の引数に引き渡せる
  • 22.
    7.6 型シノニム • 型引数を2つ取る型の例 dataEither a b = Left a | Right b deriving (Eq, Ord, Read, Show) ghci> :t Left True Left True :: Either True b ghci> :t Right ‘a’ Right ‘a’ :: Either a Char Leftは後の型引数bを多相のまま残して失敗を表し Rightは先の型引数aを多相のまま残して成功を表す Q. なぜLeftを失敗に使い、Rightを成功に使うのか?(Rightだから?)
  • 23.
    7.6 型シノニム • 失敗した時になぜ失敗したのかを返せる on1to10:: Int -> Either Ordering Int on1to10 x | x < 1 = Left LT -- value less than [1,10] | x > 10 = Left GT -- value greater than [1,10] | otherwise = Right x -- value on [1,10] 整数が範囲内であるかどうかを判定する関数on1to10 範囲外である場合に、引数が小さいor大きいという 型情報Orderingの値を失敗値Leftと一緒に返す
  • 24.
    7.7 再帰的なデータ構造 • フィールドに自分自身の型を持つデータ型 dataList’ a = Empty | Cons a (List’ a) deriving (Show, Read, Eq, Ord) ghci> Empty Empty ghci> 4 `Cons` (5 `Cons` Empty) Cons 4 (Cons 5 Empty) ghci> :t Cons Cons :: a -> List' a -> List' a List’はEmpty、もしくはCons値コンストラクタで作成された値 ConsコンストラクタはaとList’ aを取ってList’ aの値を作成する
  • 25.
    7.7 再帰的なデータ構造 • イメージでつかもう dataList’ a = Empty | Cons a (List’ a) deriving (Show, Read, Eq, Ord) List’ Empty | Cons a (List’ a) 1 : 2 : 3 : [] 1 `Cons` 2 `Cons` 3 `Cons` Empty
  • 26.
    7.7 再帰的なデータ構造 • 中置記号で書き直す コロン(:)から始まる記号のみの値コ ンストラクタは中置関数になる infixr 5 :-: data List’ a = Empty | a :-: (List’ a) deriving (Show, Read, Eq, Ord) ghci> 1 :-: 2 :-: Empty ghci> (1 :-: ( 2 :-: Empty)) infixl or infixrで右結合か左結合かを指定する(省略すると右結合) また、結合性の優先順位を指定する(数字が大きいほど優先して結合) 省略した場合はinfixl 9が指定されていることと同じ 右結合を指定しない場合は下のように括弧をつける必要がある
  • 27.
    7.7 再帰的なデータ構造 • リストを連結する関数を実装 infixr5 ^++ (^++) :: List' a -> List' a -> List' a (^++) Empty ys = ys (^++) (x :-: xs) ys = x :-: (xs ^++ ys) 記号だけで関数を実装すると、自動的に中置記法になる 関数の実装にパターンマッチを使用している パターンマッチとは値コンストラクタをマッチさせることに他ならない! (Emptyも:-:も値コンストラクタである) Q. 組み込みのリストでは[]を値コンストラクタとして使えるが このように記号を値コンストラクタにできる実装はある?
  • 28.
    7.7 再帰的なデータ構造 • 木構造(2分探索木) dataTree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show) Tree EmptyTree Node a (Tree a) (Tree b) Nodeは値aと左の木、右の木を持つ
  • 29.
    7.7 再帰的なデータ構造 Tree EmptyTree Node a (Tree a) (Tree b) P.139に描かれている木の例 5 3 7 1 4 6 8
  • 30.
    7.7 再帰的なデータ構造 • 木構造(2分[探索]木) dataTree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show) この定義だけでは、ただの2分木を表現しているに過ぎない そのため、上記のデータ型では2分探索木では無いTreeも表現できる Q. 型引数に型クラス制約をつけないというルールはあったが、 データ構造上の制約をデータ型の定義の段階で入れることはあるのか?
  • 31.
    7.7 再帰的なデータ構造 • 2分探索木を作成するための関数を定義 singleton:: a -> Tree a singleton x = Node x EmptyTree EmptyTree treeInsert :: (Ord a) => a -> Tree a -> Tree a treeInsert :: x EmptyTree = singleton x treeInsert :: x (Node v left right) | x == v = Node v left right | x < v = Node v (treeInsert x left) right | x > v = Node v left (treeInsert x right) 木が空なら、新たなノードを作成する 木に値がある場合、挿入する値が小さければ左へ、値が大きければ右へ挿入する
  • 32.
    7.7 再帰的なデータ構造 • 2分探索木に特定の値があるかを探索する関数 treeElem:: (Ord a) => a -> Tree a -> Bool treeElem :: x EmptyTree = False treeElem :: x (Node v left right) | x == v = True | x < v = treeElem x left | x > v = treeElem x right 第2引数のTree aが2分探索木であれば正しい答えを返す Q. このような時にInvalidな値を渡された場合 Haskellではどのように対処するのか?
  • 33.
    7.7 再帰的なデータ構造 • 2分探索木の作成 letnums = [8,6,4,1,7,3,5] let tree = foldr treeInsert EmptyTree nums *Main> :t foldr foldr :: (a -> b -> b) -> b -> [a] -> b foldrにはa,bの2匹数を受け取りbを返す関数(第1引数)と その関数に与える初期値(第2引数) および関数に順に与える値のリスト(第3引数)を渡す ここでは、まずtreeInsert 5 EmptyTreeが行われ その戻り値(Node 5 EmptyTree EmptyTree)に対して treeInsert 3 (Node 5 EmptyTree EmptyTree) が行われ ...と続いていき最終的にP.139の2分探索木を最終的に得る
  • 34.
    7.8 型クラス中級講座 • WARNING! この先に進む前に 型クラスという言葉に対して 手続き型言語にでてくる「クラス」については とりあえず忘れて欲しいな! 型クラスは特定の振る舞いを表現するものであり、 JavaやPythonのクラスではなくインタフェースのようなもの 型クラスの振る舞いは、型クラス関数を定義することで得られる
  • 35.
    7.8 型クラス中級講座 • Eq型クラスの実装 Eq型クラスの定義の始まり a: Eqのインスタンスになる型を表す class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool Eq型クラスのインスタンスであれば x == y = not (x /= y) 使用できる関数 ( == と /= ) x /= y = not (x == y) 型クラス関数のデフォルト実装 (省略可能) ある型をEq型クラスのインスタンスにしようとする場合 定義されている全ての関数を実装する必要がある Eq型クラスの場合はデフォルト実装が相互再帰しているので どちらか一方を書き換えるだけでよい
  • 36.
    7.8 型クラス中級講座 • Eq型クラスのインスタンスの実装 dataTrafficLight = Red | Yellow | Green instance Eq TrafficLight where Red == Red = True Yellow == Yellow = True Green == Green = True _ == _ = False Eq型クラスの関数(==)の実装 -- x /= y = not (x == y) (パターンマッチを利用) 関数(/=)は関数(==)を用いて定義されているので 関数(==)の実装を上書きすることでEq型クラスで 必要とされる関数の実装を行うことができる (型クラスの最小完全性定義)
  • 37.
    7.8 型クラス中級講座 • 型クラスのサブクラス class(Eq a) => Num a where ... 2つのインスタンス関係のイメージ Eq型クラスの インスタンス Num型クラスの インスタンス 型クラスNumのインスタンスとなる型は 型クラスEqのインスタンスである必要がある
  • 38.
    7.8 型クラス中級講座 • 多層型(Maybe)のインスタンス instance(Eq m) => Eq (Maybe m) where Just x == Just y = x == y Nothing == Nothing = True _ == _ = False Eq型クラスのインスタンスである型mを 持つ(Maybe m)の具体型に対して Eq型クラスのインスタンスを定義する (これはmに対して==を使用しているため) 逐一型ごとに(Maybe Int, Maybe Char, ...)インスタンスを実装せずとも 型を単に変数と記述してインスタンスを実装することが許されている
  • 39.
    7.8 型クラス中級講座 • 型クラスについての情報 ghci>:info Num class Num a where (+) :: a -> a -> a (*) :: a -> a -> a (-) :: a -> a -> a negate :: a -> a abs :: a -> a signum :: a -> a fromInteger :: Integer -> a -- Defined in `GHC.Num' instance Num Integer -- Defined in `GHC.Num' instance Num Int -- Defined in `GHC.Num' instance Num Float -- Defined in `GHC.Float' instance Num Double -- Defined in `GHC.Float' :info [型クラス名]で、型クラスの性質を示す関数と 実装されているインスタンス一覧を表示することができる
  • 40.
    7.9 YesとNoの型クラス • 型クラスを自作する classYesNo a where yesno :: a -> Bool • 自作の型クラスを独自実装する(例: Maybe) instance (YesNo m) => YesNo (Maybe m) where yesno (Just x) = yesno x yesno _ = False C言語やJavaScriptの条件判定がtrueとなる (ifの中に記述するとtrueと判断されるもの)を表現する型クラス 使用する時は型指定が必要なこともある (yesno 0だけではIntと推論してくれなかった)
  • 41.
    7.9 YesとNoの型クラス • If式(っぽいもの)を自作する yesnoIf:: (YesNo y) => y -> a -> a -> a yesnoIf yesnoVal tValue fValue | yesno yesnoVal = tValue | otherwise = fValue Haskellでは遅延評価であるため tValue, fValueの値が必要になるまで評価されない 一方で正格評価な言語では この関数が実行される段階でtValue, fValueともに 評価が終わっていなければならない
  • 42.
    7.10 Functor型クラス • 全体を写せる(mapover)ものの型クラス class Functor something where fmap (a -> b) -> something a -> something b 1, 2, 3, ..., 26 型クラス 型クラス Int -> Char something Char something Int ‘A’, ‘B’, ‘C’, ..., ‘Z’ ある型がFunctorであるならば、その型に対するfmapが提供されている fと書くと単なる関数のように見えたのでsomethingで書き直してある
  • 43.
    7.10 Functor型クラス • Maybe,List, TreeなどはFunctor型クラスのインスタンス instance Functor Maybe where fmap f (Just x) = Just (f x) fmap f Nothing = Nothing class Eq a where class Functor something where (==) :: a -> a -> Bool fmap (a -> b) -> (/=) :: a -> a -> Bool something a -> something b Functor型クラスのインスタンスとして指定する型は 型を1つ取る型コンストラクタを持つ必要がある (Functorは具体型では無く、型コンストラクタを要求している)
  • 44.
    7.10 Functor型クラス • 1つの型コンストラクタを持てば良いのでEitherでもOK instanceFunctor (Either a) where fmap f (Right rx) = Right $ f rx fmap f (Left lx) = Left lx Either aのaは型変数 任意のaに対してのインスタ ンスを実装 ghci> fmap (*2) (Right 10) Right 20 ghci> fmap (*2) (Left “ABC”) Left “ABC” fmapの第1匹数はRightの値の型bを別の型に変換する関数 Leftに使われる型aに対しては写像関数fの対象ではない
  • 45.
    7.11 型を司るもの、種類 • :kコマンドで型の種類を知ることができる ghci>:k Int Int :: * ghci> :k Maybe Maybe :: * -> * ghci> :k Maybe Int Maybe Int :: * ghci> :k Either String Either String :: * -> * *は具体型 (Intなど) * -> *は具体型を1つ取って具体型を返す型コンストラクタ (Maybeなど) * -> * -> *は具体型を2つ取って具体型を返す型コンストラクタ (Eitherなど) なお、部分適用を行うことも可能
  • 46.
    演習問題1 • 木の最大の深さを求める関数を実装せよ。ただし、空の木 の深さは0とし、P.139の木の深さを3とする getMaxDepth:: Tree a -> Int (以下を実装する) • 2分探索木の中から最大の値を取得する関数を実装せよ getMaxNode :: (Ord a) => Tree a -> Maybe a (以下を実装する) 引数に渡されるTree aは正しい2分探索木であると 仮定してよい
  • 47.
    演習問題2 • 型クラスMyStackを次のように定義する class MyStacks where push :: a -> s a -> s a pop :: s a -> Maybe (a, s a) peek :: s a -> Maybe a この時、標準で提供されるリストをMyStackのインスタンス として定義せよ instance MyStack [] where (以下を実装する)
  • 48.
    演習問題1: 解答例 getMaxDepth ::Tree a -> Int getMaxDepth EmptyTree = 0 getMaxDepth (Node _ left right) = 1 + max (getMaxDepth left) (getMaxDepth right) 左と右の木の深い方の木に、1を加えたものが自分の深さ getMaxNode :: (Ord a) => Tree a -> Maybe a getMaxNode EmptyTree = Nothing getMaxNode (Node x _ EmptyTree) = Just x getMaxNode (Node _ _ right) = getMaxNode right 2分探索木なので、最も右側にあるノードが最大の値を保持する
  • 49.
    演習問題2: 解答例 instance MyStack[] where push x xs = x:xs pop [] = Nothing pop (x:xs) = Just (x, xs) peek [] = Nothing peek (x:_) = Just x • push: 要素を先頭に追加したリストを返す • pop: 要素があれば、先頭から取り出した値と先頭を取り除 いたリストを返す • peek: 要素があれば、先頭の値を返す
  • 50.
    演習問題2: 動作例 ghci> letstack = foldr push [] [1,2,3,4,5] ghci> stack [1,2,3,4,5] ghci> peek stack Just 1 ghci> pop stack Just (1,[2,3,4,5]) ghci> pop [] Nothing