Haskell学習中のメモ
関数を定義する
関数名 :: 引数の型、 -> 戻り値の型
とすることで指定した型の引数を受け取り戻り値の型を返す関数を定義できる。以下のように指定すると7を受け取った場合は"LUCKY NUMBER SEVEN!"を返し、それ以外の場合は"Sorry, youre out of luck, pal!"を返すようになる。引数に対して何を返すかは定義された引数の順番でパターンマッチして最初に見つかったものを返すようにしている。そのためlucky x
が先に定義されているのであれば毎回"Sorry, youre out of luck, pal!"を返すようになる。
lucky :: Int -> String lucky 7 = "LUCKY NUMBER SEVEN!" lucky x = "Sorry, youre out of luck, pal!"
高階関数
Haskellでは関数を引数として受け取ったり、戻り値として関数を返すことができて、このような関数を高階関数と呼びます。このようなことができるのはHaskellの関数にカリー化の機能が備わっており、全ての関数は引数を一つ受け取り引数が一つ指定された関数または戻り値を返すものと見なすことが出来る。 例えば以下のような関数があったとする。
multiThree :: Int -> Int -> Int -> Int multiThree x y z = x * y * z
この関数を利用して引数2つを受けとる関数を以下のように定義することが出来る。
multiTwoNine :: Int -> Int -> Int multiTwoNine = multiThree 9
multiTwoNine = multiThree 9
の部分で2つの引数と9を掛け合わせる関数をmultiTwoNineに代入している。
関数を引数として受け取る場合は以下のように定義できる。
applyTwice :: (a -> a) -> a -> a
では'引数を一つ受け取り同じ型を戻りとして返す関数'と'引数に使う関数の引数の型'を受け取り'引数に使う関数の戻り値の型'を返す関数として扱われる。以下ではmultiTwoNine 5
で生成した関数をapplyTwiceの第一引数として利用している。applyTwice f x = f ( f x )
の部分は関数合成でapplyTwiceに渡した関数を2回連続で実行(2度目の実行は1度目の結果を引数として受け取る)するようになる。
applyTwice :: (a -> a) -> a -> a applyTwice f x = (f . f ) x print $ applyTwice (multiTwoNine 5) 10
ラムダ式
ラムダ式による無名関数を利用することもできる。例えば引数2つと9を掛け合わせるラムダ式であれば、以下のようになる。下の(\x y -> x * y * 9)
の部分がラムダ式になる。
print $ (\x y -> x * y * 9) 5 4
map
リストに関しては関数の逐次処理をmapで行うことが出来る。
print $ map (applyTwice (multiTwoNine 5)) [10, 11, 12, 13]
型、型クラス
Haskellの型システムには以下のような機能がある
- 型チェック
引数の型と戻り値の型から式が出し以下を静的にチェックする
- 多相性
再利用性を高めるためhaskellの型には多層
以下のように汎用の型t, t1を使っている(パラメータ多層)場合は、実行時に具体的な型が決まる動きをする
1 == 1
や "OCaml" == "OCaml"
のように同じ関数名で別の実装を定義できるアドホック多層がある(オブジェクト指向にオーバーロードに相当する)
- 型推論
型が不明なσ機に対してなるべく汎用的な型を割り当てるようにする
型コンストラクタと型引数
型の記述に使われる識別子を型コンストラクタという。Maybeなど引数を受け取る型コンストラクタについて、Intを与えてMaybe Int
とするときこのIntのような型コンストラクタに与える引数を型引数と呼ぶ。型コンストラクタが型引数が必要かどうかはGHCiで:k 型コンストラクタ
を実行することで確認できるMaybeの場合はMaybe :: * -> *
になり、これは任意の型が型引数として必要なことを表している。Intの場合はInt :: *
でこれは型引数が必要でないことを表している。この:k
の実行結果をカインドという。
型変数
idやheadのように型が異なっていても同じ実装を使いまわせる関数は型変数を用いて実装されている。:t
で確認するとそれぞれ以下のようになる。
id :: a -> a
, head :: [a] -> a
ここではa, [a]が型変数で使われており、通常は小文字1文字が使われることが多い。
型制約
特定の型クラスに所属する型のみ引数として受け取れるように制限でき、これを型制約と言います。例えばshow関数の型制約は以下のようになっています。
show :: Show a => a -> String
これはShow型クラスに属しているクラスのみがshowメソッドの引数として使用できるという制約になる。
自作の型クラスをShow型クラスに所属させる場合は以下のようにderivingで指定すれば良い。
data MyData = I Int deriving (Show)
代数的データ型を定義する
代数データ型により型同士を組み合わせて新しい型を作ることが出来る。代数データ型の定義にはデータコンストラクタを使用する。例えばInt型、Bool型、String型の3つのフィールドをもちデータコンストラクタにNewEmployeeを使用するEmployee型は以下のように定義できる。
data Employee = NewEmployee Integer Bool String
以下のように定義することもできる。
data Employee = NewEmployee {age :: Integer, isManager :: Bool, name :: String}
インスタンスの生成及びデータの取り出しは以下のように行える。
ghci>employee = NewEmployee 30 False "abc" ghci>NewEmployee age isManager name = employee ghci>name "abc"
複数のデータコンストラクタを持つデータ型の定義
データコンストラクタが複数ある場合は |
つなぎで定義できる。
data CmdOption = COptInt Integer | COptBool Bool | COptStr String
データの正格性フラグ
代数データ型ではデータコンストラクタの引数がデフォルトで非正格となっている。非性格の引数については値が入っていなくてもコンストラクタの結果を評価できるが、正格な引数がundefinedの場合はデータコンストラクタの結果を利用することができない。
data LazyAndStrict = LazyAndStrict { lsLazy :: Int, lsStrict :: !Int }
ここではlsStrictが非正格なので以下のようにlsStrictがundefinedの場合はlsLazyを取得するのにも失敗する。
ghci>lsStrict $ LazyAndStrict undefined 2 2 ghci>lsLazy $ LazyAndStrict 1 undefined *** Exception: Prelude.undefined CallStack (from HasCallStack): error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err undefined, called at <interactive>:143:26 in interactive:Ghci44
フィールドの値の差し替え
Haskellの変数は全てイミュータブルであり生成済みのデータ型に対して値を更新するということができず、フィールドの値を差し替えたい場合はコピーして生成することになります。 例えば生成済みのEmployee型のデータに対して年齢を一つ増やしたデータを生成する場合は以下のようなことが出来る。
employee' :: Employee -> Employee employee' employee = employee {employeeIsManager = True , employeeAge = employeeAge employee + 1 }
型シノニム
typeキーワードを使うことで型に別名をつけることが出来る。これを型シノニムという。Integer型にAgeという別名をつける場合は以下のようになる。
type Age = Integer
newtype
型シノニムと似た概念としてnewtypeがある。型シノニムとの違いとして、型シノニムは既存のクラスの別名として使用する(Integerの型シノニムとしてAgeを定義したらAge型の値はIntegerが引数の関数に利用できる。)がnewtypeは別のクラスとして定義される。またコンストラクタ名が必要になるという違いもある。
ghci>newtype NTIndexed a = NewNTIndexed (Integer, a) deriving Show ghci>x = NewNTIndexed (11, "eleven") ghci>:type x x :: NTIndexed [Char] ghci>x NewNTIndexed (11,"eleven")
型の別名だけを外部に公開すると、内部の実装を隠蔽し実装の修正が行いやすくなる。
型クラス
javaでいうインターフェースのようなもので、例えばserializableを実装したクラスから生成したインスタンスがシリアライズ可能なことを表すように、Show型クラスに属するデータ型の値はshowメソッドの引数として利用できる。型クラスは以下のように定義できる。
class 型クラス名 関数名 where 関数名1 :: 型名1 関数名1のデフォルト実装 関数名2 :: 型名2 ...
型クラスの定義では各関数でのデフォルト実装を定義しますが、データの型毎での関数を定義したい場合はインスタンスの定義を行います。以下のようなフォーマットになります。
instance 型クラス名 型名 ( インスタンス名 ) where 関数名 = 式
例えば以下のような代数データ型を定義しておいて
data Human = Human String deriving (Show, Read, Eq) data Dog = Dog deriving (Show, Read, Eq) data Cat = Cat deriving (Show, Read, Eq)
型クラスとして以下を定義する。
class Greeting a where name :: a -> String hello :: a -> String hello _ = "..." -- hello関数のデフォルトの実装 bye :: a -> String bye _ = "..." -- bye関数のデフォルトの実装 instance Greeting Human where name (Human n) = n hello h = "Hi, I'm " ++ name h ++ "." bye _ = "See you." instance Greeting Dog where name _ = "a dog" hello _ = "Bark!" instance Greeting Cat where name _ = "a cat" bye _ = "meow"
これでHumanデータ型はinstanceで定義されたメソッドが呼び出されDogデータ型ではhello関数がinstance定義でbyeは型クラスでのデフォルトが適用される。
ファンクター型クラス
ファンクター、アプリカティブファンクター、モナドはややこしいですが有効に使えれば便利な機能になるのかと思います。まずはファンクター型クラスを確認していきたいと思います。実装は以下のようになっています。
ghci>:i Functor class Functor (f :: * -> *) where fmap :: (a -> b) -> f a -> f b (<$) :: a -> f b -> f a {-# MINIMAL fmap #-} -- Defined in ‘GHC.Base’ instance Functor (Either a) -- Defined in ‘Data.Either’ instance Functor [] -- Defined in ‘GHC.Base’ instance Functor Maybe -- Defined in ‘GHC.Base’ instance Functor IO -- Defined in ‘GHC.Base’ instance Functor ((->) r) -- Defined in ‘GHC.Base’ instance Functor ((,) a) -- Defined in ‘GHC.Base’
クラス定義よりFunctor型クラスはfmap、(<$)の2つの関数が定義されていることが条件になることがわかります。fmapの定義を直接見にいっても同様に以下のようになっています。
ghci>:t fmap fmap :: Functor f => (a -> b) -> f a -> f b
これより、fmapの定義はa型を引数に受け取りb型を返す関数
とf aの戻り値の型
を引数としてf bの結果
を返すように見えましたが、関数a -> b
をとって関数f a -> 関数 f b
を返すようでこの操作を関数の持ち上げ(liftup)というようです。 f a
の値としてはJust 1
などが使えるのですが、Justの定義を見るとMaybeを返すことがわかり
ghci>:t Just Just :: a -> Maybe a
Maybeの定義を確認すると以下のようにずらずらと表示されFunctorの型クラスに所属していることがわかります。
ghci>:i Maybe data Maybe a = Nothing | Just a -- Defined in ‘GHC.Base’ instance Eq a => Eq (Maybe a) -- Defined in ‘GHC.Base’ instance Monad Maybe -- Defined in ‘GHC.Base’ instance Functor Maybe -- Defined in ‘GHC.Base’ instance Ord a => Ord (Maybe a) -- Defined in ‘GHC.Base’ instance Read a => Read (Maybe a) -- Defined in ‘GHC.Read’ instance Show a => Show (Maybe a) -- Defined in ‘GHC.Show’ instance Applicative Maybe -- Defined in ‘GHC.Base’ instance Foldable Maybe -- Defined in ‘Data.Foldable’ instance Traversable Maybe -- Defined in ‘Data.Traversable’ instance Monoid a => Monoid (Maybe a) -- Defined in ‘GHC.Base’
fmap (*5) Just 3
を実行するとJust 15
を返しますが、これは関数Justに渡す引数に対してfmapの第一引数として渡している(*5)
を適用してその結果をJustに渡している動きになります。
ghci>:t fmap (*5) fmap (*5) :: (Num b, Functor f) => f b -> f b
fmap (*5)
の定義を見るとファンクター値を渡したらファンクター値を返すことがわかります。
リストを利用する場合は、fmap (*5) [1,2,3,4,5]
のようにするとリストの中の各値に対して*5
を適用した結果のリストが得られます。
アプリカティブファンクター
次にアプリカティブファンクターを確認していきます。定義を確認すると以下のようになりApplicative型クラスに属する場合はFunctorに属させる必要があることがわかります。
class Functor f => Applicative (f :: * -> *) where pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b (*>) :: f a -> f b -> f b (<*) :: f a -> f b -> f a {-# MINIMAL pure, (<*>) #-} -- Defined in ‘GHC.Base’ instance Applicative (Either e) -- Defined in ‘Data.Either’ instance Applicative [] -- Defined in ‘GHC.Base’ instance Applicative Maybe -- Defined in ‘GHC.Base’ instance Applicative IO -- Defined in ‘GHC.Base’ instance Applicative ((->) a) -- Defined in ‘GHC.Base’ instance Monoid a => Applicative ((,) a) -- Defined in ‘GHC.Base’
pureは値を引数にとり、その値を包んだアプリカティブ値を返します。fmap (*5)
はアプリカティブ値を引数に取るためfmap (*5) 3
は実行できませんがfmap (*5) (pure 3)
は実行することができます。
次に(<*>)
ですが定義はfmapに似ていることがわかります。引数が(a -> b)
からf (a -> b)
に変わっているという違いがありまして、fmapは関数のファンクター値の中の値に適用してくれるのに対して、<*>
は関数の入っているファンクター値と値の入っているファンクター値を引数にとって、1つ目のファンクターの中身である関数を2つ目のファンクターの中身に適用します。
ghci>Just (*3) <*> Just 12 Just 36
ではJust (*3)
を関数の入っているファンクター値、Just 12
を値の入っているファンクター値として<*>
に渡し、Just 36
を1つ目のファンクターの中身である関数を2つ目のファンクターの中身に適用した結果として返します。<*>
はファンクターの中に入った関数適用の結果をファンクターの外に取り出して関数適用することができるので、以下のように連続できの適用も行うことができます。
pure (+) <*> Just 2 <*> Just 3
また<<$>
を使うと<*>
の第一引数として関数の入っているファンクター値
ではなく関数を渡せるようになります。先ほどの例でしたら以下のように修正できます。
(+) <$> Just 2 <*> Just 3
リスト同士で適用する場合は、各要素に対して第一引数の関数の入っているファンクター値の中身である関数を第2引数の各要素に適用します。
例えば[(*0), (+100), (*10)] <*> [1, 2, 3]
を実行すると以下のようになります。
ghci>[(*0), (+100), (*10)] <*> [1, 2, 3] [0,0,0,101,102,103,10,20,30]
モナド
次にモナドの型クラスです。モナドはファンクター、アプリカティブファンクターの強化版であり機能を兼ねそろえています。(ファンクターでは文脈付きの値を保持でき、アプリカティブファンクターでは文脈を持ったまま中の値に関数を適用できるようになった)モナドではそれらに加え普通の値aをとって文脈付きの値を返す関数に、文脈付きの値m aを渡せるようにする
という機能があります。ここでのmはファンクターのことでf a
の代わりにモナドであることをわかりやすくするようにm a
としています。Monad型クラスの実装を確認して見ます。
ghci>:i Monad class Applicative m => Monad (m :: * -> *) where (>>=) :: m a -> (a -> m b) -> m b (>>) :: m a -> m b -> m b return :: a -> m a fail :: String -> m a {-# MINIMAL (>>=) #-} -- Defined in ‘GHC.Base’ instance Monad (Either e) -- Defined in ‘Data.Either’ instance Monad [] -- Defined in ‘GHC.Base’ instance Monad Maybe -- Defined in ‘GHC.Base’ instance Monad IO -- Defined in ‘GHC.Base’ instance Monad ((->) r) -- Defined in ‘GHC.Base’ instance Monoid a => Monad ((,) a) -- Defined in ‘GHC.Base’
モナドの機能は(>>=) :: m a -> (a -> m b) -> m b
からも確認できます。
試しに実行してみます。
ghci>Just 3 >>= \x -> Just (x+1) Just 4
return :: a -> m a
についてはpureと同じようで引数の値をアプリカティブ値に包んで返します。
MaybeはMonad型クラスに属しているので引数として渡すことができます。ここでは普通の値aをとって文脈付きの値を返す\x -> Just (x+1)
に対して文脈付きの値Just 3
を渡しています。
関数の結果としてNothingを返すこともできます。
ghci>Just 1 >>= \x -> if x > 2 then Just (x+1) else Nothing Nothing
次にサンプルで確認してみたいと思います。まずモナドを使わない例として以下を定義する。
type Birds = Int type Pole = (Birds, Birds) landLeftA :: Birds -> Pole -> Pole landLeftA n (left, right) = (left + n, right) landRightA :: Birds -> Pole -> Pole landRightA n (left, right) = (left, right + n) x -: f = f x
ここではBirds
とPole
の2つの型シノニムを使います。landLeftA, landRightAでPoleの左右にとまっているBirdの数を管理します。x -: f = f x
は関数と引数の順番を逆にする働きをします。
これより(0, 0) -: landLeftA 1 -: landRightA 1 -: landLeftA 2
とlandLeftA 2 (landRightA 1 (landLeftA 1 (0, 0)))
は同じ動きをします。
このlandLeftの機能を拡張しPoleの左右のBirdの数の差が4以上の場合は結果をNothingにするという場合はモナドを使えば簡単に修正できます。landLeftA, landRightAの戻り値が値を持つかNothingの状態を持つのでMaybeを返すようにします。
landLeft :: Birds -> Pole -> Maybe Pole landLeft n (left, right) | abs ((left + n) - right) < 4 = Just (left + n, right) | otherwise = Nothing landRight :: Birds -> Pole -> Maybe Pole landRight n (left, right) | abs (left - (right + n)) < 4 = Just (left, right + n) | otherwise = Nothing
それから先ほどと同じように文脈付きの値に包まれたPoleに対してlandLeft, landRightを連続で読んでいき、途中で左右のBirdの数の差が4以上になったらNothingが返されるか確認します。以下を実行してみるとNothingが帰ってますが、return (0, 0) >>= landLeft 1 >>= landRight 4 >>= landLeft (-1)
の時点で左右の差が4になってNothingを返すはずなので大丈夫そうです。
print $ return (0, 0) >>= landLeft 1 >>= landRight 4 >>= landLeft (-1) >>= landRight (-2)
return (0, 0)
としていますがreturnはpureと同じで値をとってファンクター値にしています。それから>>=を連続で使いファンクターの中の値に関数を適用しているのがわかるかと思います。