きくらげ観察日記

好きなことを、適当に。

Scala的な型クラスをHaskellで作る

Scalaの型クラスはHaskellの型クラスとは全くの別物ですが、HaskellでもImplicitParams拡張を使うことによってScalaの型クラスのようなものを利用できるようになります。

ImplicitParams

このマイナーな拡張を有効化すると、関数に対して暗黙的に引数を渡すことができるようになります。
暗黙引数を受け取る関数は以下のように定義します。

-- sortBy :: (a -> a -> Ordering) -> [a] -> [a]
sort' :: (?compare :: a -> a -> Ordering) => [a] -> [a]
sort' xs = sortBy ?compare xs

この定義のうち、

?compare :: a -> a -> Ordering

の部分で暗黙引数を要求しています。
暗黙引数は?で始まる名前を持ち、関数を呼び出した位置のスコープに同名で型の一致する変数が存在した場合、明示的に関数に渡さなくてもその値を呼び出した関数側から参照することができます。

実行例:

>>> :m +Data.List Data.Function
>>> let xs = [("hoge", 5), ("fuag", 10), ("piyo", 1), ("foo", 7)] :: [(String, Int)]
>>> let ?compare = flip (compare `on` snd) in sort' xs
[("fuag",10),("foo",7),("hoge",5),("piyo",1)]

2番目の要素について降順に並べられていることがわかります。

Scala的な型クラス

ScalaにはHaskellで言うところの型クラスは存在しません。それでどうやって型クラスを実現しているのかというと、型クラスのメソッドに対応する関数をまとめたデータ型を作り、それを暗黙引数として受け渡すことによって型クラスに対する処理を行っています。

例えば、Scala的な方法でApplicativeを作りたい場合、Applicative型クラスを作るのではなく、次のようなデータ型を定義します。

data Applicative f = Applicative {
    point :: forall a. a -> f a                     -- pureの代わり
  , apply :: forall a b. f (a -> b) -> f a -> f b   -- apの代わり
  }

ついでに、Applicative fを暗黙的に受け取るような関数も用意してみましょう。

pure :: (?app :: Applicative f) => a -> f a
pure = point ?app

(<*>) :: (?app :: Applicative f) => f (a -> b) -> f a -> f b
(<*>) = apply ?app

infixl 4 <*>

ghciでロードすると「Control.Applicativeのやつと名前被ってる」と怒られますが、とりあえず無視。

さらにこのApplicativeに対する"インスタンス宣言"を行います。

appIO :: Applicative IO
appIO = Applicative {
    point = return
  , apply = \mf mx -> mf >>= \f -> mx >>= \x -> return (f x)
  }

実際に使用してみましょう。

>>> let addTwo = return (+ 2) :: IO (Int -> Int)
>>> let ?app = appIO in addTwo <*> readLn
5
7

この方法でも、十分型クラスに近い動作が行えていることがわかります。


この方法のメリットは、1つの型について2種類以上のインスタンス定義を行うことができることです。
例えば[]をApplicativeのインスタンスにする方法は、以下の2通りが考えられます。

[f, g, h] <*> [a, b, c] = [f a, f b, f c, g a, g b, g c, h a, h b, h c]
[f, g, h] <*> [a, b, c] = [f a, g b, h c]

実際は前者の方法でApplicativeのインスタンスとなっています。
後者の操作を行うためにはZipListというnewtypeが用意されており、リストをZipListに包んでから<*>に適用することによって、期待通りの動作を行うことができます。

一方後者では、Applicative []を複数作っておいて、必要に応じて使い分けることで対応できます。

appList :: Applicative []
appList = Applicative {
    point = (:[])
  , apply = \fs xs -> [f x | f <- fs, x <- xs]
  }

appZipList :: Applicative []
appZipList = Applicative {
    point = (:[])
  , apply = zipWith ($)
}

applyAll :: (?app :: Applicative []) => [Int]
applyAll = [(+ 3), (* 20), (`div` 5)] <*> [4, 9, 12]

2つのApplicativeを使ってapplyAllを実行してみましょう。

>>> let ?app = appList in applyAll
[7,12,15,80,180,240,0,1,2]
>>> let ?app = appZipList in applyAll
[7,180,2]

関数プログラミング入門 ―Haskellで学ぶ原理と技法―

関数プログラミング入門 ―Haskellで学ぶ原理と技法―