Functor(関手)の話です。
HaskellのFunctorクラスはHask圏からHask圏への関手しか表現できません。
そこで、Hask圏以外の圏についても使える関手のクラスを書いてみました。
よろしくね (*´σー`)エヘヘ
#基本的な用語と、HaskellのFunctorクラス
別のところにまとめました
この章に書こうとしていた、「基本的な用語の確認とHaskellのFunctorクラス」についての内容は、別の記事にまとめさせて貰いました。
内容としては、「HaskellのFunctorクラスはHask圏からHask圏への関手しか表現できません。」という文章の説明なので、それを読んで「そらそうだろ」って感じなら読み飛ばして次の章を読んで頂いて大丈夫です。
#とびだせ!Hask圏
HaskellのFunctorは、Hask圏からHask圏への関手を表現しています。
しかし、HaskellではHask圏以外の圏について考えることもできますし、実際 Control.Category には様々な圏を統一的に表現できる型クラス Cat
が定義されています。
私の今回の目的は、もっと一般的な圏から圏への関手を表現できるFunctorクラスを作り、Hask圏から飛び出すことです。
もっと一般的なFunctorクラス
以下が今回書いた「もっと一般的なFunctor」のクラスです。
「もっと一般的」なので GeneralFunctor
という名前のクラスにしました。
(適当にカッコいい名前をつけちゃったので、「数学にはGeneralFunctorっていう名前の別の概念があるよ」みたいなことがあったらコメントで教えて下さい)
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FunctionalDependencies #-}
module Functor where
import Control.Category
class (Category c1, Category c2) =>
GeneralFunctor (name :: *)
(c1 :: k1 -> k1 -> *)
(c2 :: k2 -> k2 -> *)
|name -> c1, name -> c2 where
type F name (x :: k1) :: k2
gmap :: name -> c1 a b -> c2 (F name a) (F name b)
短いですね。
対象を*
以外のカインドの型にもできるように、PolyKinds
拡張が使われています。
このクラスは、c1
という圏からc2
という圏への関手を表現しているのですが、移る前の圏と移る先の圏を決定しても、そのあいだの関手は唯一つに定まるとは限りません。
そのため、それぞれの関手に名前をつけてname
という型を使って、どのインスタンス宣言が呼び出されるのか曖昧さがなく決定できるようにします。
F
という型族で、「C の任意の対象 X を、 D の対象 F(X) に対応させる」という性質を表現しています。
また、gmap
という関数が「C の任意の射 f : X -> Y を、Dの射 F(f) : F(X) -> F(Y) に対応させる」という性質を表現しています。
使ってみる
Sample.hs は記事の最後にコードを全部貼り付けるので、使っている拡張機能やimportしているモジュールはそちらで確認して下さい。
従来のFunctor
では、さっそくGeneralFunctor
クラスを使ってみます。
まずは、従来のHaskellのFunctorをGeneralFunctor
クラスで表現してみます。
data EndoFunctor f = EF
instance Functor f => GeneralFunctor (EndoFunctor f) (->) (->) where
type F (EndoFunctor f) x = f x
gmap _ = fmap
ghci> gmap (EF :: EndoFunctor Maybe) (+40) (Just 2)
Just 42
良さそうですね。
関手の名前としてEndoFunctor f
という型をつくり、それを第一引数としてgmap
を呼び出すことでfmap
と同じ働き(射の対応付け)ができていることが確認できます。
Arrow
つぎはArrowの話です。
HackegeのArrowにも書かれている通り、ArrowクラスはCategoryクラスを継承しています。
また、そのインスタンスはarr
という関数を実装する必要があり、arr (f >>> g) = arr f >> arr g
を満たす必要があります。
これを言い換えると、
- Arrowクラスのインスタンスは圏である
- Hask圏の射を
arr
でArrowのインスタンスである圏の射に対応付ける - Hask圏の対象の型はArrowのインスタンスである圏の同じ型に対応づける
- この対応付けは関手則を満たす
というこうとが要求されているとわかります。
この、Hask圏からの関手をGeneralFunctor
クラスで表現してみましょう。
data Arr (a :: * -> * -> *) = Arr
instance Arrow a => GeneralFunctor (Arr a) (->) a where
type F (Arr a) x = x
gmap _ = arr
ghci> let kl = gmap (Arr :: Arr (Kleisli Maybe)) (* 7)
ghci> :t kl
Num a => Kleisli Maybe a a
ghci> runKleisli kl 6
Just 42
良さそうですね。
ghciでは例として、Hask圏からKleisli圏への関手をGeneralFunctor
クラスで表現して、射の対応付けをgmap
を用いて行っています。
対象が一つの圏
つぎは、対象のカインドが*
ではないような圏についても考えてみましょう。
射と射の合成を適当に定義すれば、任意のモノイドを「対象が一つの圏」として表現できます。
圏の公理からわかるように、対象が一つしかない圏は、射の合成が結合則を満たし恒等射が単位元とみなせるからです。
では、Haskellでそのような「対象が一つの圏」を定義してみましょう。
{-# LANGUAGE DataKinds #-}
data Unit = U
data As (m :: *) (a :: Unit) (b :: Unit) = As {toMonoid :: m} deriving (Show, Eq)
instance Monoid m => Category (As m) where
(.) (As m1) (As m2) = As $ m1 <> m2
id = As mempty
as :: Monoid m => m -> As m U U
as = As
こんな感じですね。
対象がただ一つであることを表現したかったので、Unit
というカインドを定義して、そのカインドを持つ型はU
だけになるようにしています。
as
関数は幽霊型となっているa
とb
をいちいち指定するのが面倒くさいので作りました。
ところで、列(HaskellにおいてはList)は自由モノイドなので、対象が一つで射がモノイドの列であるような圏は、対象が一つであり射がそのモノイドである圏への関手が存在するはずです。
そのような関手をGeneralFunctor
クラスで表現します。
data ListToAs m = LtoA
instance Monoid m => GeneralFunctor (ListToAs m) (As [m]) (As m) where
type F (ListToAs m) x = U
gmap _ (As xs) = As $ mconcat xs
ghci> gmap (LtoA :: ListToAs (Sum Int)) (as [Sum 2, Sum 3, Sum 10])
As {toMonoid = Sum {getSum = 15}}
良い感じですね。
まとめ
従来のFunctor、関数のArrowへの持ち上げ、モノイドの列の畳み込みといった、さまざまなことがGeneralFunctor
クラスで表現できることがわかりました。
関手という概念の汎用性の高さと、それを実装できるHaskellという言語の表現力の高さには驚かされますね。
ぜひ、いろんな関手をHaskellで表現して遊んでみてください!
おまけ
実例のところにちょこちょこコードの断片を貼っていた、Sample.hsの全コードをここに貼っておきます。
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE DataKinds #-}
import Prelude hiding (id,(.))
import Data.Monoid
import Control.Arrow
import Control.Category
import Functor
data EndoFunctor f = EF
instance Functor f => GeneralFunctor (EndoFunctor f) (->) (->) where
type F (EndoFunctor f) x = f x
gmap _ = fmap
data Arr (a :: * -> * -> *) = Arr
instance Arrow a => GeneralFunctor (Arr a) (->) a where
type F (Arr a) x = x
gmap _ = arr
data Unit = U
data UnitCat (u1 :: Unit) (u2 :: Unit) = UnitCat deriving (Show, Eq)
instance Category UnitCat where
(.) _ _ = UnitCat
id = UnitCat
data As (m :: *) (a :: Unit) (b :: Unit) = As {toMonoid :: m} deriving (Show, Eq)
as :: Monoid m => m -> As m U U
as = As
instance Monoid m => Category (As m) where
(.) (As m1) (As m2) = As $ m1 <> m2
id = As mempty
data ListToAs m = LtoA
instance Monoid m => GeneralFunctor (ListToAs m) (As [m]) (As m) where
type F (ListToAs m) x = U
gmap _ (As xs) = As $ mconcat xs
追記
この記事を読んで下さった makoraru さんから Data.Category.Functor というモジュールのことを教えて頂きました。
ここに定義されているFunctor
は、今回私が定義したのと同じように関手の名前ftag
をFunctor
クラスのインスタンスとすることで、メソッド(%)
により射の対応付け、型族(:%)
で対象の対応付けをしています。
MultiParamClasses
を使って関手の名前と関手で移る前後の圏をまとめてインスタンス化している私のGeneralFunctor
より見た目が綺麗ですね。
ただし、型族(:%)
は Poly Kind に定義されていないので、対象が*
以外の圏への関手はこのモジュールを使って書くことはできなさそうです。
このモジュールに定義されている様々なFunctor
を自分のGeneralFunctor
を使って実装できないか試してみようと思います。