13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Maybe自作から学ぶHaskell!

Last updated at Posted at 2017-12-15

haskell Advent Calendar 2017に参加しました!!
https://qiita.com/advent-calendar/2017/haskell3

はい、というわけで! 初心者ですが頑張って書きます!

やること

説明を交えながら、Maybeを自作していく。
型クラスもやる。

なぜMaybeを自作しようと思ったのか。

Haskellを理解するには、実際に何らかの既存の型を自作してみて、体に馴染ませていくのが良いと思ったからである。

#Maybeとは
Maybeは標準で使える代数データ型の一つであり、何らかの計算が成功・失敗したかを安全に扱える便利なやつである。
計算が成功すると、Just <計算結果>を返し、
失敗なら、Nothingを返す。

#Maybeの代数データ型を定義してみる
というわけで、オリジナルのMaybe型を定義しよう。
名前は…うーん……Elipmocで!!
data Elipmoc a = Bat | Good a
実際に使用してみよう。


data Elipmoc a = Bat | Good a 

main::IO()
main = do
    print . Good $ 2
    print . Good $ "hogehoge"
    print (Bat::Elipmoc Int)

ファイル名はMain.hsにした
コンパイルはめんどいのでrunghcで直接実行する。
$ stack runghc Main.hs
実行結果

Main.hs:5:5: error:
    ? No instance for (Show (Elipmoc a0)) arising from a use of ‘print’
    ? In the first argument of ‘(.)’, namely ‘print’
      In the expression: print . Good
      In a stmt of a 'do' block: print . Good $ 2
以下略

おっとエラーになってしまったぞ。

#derivingしよう
先程の例のprint関数は内部でshow関数を使っている。
Show型クラスはオブジェクトから文字列を返すshow関数を定義しており。
ElipmocはShow型クラスのインスタンスでないので、show関数が使えずエラーとなった。
とりあえず、Maybeが持つ型クラスを調べてみることにしよう。
REPLを使用する。

$ stack ghci

―略―

Prelude> :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’

Maybe型の持つ型クラスがずらずら列挙された。
この内Eq,Ord,Read,Showはderivingを使って自動でインスタンスにできるので、早速やってみる。

修正後のコード

data Elipmoc a = Bat | Good a deriving (Eq,Ord,Read,Show)

main::IO()
main = do
    print . Good $ 2
    print . Good $ "hogehoge"
    print (Bat::Elipmoc Int)

実行結果

$ stack runghc Main.hs
Good 2
Good "hogehoge"
Bat

うん、ちゃんとできた。
ついでに、Eq,Ord,Readについても確かめてみよう。
その前に各種型クラスの説明をすこしだけ。

Eqは==と/=演算子が定義されていて、
等しいかどうかを調べることができる型クラスだ。

OrdはEqを継承した型クラスであり、
<と<=と>と>=演算子を定義されていて、
大小比較ができる型クラスだ。

ReadはShowクラスとは逆で、
文字列からオブジェクトを構成するread関数が定義されている。

Eq,Ord,Readも試してみよう。

data Elipmoc a = Bat | Good a deriving (Eq,Ord,Read,Show)

main::IO()
main = do
    print (Good 2 == Good 2)
    print (Good 5 /= Good 5)
    print (Good 20 > Good 2)
    print $ Good 20 < Good 8
    print $ (read "Bat"::Elipmoc Int)
    print $ (read "Good 35"::Elipmoc Int)

実行結果

True
False
True
False
Bat
Good 35

Monoid実装

Monoidは二項演算の型クラスである。

定義を示す

class Monoid a where
    mempty :: a 
    mappend :: a -> a -> a 
    mconcat :: [a] -> a
    mconcat = foldr mappend mempty

memptyは単位元を返す関数で、
mappendは2つの値を結合して1つの値にする関数だ。
例としてリストで説明しよう。
リストには++という二項演算子がある。
この演算子は
( ( [1]++[2] )++[3] )
でも
( [1]++( [2]++[3] ) )
でも
結果は[1,2,3]であり、どの順番で計算しても問題無いことがわかる。
このような性質をMonoidと呼び、
haskellではMonoid型クラスのインスタンスにすることができる。
このとおり、リストはMonoid型クラスのインスタンスであるので、
上記のコードをMonoid型クラスのmappendで書き換えることができる。
[1] `mappend` [2] `mappend` [3]
このように、リストでmappendを使うと++演算子と同じ働きをする。
Monoidになるためにはさらにもう一つ条件があり、それは単位元が定義できることだ。
単位元とは、 a `mappend` e → a を満たすeがあることである。
例えばリストの単位元は[]である。
[1]++[] → [1] 
ほらね。
Monoid型クラスではmemptyとして定義されている。
上記をmemptyで書き換えると
[1] `mappend` mempty → [1]

で、MaybeもMonoidのインスタンスなので
Elipmocにも実装してみよう!

ではElipmocをMonoidのインスタンスにしてみよう。

instance Monoid a => Monoid (Elipmoc a) where
    mempty = Bat
    Bat `mappend` m = m
    m `mappend` Bat = m
    Good m1 `mappend` Good m2 = Good (m1 `mappend` m2)

mconcatはデフォルトで実装されているので、
実装する必要はない。

では実際に使用してみよう。

main::IO()
main = do
    print (Good [5] `mappend` Good [4])
    print (Bat `mappend` Good [3])
    print (Good [2] `mappend` Bat)

実行結果

Good [5,4]
Good [3]
Good [2]

#Functorを作ろう

Functorの定義を示す。

class  Functor f  where
    fmap        :: (a -> b) -> f a -> f b
    (<$)        :: a -> f b -> f a
    (<$)        =  fmap . const

Functorには、mapという関数が定義されている。
例えば、
f x = 2*x という関数があったとして、
f (Just 5) のように値を渡すことはできない。5はJustで包まれているからだ。
こういうときにfmapを使うと
fmap f (Just 5) というふうに書くことができる。
fmapはfで包まれたaを取り出して、関数(a->b)に渡して加工して、加工したbを再びf型に包むという操作を行う。
さらにもう一つ、Functorには<$という演算子が定義されている。
<$の左辺には任意の型aを、右辺にはf型で包まれているbを渡す。
そうすると、左辺のaを右辺のf型で包んだaを返す。右辺の値は捨てられる。
例を挙げる。
176 <$ Just 810
右辺はMaybeで包まれているので、Just 176が返り、Just 810は捨てられる。

では、サクッと実装しますね。
<$はデフォルトで実装されるので自分で定義する必要はありません。

instance Functor Elipmoc where
    fmap _ Bat = Bat
    fmap f (Good a) = Good (f a)

プログラム例

main::IO()
main = do
    print ( fmap (\x->[4+x]) (Good 5))
    print ( fmap (\x->[4+x]) (Bat))

このようにfmapに加工する関数を渡すと、Elipmocの中身を取り出し加工して再び包んで返す。

実行結果

Good [9]
Bat

#Applicativeを作ろう

Applicativeの実装を示す。

class Functor f => Applicative f where
    pure :: a -> f a

    (<*>) :: f (a -> b) -> f a -> f b

    liftA2 :: (a -> b -> c) -> f a -> f b -> f c
    liftA2 f x = (<*>) (fmap f x)

    (*>) :: f a -> f b -> f b
    a1 *> a2 = (id <$ a1) <*> a2

    (<*) :: f a -> f b -> f a
    (<*) = liftA2 const

pure関数は任意のa型の値を受け取って、それをfの型で包んで返します。
<\*>演算子はfで包まれた左辺f(a->b)と任意の型aの右辺を受け取り、左辺の中身の関数(a->b)に、右辺の中身bを引数として渡し、結果をfで包んで返します。
<*演算子は左辺と右辺を受け取り、右辺を捨てて左辺を返す。
*>演算子は左辺を捨てて右辺を返す。
あと<$>演算子も紹介しよう。
<$>の定義を示す。

(<$>) :: Functor f => (a -> b) -> f a -> f b
(<$>) = fmap

<$>演算子は<\*>ととても似ていて、違いは左辺がfで包まれていないだけである。
というか、ただのfmapである。
これらを使うことで見やすいコードを書くことができる。(投げやり)
じゃあ型クラス作りましょう。

instance Applicative Elipmoc where
    pure a = Good a
    (<*>) (Good f) x = fmap f x
    (<*>) Bat _ = Bat 

サンプルコード

func x y= x+y

main::IO()
main = do
    print ( func <$> Bat <*> Good 2)
    print ( func <$> Good 5 <*> Good 3 <* Good 2)
    print ( func <$> Good 5 <*> Good 3 <* Bat)

実行結果

Bat
Good 8
Bat

#Monadを作ろう

どんどん作っていきます

Monadの定義を示す

class Applicative m => Monad m where
    (>>=)       :: m a -> (a -> m b) -> m b

    (>>)        :: m a -> m b -> m b
    m >> k = m >>= \_ -> k

    return      :: a -> m a
    return      = pure

Monadを使うことで、複数の処理を連結させて、1つの処理にまとめることができる。

ElipmocをMonadのインスタンスにする。

instance Monad Elipmoc where
    (Good x) >>= k = k x
    Bat  >>= _ = Bat

    (Good _) >> k = k
    Bat  >>  _ = Bat

    return = Good
    fail _ = Bat

サンプルコード

func 0= Bat
func x= return (x*10)

func2 = return (931::Int)

main::IO()
main = do
    print ( Good 2 >>= func)
    print ( Good 0 >>= func)
    print ( Good 1 >>= func >>= func)
    print ( Good 32 >> func2)

計算結果

Good 20
Bat
Good 100
Good 931

#この記事を書いた感想
うん。正直どこまで説明を細かく書くべきかわからず苦しんだよ。
モナドについてはもはや説明という説明すらない。
途中で、あれ?これ定義を書いとけば日本語でいちいち説明する必要なくね?と思ったからだ。

13
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?