LoginSignup
6

More than 5 years have passed since last update.

posted at

updated at

[Haskell] とびだせ!Hask圏

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っていう名前の別の概念があるよ」みたいなことがあったらコメントで教えて下さい)

Functor.hs
{-# 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クラスで表現してみます。

Sample.hs
data EndoFunctor f = EF

instance Functor f => GeneralFunctor (EndoFunctor f) (->) (->) where
    type F (EndoFunctor f) x = f x
    gmap _ = fmap
ghci
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クラスで表現してみましょう。

Sample.hs
data Arr (a :: * -> * -> *) = Arr
instance Arrow a => GeneralFunctor (Arr a) (->) a where
    type F (Arr a) x = x
    gmap _ = arr
ghci
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でそのような「対象が一つの圏」を定義してみましょう。

Sample.hs
{-# 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関数は幽霊型となっているabをいちいち指定するのが面倒くさいので作りました。

ところで、列(HaskellにおいてはList)は自由モノイドなので、対象が一つで射がモノイドの列であるような圏は、対象が一つであり射がそのモノイドである圏への関手が存在するはずです。

そのような関手をGeneralFunctorクラスで表現します。

Sample.hs
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
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の全コードをここに貼っておきます。

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は、今回私が定義したのと同じように関手の名前ftagFunctorクラスのインスタンスとすることで、メソッド(%)により射の対応付け、型族(:%)で対象の対応付けをしています。

MultiParamClassesを使って関手の名前と関手で移る前後の圏をまとめてインスタンス化している私のGeneralFunctorより見た目が綺麗ですね。

ただし、型族(:%)は Poly Kind に定義されていないので、対象が*以外の圏への関手はこのモジュールを使って書くことはできなさそうです。

このモジュールに定義されている様々なFunctorを自分のGeneralFunctorを使って実装できないか試してみようと思います。

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
What you can do with signing up
6