私はHaskellを学ぼうとしていますが、すべての基本を学びました。しかし今、私は行き詰まり、ファンクターの周りに頭を抱えようとしています。
「ファンクターはあるカテゴリーを別のカテゴリーに変換する」と読みました。これは何を意味するのでしょうか?
質問がたくさんあることはわかっていますが、だれでもプレーンな英語ファンクタの説明または簡単な使用例を教えてもらえますか?
あいまいな説明としては、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
インスタンスに関する情報を取得できます。多くのモジュールは、Functor
s(および他の型クラス)のインスタンスをさらに定義します。
ただし、「コンテナ」という言葉をあまり真剣に受け止めないでください。 Functor
sは明確に定義された概念ですが、多くの場合、このファジーなアナロジーでそれについて推論できます。
何が起こっているのかを理解する最善の策は、単に各インスタンスの定義を読むことです。これにより、何が起こっているのかを直感的に理解できるはずです。そこからは、概念の理解を実際に正式化するための小さなステップにすぎません。追加する必要があるのは、「コンテナ」が実際に何であるかを明確にすることであり、各インスタンスは、単純な2つの法則を十分に満たします。
誤って書きました
例を使用して質問に答え、コメントの下にタイプを配置します。
タイプのパターンに注意してください。
fmap
はmap
の一般化ですファンクタは、fmap
関数を提供するためのものです。 fmap
はmap
と同様に機能するため、最初にmap
を確認してみましょう。
map (subtract 1) [2,4,8,16] = [1,3,7,15]
-- Int->Int [Int] [Int]
したがって、リストは関数(subtract 1)
insideを使用します。実際、リストの場合、fmap
はmap
が行うこととはまったく異なります。今回はすべてに10を掛けてみましょう:
fmap (* 10) [2,4,8,16] = [20,40,80,160]
-- Int->Int [Int] [Int]
これを、リストに10を掛ける関数をマッピングすることとして説明します。
fmap
はMaybe
でも機能します他に何をfmap
超えることができますか? Nothing
とJust 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]
fmap
はEither 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
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))
notはdo
バージョンよりも優れています。実際、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]
はInt
s inそれ。)
fmap :: (a -> b) -> Something a -> Something b
これはSomething
がList-of([]
と書かれている)、Maybe
、Either String
、Either Int
、IO
であり、物事の上。これが賢明な方法で機能する場合は、ファンクターと呼びます(いくつかのルールがあります-後で)。 fmapの実際のタイプは
fmap :: Functor something => (a -> b) -> something a -> something b
ただし、簡潔にするために、通常はsomething
をf
に置き換えます。ただし、コンパイラーにとってはすべて同じです。
fmap :: Functor f => (a -> b) -> f a -> f b
タイプを振り返り、これが常に機能することを確認してください-Either String Int
について注意深く-そのときのf
は何ですか?
id
は恒等関数です。
id :: a -> a
id x = x
ルールは次のとおりです。
fmap id == id -- identity identity
fmap (f . g) == fmap f . fmap g -- composition
まず、アイデンティティアイデンティティ:何もしない関数をマップしても、何も変更されません。これは明白に聞こえます(多くのルールではそうです)が、fmap
はonlyで値の変更が許可されていると解釈できます、構造ではありません。 fmap
は、Just 4
をNothing
に、または[6]
を[1,2,3,6]
に、またはRight 4
をLeft 4
に変換することはできません。データが変更されただけでなく、そのデータの構造またはコンテキストが変更されたためです。
グラフィカルユーザーインターフェイスプロジェクトで作業していたときに、このルールに一度ぶつかりました。値を編集できるようにしたかったのですが、その下の構造を変更せずにそれを行うことはできませんでした。同じ効果があったので、誰も違いに気づかなかったでしょうが、ファンクターのルールに従わないことに気付いたので、デザイン全体を再考することができました。今では、よりすっきりとして、すっきりとして、速くなっています。
次に、コンポジション:これは、一度に1つの関数をfmapするか、同時に両方をfmapするかを選択できることを意味します。 fmap
が値の構造/コンテキストをそのままにして、指定された関数を使用してそれらを編集するだけの場合、このルールでも機能します。
数学者には秘密の3つ目のルールがありますが、型宣言のように見えるため、Haskellではこれをルールと呼びません。
fmap :: (a -> b) -> something a -> something b
これにより、たとえば、リスト内の最初の値のみに関数を適用できなくなります。この法律は編集者によって施行されています。
なぜ私たちはそれらを持っているのですか? fmap
が裏で何かをこっそりと行ったり、予期しないものを変更したりしないようにするため。これらはコンパイラーによって強制されません(コードをコンパイルする前に定理を証明するようコンパイラーに要求するのは公平ではなく、コンパイルが遅くなります-プログラマーがチェックする必要があります)。つまり、法律を少しごまかすことができますが、コードが予期しない結果をもたらす可能性があるため、これは悪い計画です。
Functorの法則は、fmap
が関数を公平に、均等に、どこにでも適用し、他の変更を加えないようにすることです。それは、良い、クリーン、クリア、信頼できる、再利用可能なものです。
Haskellでは、ファンクターが「もの」のコンテナーを持つという概念を捉え、コンテナーの形状を変更せずにその「もの」を操作できるようにします。
ファンクタは1つの関数fmap
を提供します。これにより、通常の関数を使用して、あるタイプの要素のコンテナから別のタイプのコンテナに関数を「持ち上げ」ます。
fmap :: Functor f => (a -> b) -> (f a -> f b)
たとえば、リスト型コンストラクタである[]
はファンクタです。
> fmap show [1, 2, 3]
["1","2","3"]
Maybe
やMap 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 Just
Nothing
になることはできません。正式には、fmap id = id
、つまり、アイデンティティ関数をfmap
した場合、何も変更されません。
これまでは「コンテナ」という用語を使用してきましたが、実際にはそれよりも少し一般的です。たとえば、IO
もファンクタであり、その場合の「形状」とは、fmap
アクションのIO
が副作用を変更しないことを意味します。実際、どのモナドもファンクタです2。
カテゴリ理論では、ファンクタを使用すると、異なるカテゴリ間で変換できますが、Haskellでは、実際にはHaskと呼ばれる1つのカテゴリしかありません。したがって、HaskellのすべてのファンクタはHaskからHaskに変換されるので、それらは内部ファンクタ(カテゴリからそれ自体へのファンクタ)と呼ばれます。
最も単純な形では、ファンクタはやや退屈です。たった一回の操作でできることはたくさんあります。ただし、操作を追加し始めると、通常のファンクターからアプリケーションファンクター、モナドなどにすばやく移行できるようになりますが、それはこの回答の範囲を超えています。
1 ただし、Set
はOrd
タイプのみを格納できるため、そうではありません。ファンクタは任意のタイプを含むことができる必要があります。
2 歴史的な理由により、Functor
はMonad
のスーパークラスではありませんが、多くの人がそうであると考えています。
タイプを見てみましょう。
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
型のものに変換する関数です。ここでは、タイプa
もb
も取得できないことに注意してください。関数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
つまり、a
、b
、およびc
タイプの間に関係が存在する場合は、ワイヤーを接続することにより、関係が存在しますg
およびh
、f a
、f b
とf 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
です。