web-dev-qa-db-ja.com

ファンクターはhaskellでどのように機能しますか?

私はHaskellを学ぼうとしていますが、すべての基本を学びました。しかし今、私は行き詰まり、ファンクターの周りに頭を抱えようとしています。

「ファンクターはあるカテゴリーを別のカテゴリーに変換する」と読みました。これは何を意味するのでしょうか?

質問がたくさんあることはわかっていますが、だれでもプレーンな英語ファンクタの説明または簡単な使用例を教えてもらえますか?

46

あいまいな説明としては、Functorはコンテナの一種であり、関連する関数fmapを使用すると、含まれているものを変換する関数を前提として、含まれているものを何でも変更できます。

たとえば、リストはこの種のコンテナであり、fmap (+1) [1,2,3,4]は_[2,3,4,5]_を生成します。

Maybeをファンクタにすることもでき、fmap toUpper (Just 'a')は_Just 'A'_を生成します。

fmapの一般的なタイプは、何が起こっているのかを非常にきちんと示しています。

_fmap :: Functor f => (a -> b) -> f a -> f b
_

そして、特別なバージョンはそれをより明確にするかもしれません。これがリストバージョンです。

_fmap :: (a -> b) -> [a] -> [b]
_

そして多分バージョン:

_fmap :: (a -> b) -> Maybe a -> Maybe b
_

_:i Functor_を使用してGHCIをクエリすることにより、標準のFunctorインスタンスに関する情報を取得できます。多くのモジュールは、Functors(および他の型クラス)のインスタンスをさらに定義します。

ただし、「コンテナ」という言葉をあまり真剣に受け止めないでください。 Functorsは明確に定義された概念ですが、多くの場合、このファジーなアナロジーでそれについて推論できます。

何が起こっているのかを理解する最善の策は、単に各インスタンスの定義を読むことです。これにより、何が起こっているのかを直感的に理解できるはずです。そこからは、概念の理解を実際に正式化するための小さなステップにすぎません。追加する必要があるのは、「コンテナ」が実際に何であるかを明確にすることであり、各インスタンスは、単純な2つの法則を十分に満たします。

57
Sarah

誤って書きました

Haskell Functorsチュートリアル

例を使用して質問に答え、コメントの下にタイプを配置します。

タイプのパターンに注意してください。

fmapmapの一般化です

ファンクタは、fmap関数を提供するためのものです。 fmapmapと同様に機能するため、最初にmapを確認してみましょう。

map (subtract 1) [2,4,8,16] = [1,3,7,15]
--    Int->Int     [Int]         [Int]

したがって、リストは関数(subtract 1)insideを使用します。実際、リストの場合、fmapmapが行うこととはまったく異なります。今回はすべてに10を掛けてみましょう:

fmap (* 10)  [2,4,8,16] = [20,40,80,160]
--  Int->Int    [Int]         [Int]

これを、リストに10を掛ける関数をマッピングすることとして説明します。

fmapMaybeでも機能します

他に何をfmap超えることができますか? NothingJust xの2種類の値を持つMaybeデータ型を使用してみましょう。 (Nothingを使用して回答を取得できないことを表すことができますが、Just xは回答を表します。)

fmap  (+7)    (Just 10)  = Just 17
fmap  (+7)     Nothing   = Nothing
--  Int->Int  Maybe Int    Maybe Int

OK、それでまた、fmapは多分(+7)insideを使用しています。また、他の関数もfmapできます。 lengthはリストの長さを検出するので、Maybe [Double]にfmapできます

fmap    length             Nothing                      = Nothing
fmap    length    (Just [5.0, 4.0, 3.0, 2.0, 1.573458]) = Just 5
--  [Double]->Int         Maybe [Double]                  Maybe Int

実際にはlength :: [a] -> Intですが、ここでは[Double]で使用しているので、専門にしています。

showを使用して、文字列に変換してみましょう。ひそかにshowの実際のタイプはShow a => a -> Stringですが、少し長いので、ここではIntで使用しているため、Int -> Stringに特化しています。

fmap  show     (Just 12)  = Just "12"
fmap  show      Nothing   = Nothing
-- Int->String  Maybe Int   Maybe String

また、リストを振り返って

fmap   show     [3,4,5] = ["3", "4", "5"]
-- Int->String   [Int]       [String]

fmapEither somethingで動作します

少し異なる構造Eitherで使用してみましょう。タイプEither a bの値は、Left a値またはRight b値です。 Eitherを使用して、成功Right goodvalueまたは失敗Left errordetailsを表すこともあれば、2つの型の値を1つに混合することもあります。いずれにせよ、Eitherデータ型のファンクターはRightでのみ機能します-Left値のみを残します。これは、Right値を成功した値として使用している場合に特に意味があります(実際、両方で機能させるためにableはできません。タイプは必ずしも同じではありません)。タイプEither String Intを例として使用してみましょう

fmap (5*)      (Left "hi")     =    Left "hi"
fmap (5*)      (Right 4)       =    Right 20
-- Int->Int  Either String Int   Either String Int

Either内で(5*)を機能させますが、Eithersの場合、Rightの値のみが変更されます。ただし、関数が文字列で機能する限り、Either Int Stringで逆方向にそれを行うことができます。 ", cool!"を使用して、(++ ", cool!")を最後に付けましょう。

fmap (++ ", cool!")          (Left 4)           = Left 4
fmap (++ ", cool!") (Right "fmap edits values") = Right "fmap edits values, cool!"
--   String->String    Either Int String          Either Int String

IOでfmapを使用すると特に便利です

Fmapを使用する私のお気に入りの方法の1つは、IOの値に使用して値を編集することです。いくつかのIO操作により、次のようになります。そしてすぐにそれを印刷します:

echo1 :: IO ()
echo1 = do
    putStrLn "Say something!"
    whattheysaid <- getLine  -- getLine :: IO String
    putStrLn whattheysaid    -- putStrLn :: String -> IO ()

私はそれを私にきれいに感じるように書くことができます:

echo2 :: IO ()
echo2 = putStrLn "Say something" 
        >> getLine >>= putStrLn

>>は次々に処理を実行しますが、これが好きな理由は、>>=getLineから提供された文字列を受け取り、それをputStrLnにフィードするためです。 。ユーザーに挨拶したい場合:

greet1 :: IO ()
greet1 = do
    putStrLn "What's your name?"
    name <- getLine
    putStrLn ("Hello, " ++ name)

私たちがそれをよりきれいな方法で書きたかったのであれば、私は少し行き詰まっています。私は書く必要があります

greet2 :: IO ()
greet2 = putStrLn "What's your name?" 
         >> getLine >>= (\name -> putStrLn ("Hello, " ++ name))

notdoバージョンよりも優れています。実際、do表記があるので、これを行う必要はありません。しかし、fmapが助けになってくれるでしょうか?はい、できます。 ("Hello, "++)は、getLineにfmapできる関数です。

fmap ("Hello, " ++)  getLine   = -- read a line, return "Hello, " in front of it
--   String->String  IO String    IO String

次のように使用できます。

greet3 :: IO ()
greet3 = putStrLn "What's your name?" 
         >> fmap ("Hello, "++) getLine >>= putStrLn

与えられたものなら何でもこのトリックを利用できます。 「True」または「False」のどちらが入力されたかに同意しません。

fmap   not      readLn   = -- read a line that has a Bool on it, change it
--  Bool->Bool  IO Bool       IO Bool

または、ファイルのサイズを報告してみましょう。

fmap  length    (readFile "test.txt") = -- read the file, return its length
--  String->Int      IO String              IO Int
--   [a]->Int        IO [Char]              IO Int     (more precisely)

結論:fmapは何をし、何をするのですか?

タイプのパターンを見て例について考えていると、fmapが一部の値で機能する関数を受け取り、何らかの方法でそれらの値を持つまたは生成するものにその関数を適用して、値を編集していることに気付くでしょう。 (たとえば、readLnはBoolを読み取るため、タイプIO Boolがあり、Boolを生成するという意味でブール値があります。eg2[4,5,6]Ints inそれ。)

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

これはSomethingがList-of([]と書かれている)、MaybeEither StringEither IntIOであり、物事の上。これが賢明な方法で機能する場合は、ファンクターと呼びます(いくつかのルールがあります-後で)。 fmapの実際のタイプは

fmap :: Functor something => (a -> b) -> something a -> something b

ただし、簡潔にするために、通常はsomethingfに置き換えます。ただし、コンパイラーにとってはすべて同じです。

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

タイプを振り返り、これが常に機能することを確認してください-Either String Intについて注意深く-そのときのfは何ですか?

付録:Functorルールとは何ですか?なぜそれらがあるのですか?

idは恒等関数です。

id :: a -> a
id x = x

ルールは次のとおりです。

fmap id  ==  id                    -- identity identity
fmap (f . g)  ==  fmap f . fmap g  -- composition

まず、アイデンティティアイデンティティ:何もしない関数をマップしても、何も変更されません。これは明白に聞こえます(多くのルールではそうです)が、fmaponlyで値の変更が許可されていると解釈できます、構造ではありません。 fmapは、Just 4Nothingに、または[6][1,2,3,6]に、またはRight 4Left 4に変換することはできません。データが変更されただけでなく、そのデータの構造またはコンテキストが変更されたためです。

グラフィカルユーザーインターフェイスプロジェクトで作業していたときに、このルールに一度ぶつかりました。値を編集できるようにしたかったのですが、その下の構造を変更せずにそれを行うことはできませんでした。同じ効果があったので、誰も違いに気づかなかったでしょうが、ファンクターのルールに従わないことに気付いたので、デザイン全体を再考することができました。今では、よりすっきりとして、すっきりとして、速くなっています。

次に、コンポジション:これは、一度に1つの関数をfmapするか、同時に両方をfmapするかを選択できることを意味します。 fmapが値の構造/コンテキストをそのままにして、指定された関数を使用してそれらを編集するだけの場合、このルールでも機能します。

数学者には秘密の3つ目のルールがありますが、型宣言のように見えるため、Haskellではこれをルールと呼びません。

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

これにより、たとえば、リスト内の最初の値のみに関数を適用できなくなります。この法律は編集者によって施行されています。

なぜ私たちはそれらを持っているのですか? fmapが裏で何かをこっそりと行ったり、予期しないものを変更したりしないようにするため。これらはコンパイラーによって強制されません(コードをコンパイルする前に定理を証明するようコンパイラーに要求するのは公平ではなく、コンパイルが遅くなります-プログラマーがチェックする必要があります)。つまり、法律を少しごまかすことができますが、コードが予期しない結果をもたらす可能性があるため、これは悪い計画です。

Functorの法則は、fmapが関数を公平に、均等に、どこにでも適用し、他の変更を加えないようにすることです。それは、良い、クリーン、クリア、信頼できる、再利用可能なものです。

117
AndrewC

Haskellでは、ファンクターが「もの」のコンテナーを持つという概念を捉え、コンテナーの形状を変更せずにその「もの」を操作できるようにします。

ファンクタは1つの関数fmapを提供します。これにより、通常の関数を使用して、あるタイプの要素のコンテナから別のタイプのコンテナに関数を「持ち上げ」ます。

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

たとえば、リスト型コンストラクタである[]はファンクタです。

> fmap show [1, 2, 3]
["1","2","3"]

MaybeMap Integerなどの他の多くのHaskell型コンストラクタも同様です1

> fmap (+1) (Just 3)
Just 4
> fmap length (Data.Map.fromList [(1, "hi"), (2, "there")])
fromList [(1,2),(2,5)]

fmapはコンテナの「形状」を変更できないため、たとえばfmapリストを作成した場合、結果の要素数は同じで、fmap a JustNothingになることはできません。正式には、fmap id = id、つまり、アイデンティティ関数をfmapした場合、何も変更されません。

これまでは「コンテナ」という用語を使用してきましたが、実際にはそれよりも少し一般的です。たとえば、IOもファンクタであり、その場合の「形状」とは、fmapアクションのIOが副作用を変更しないことを意味します。実際、どのモナドもファンクタです2

カテゴリ理論では、ファンクタを使用すると、異なるカテゴリ間で変換できますが、Haskellでは、実際にはHaskと呼ばれる1つのカテゴリしかありません。したがって、HaskellのすべてのファンクタはHaskからHaskに変換されるので、それらは内部ファンクタ(カテゴリからそれ自体へのファンクタ)と呼ばれます。

最も単純な形では、ファンクタはやや退​​屈です。たった一回の操作でできることはたくさんあります。ただし、操作を追加し始めると、通常のファンクターからアプリケーションファンクター、モナドなどにすばやく移行できるようになりますが、それはこの回答の範囲を超えています。

1 ただし、SetOrdタイプのみを格納できるため、そうではありません。ファンクタは任意のタイプを含むことができる必要があります。
2 歴史的な理由により、FunctorMonadのスーパークラスではありませんが、多くの人がそうであると考えています。

8
hammar

タイプを見てみましょう。

Prelude> :i Functor
class Functor f where fmap :: (a -> b) -> f a -> f b

しかし、それはどういう意味ですか?

まず、ここでfは型変数であり、型コンストラクタを表します。f aは型です。 aは、ある型を表す型変数です。

次に、関数g :: a -> bを指定すると、fmap g :: f a -> f bを取得します。つまりfmap gは、f a型のものをf b型のものに変換する関数です。ここでは、タイプabも取得できないことに注意してください。関数g :: a -> bは、何らかの形でf a型のものを処理し、それらをf b型のものに変換するように作成されています。

fは同じであることに注意してください。他のタイプのみが変更されます。

どういう意味ですか?それは多くのことを意味することができます。 fは通常、「コンテナ」と見なされます。次に、fmap gを使用すると、gがこれらのコンテナーを開くことなく、コンテナーの内部で動作できるようになります。結果はまだ「内部」で囲まれています。typeclassFunctorは、結果を開いたり、内部を覗いたりする機能を提供しません。不透明なものの内部のいくつかの変形だけが私たちが得るすべてです。他の機能はどこか別の場所から取得する必要があります。

また、これらの「コンテナ」がタイプaの「もの」を1つだけ運ぶとは言っていないことに注意してください。多くの個別の「もの」が「その中に」存在する可能性がありますが、すべて同じタイプaです。

最後に、ファンクターの候補者は ファンクターの法則 に従う必要があります。

fmap id      ===  id
fmap (h . g) ===  fmap h . fmap g

2つの(.)演算子のタイプが異なることに注意してください。

   g    :: a -> b                    fmap g       :: f a -> f b
   h    ::      b -> c               fmap h       ::        f b -> f c
----------------------          --------------------------------------
(h . g) :: a      -> c          (fmap h . fmap g) :: f a        -> f c

つまり、ab、およびcタイプの間に関係が存在する場合は、ワイヤーを接続することにより、関係が存在しますgおよびhf af bf cタイプの間にも、ワイヤを接続することで存在します関数fmap gおよびfmap h

または、a, b, c, ...の世界で「左側」に描画できる接続図であれば、関数を変更することでf a, f b, f c, ...の世界で「右側」に描画できますg, h, ...を関数fmap g, fmap h, ...に変更し、関数id :: a -> aを関数fmap idに変更します。これらもFunctorの法則により、単なるid :: f a -> f aです。

4
Will Ness