3
4

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 3 years have passed since last update.

Functorに触れてみよう

Last updated at Posted at 2020-05-08

Haskellを書く上で、避けては通れない概念がいくつかあります。Functor(ファンクタ)もその1つですが、特徴的なものなので、理解しにくい人もいると思います。今回はできるだけコード例と図を示しながら解説していきます。後半ではFunctorを扱うライブラリのData.Functorについて見ていきます。

注意

・数学の圏論には詳しくないです。そのため、関手(数学のFunctor)についての話はできません。

・この記事で使用しているGHCのバージョンは8.8.3です。

最初のFunctor

はっきり言うと、Functorは型クラスです。GHC.Baseで定義されていて、Preludeにも含まれています。定義はこうです。

class Functor (f :: * -> *) where
  fmap :: (a -> b) -> f a -> f b
  (<$) :: a -> f b -> f a
  {-# MINIMAL fmap #-}

MINIMAL fmapとあるので、何かしらの型をFunctorのインスタンスにしたければfmapのみを定義すればよい、とのことです。

* -> *がFunctorのキモです。型注釈に似ていますが、これはカインドについての注釈です。IntCharBoolのカインドは*つまり値そのものなのでFunctorのインスタンスにはなれません。GHCiで:infoを使って型宣言を、:kindでカインドを見られます。見てみましょう。

Prelude> :info Int
data Int = GHC.Types.I# GHC.Prim.Int#   -- Defined in ‘GHC.Types’
...
Prelude> :info Char
data Char = GHC.Types.C# GHC.Prim.Char#   -- Defined in ‘GHC.Types’
...
Prelude> :info Bool
data Bool = False | True  -- Defined in ‘GHC.Types’
...
Prelude> :kind Int
Int :: *
Prelude> :kind Char
Char :: *
Prelude> :kind Bool
Bool :: *

型宣言のあとにその型がどのインスタンスになっているかが表示されます。ここでは型宣言だけを見るためにそれらは省略しました。3つの型に共通するのは、どれも型コンストラクタが引数をとらないということ、すなわち値そのものということです。

ここで、Functorのインスタンスで私たちにも馴染み深いMaybeの型宣言を見てみましょう。

data Maybe a = Nothing | Just a   -- Defined in ‘GHC.Maybe’

Maybe aというのは、aという型を引数にとるよ、という意味です。型コンストラクタが引数をとって、NothingJust aの型になるよ、と言っています。なんだか関数みたいですね。カインドも見てみましょう。

Maybe :: * -> *

型引数を1つとって何かしらの値を「返す」型だということです。このカインドはFunctorのインスタンス* -> *に一致するので、Functorになれます。

Functorになるためのfmapは、面白い型宣言をしています。

fmap :: (a -> b) -> f a -> f b

fmapは関数と、Functorに包まれた値を1つとって、関数を中の値に適用して再び包んだものを返します。図にするとこうです。

fmap.png

fmapは、その内部で下のように関数を適用しています。

inside-of-fmap.png

MaybeはGHC.BaseでFunctorのインスタンスとして定義されています。

instance  Functor Maybe  where
    fmap _ Nothing       = Nothing
    fmap f (Just a)      = Just (f a)

fmapでMaybeの値に関数を適用すると、Nothingなら取り出せる値がないのでNothingを、Justなら中の値に関数を適用してJustに包んで返す、ということがわかります。このとき、fmapの型注釈は次のようになると予想できます。

fmap :: (a -> b) -> Maybe a -> Maybe b

さっそく使ってみましょう。

Prelude> fmap (*3) (Just 8)
Just 24
Prelude> fmap (*3) Nothing
Nothing

fmapを使って、包まれた値に関数を適用できるというわけです。

ファンクタ則

Functorのインスタンスになるためには、その動作がファンクタ則と呼ばれるルールを満たしていなければなりません。ルールは以下の2つです。

fmap id = id
fmap (f . g) == fmap f . fmap g

つまり、仮にFunctorのインスタンスSomeTypeがあるとき、次の2つの式はTrueにならなければならない、ということです。

fmap id (SomeType x) == id (SomeType x)
fmap (f . g) (SomeType x) == (fmap f . fmap g) (SomeType x)

その1

fmap id = id

この法則は、idfmapで中の値に適用したときに中の値が変わってはならない、というものです。

前提として、idは引数をそのまま返します。図にするとこうです。

id.png
id :: a -> a

fmap idの挙動は、こうなると予想できます。

fmap-id.png
fmap id :: Functor f => f a -> f a

Maybeでこれを確認してみましょう。

ghci> fmap id (Just 2)
Just 2
ghci> id (Just 2)
Just 2
ghci> fmap id Nothing
Nothing
ghci> id Nothing
Nothing

ちゃんと満たしていますね。

その2

fmap (f . g) == fmap f . fmap g

これは、関数合成を行ったf . gfmapするのは、fmap gに続けてfmap fをしたものと等しい、という意味です。

前提として、fgは次のような関数とします。

compose.png
f :: a -> b
g :: b -> c
f . g :: a -> c

fmap f . gは次のように動作するはずです。

fmap-f-g.png
fmap f . g :: Functor f => f a -> f c

そして、fmap f . fmap gはこう動作するはずです。

fmap-f-fmap-g.png
fmap f . fmap g :: Functor f => f a -> f c

Maybeを使って確認してみます。

ghci> fmap ((+2) . (*3)) (Just 30)
Just 92
ghci> (fmap (+2) . fmap (*3)) (Just 30)
Just 92
ghci> fmap ((+2) . (*3)) Nothing
Nothing
ghci> (fmap (+2) . fmap (*3)) Nothing
Nothing

等しい値になっていることがわかります。

以上の2つから、Maybeはファンクタ則を満たしていることがわかりました。この後に紹介するFunctorたちもこの2つの法則を満たしています。

その他のFunctorたち

Maybe以外にも、Functorとして定義されているものがいくつかあります。

リスト

リストといってもFunctorになれるのは空リストです。リストは非決定性計算としても扱われるので、ある意味で値を包んだものと言えます。

instance Functor [] where
    fmap = map

fmapmapであると定義されています。

mapはリストの各要素に関数を適用するので、fmapに求められる動作としては間違っていません。fmapmapで書き直すとこうなります。

instance Functor [] where
    map _ [] = []
    map f (x:xs) = f x : map xs

リストが空ならそのまま返し、そうでなければ関数を適用して返す、という動作です。もちろん、これはファンクタ則を満たしています。

  fmap id [x1, x2, ..]
= [x1, x2, ..]
= id [x1, x2, ..]

  fmap (f . g) [x1, x2, ..]
= (f . g) [x1, x2, ..]
= fmap f (g [x1, x2, ..])
= (fmap f . fmap g) [x1, x2, ..]
ghci> fmap id []
[]
ghci> fmap (*2) []
[]
ghci> (fmap (*2) . fmap (+10)) [1..10]
[22,24,26,28,30,32,34,36,38,40]
ghci> fmap ((*2) . (+10)) [1..10]
[22,24,26,28,30,32,34,36,38,40]

タプル

GHCでは、2~4要素のタプルまでがFunctorのインスタンスとして定義されています。

instance Functor ((,) a) where
    fmap f (x,y) = (x, f y)

instance Functor ((,,) a b) where
    fmap f (a, b, c) = (a, b, f c)

instance Functor ((,,,) a b c) where
    fmap f (a, b, c, d) = (a, b, c, f d)

どのファンクタも、最後の要素にのみ関数が適用されます。この実装は、ファンクタ則を満たしています。

  fmap id (x, y)
= (x, id y)
= (x, y)
= id (x, y)

  fmap (f . g) (x, y)
= (x, (f . g) y)
= fmap f (x, g y)
= (fmap f . fmap g) (x, y)

もちろん、ペア以外のタプルでも同様です。

Either

Eitherは2つの型引数をとり、LeftかRightで包んで返す型です。1

data Either a b = Left a | Right b

失敗する可能性のある計算を行う時に使い、Leftは失敗したときの値、Rightは成功したときの値です。ただ、Eitherのままだと型コンストラクタは* -> * -> *なので、Functorのインスタンスになれません。なので、aのみを部分適用して* -> *の形にします。

instance Functor (Either a) where
    fmap _ (Left x) = Left x
    fmap f (Right y) = Right (f y)

ファンクタ則を満たしているという確認は一瞬です。型引数はbなので、bに関数が適用されるのがポイントです。

  fmap id (Either a (b))
= Either a (id b)
= Either a b
= id (Either a b)

  fmap (f . g) (Either a (b))
= Either a ((f . g) b)
= fmap f (Either a (g b))
= (fmap f . fmap g) (Either a b)

IO

IOは、I/Oアクションつまり動作とその結果を扱う型です。

instance Functor IO where
   fmap f x = x >>= (pure . f)

この時、fmapは包まれた値に関数を適用するため、その型はこうなるはずです。

fmap :: (a -> b) -> IO a -> IO b

注意してほしいのは、戻り値はIOに包まれているということです。

>>=pureの型は次のようになります。

(>>=) :: Monad m => m a -> (a -> m b) -> m b
pure :: Applicative f => a -> f a

>>=は動作の結果を取り出して関数に渡し、pureは文脈に合う値を生成します。これを踏まえると、fmapxから値を取り出してfを適用し、pureで文脈に合う値(=IO)を返すことがわかります。

I/Oアクションの例としては、getLineputStrLnがあります。

getLine :: IO String
putStrLn :: String -> IO ()

getLineはユーザからの入力を受け取り、IOで包んだ文字列として返します。putStrLnは文字列を受け取り、文字列を出力する動作を返します。

IOをFunctorにするメリットは、IOの殻をあまり意識せずにI/Oアクションから値を操作できることです。つまり、プログラムの純粋な部分とそうでない部分を分離できます。

サンプルとして、プログラムの引数を改行で区切って表示するプログラムを書いてみます。コマンドライン引数をとる関数としてgetArgsをインポートします。unlinesを使って改行で引数をつなぎます。

getArgs :: IO [String]
unlines :: [String] -> String

下がプログラムの本体です。

import System.Environment (getArgs)
main = putStrLn =<< unlines <$> getArgs

getArgsで受け取ったコマンドライン引数のリストから値を取り出し、unlinesを適用します。<$>fmapの中置演算子バージョンです。それを先程の=<<putStrLnに突っ込んで出力しています。

関数

関数、つまり((->) r)は、Functorの一種です。Functorは、包まれた値の世界f aでの写像2とも言えます。インスタンス実装の前に、fmapの型を考えてみます。Functor fの部分に((->) r)がくるので、下のようになると思われます。

fmap :: (a -> b) -> (r -> a) -> (r -> b)

fmap(a -> b)(r -> a)をくっつけているように見えませんか?

instance Functor ((->) r) where
    fmap = (.)

((->) r)は出力の欠けた関数なので、カインドは* -> *になります。もちろん、この実装はファンクタ則を満たします。

  fmap id f
= f . id
= f
= id f

  fmap (g . h) f
= (g . h) . f
= fmap g (h . f)
= (fmap g . fmap h) f

関数のファンクタにfmapすることは、関数合成をすることと等しいことがわかります。

Functorのインスタンスを作ってみる

いくつかFunctorを見てきたので、そろそろFunctorのインスタンスを自作してみましょう。今回インスタンスにするのは次のような再帰的な型です。

infixr 5 :::
data List a = None | List a ::: a
  deriving (Show)

目標は、fmap(a -> b) -> List a -> List bになるように実装することです。早速やってみましょう。

instance Functor List where
  fmap _ None = None
  fmap f (as ::: a) = fmap f as ::: f a

ghci> fmap (*2) (None:::1:::2:::3:::4:::5)
 None ::: (2 ::: (4 ::: (6 ::: (8 ::: 10))))
ghci> fmap sum (None ::: [1..5]:::[1..10]:::[1..20])
None ::: (15 ::: (55 ::: 210 ))

ちゃんと動いています!!

Data.Functor

続いては、Functorに関連する関数をまとめたData.Functorモジュールを見ていきます。次のような関数群があります。

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

fmap, (<$>), (<&>)

fmapはもうおなじみですね。<$>fmapの中置演算子バージョンで、<&>はその引数を入れ替えたものです。fmapはインスタンスに依存しますが、<$><&>はData.Functorで実装されています。

infixl 4 <$>

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

infixl 1 <&>

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

どちらの実装も、すんなり理解できると思います。<$>fmapそのもの、<&>は関数と包まれた値を入れ替えたものですね。どちらも左結合で、<&>の優先順位は下から2番目で、$の1つ上なので、<$>より比較的後に評価されます。関数合成.infixr 9ですから、関数を書き連ねたいときは<&>を、そうでなければ<$>を使うとよいかと思います。

(<$), ($>)

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

<$は、右手のファンクタの値からファンクタのみを取り出して左手の値に被せる演算子です。$>はその逆です。

infixr 4 <$
(<$)  :: a -> f b -> f a
(<$) =  fmap . const
infixr 4 $>
($>) :: Functor f => f a -> b -> f b
($>) = flip (<$)
ghci> "abc" <$ Just 100
Just "abc"
ghci> "abc" <$ Nothing
Nothing
ghci> "abc" <$ Left 100
Left 100
ghci> "abc" <$ Right 100
Right "abc"
ghci> "abc" <$ [1..10]
["abc","abc","abc","abc","abc","abc","abc","abc","abc","abc"]
ghci> "abc" <$ (100,200)
(100,"abc")
ghci> Just "abc" <$ Just 100
Just (Just "abc")

void

void :: Functor f => f a -> f ()

voidはファンクタの値をとり、ファンクタを()(ユニット)に被せて返します。

ghci> void Just 100
Just ()
ghci> void Nothing
Nothing
ghci> void [1..10]
[(),(),(),(),(),(),(),(),(),()]

また、IOアクションの戻り値を捨てられます。

ghci> mapM print "abc"
'a'
'b'
'c'
[(),(),()]
ghci> void $ mapM print "abc"
'a'
'b'
'c'

まとめ

  • Functorは何かしらの包まれた値を扱う型クラスです
  • Functorのインスタンスはファンクタ則を満たします
  • Haskellの「失敗を扱う型」や「状態を扱う型」(要するに値を包む型)はFunctorのインスタンスになっています

感想

個人的には、Functorはやっぱり包まれた値と相性がいいと思います。

参考文献

  1. Eitherについてはこちらの記事をどうぞ。

  2. 関数の別名で、入力が出力を定めているともいえるので、こう呼びます。

3
4
1

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
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?