私はHaskellを学ぼうとしていて、すべての基本を学んでいます。しかし今、私は行き詰まり、ファンクターに頭を悩ませようとしています。
「ファンクターが1つのカテゴリーを別のカテゴリーに変換する」と読んだことがあります。これは何を意味するのでしょうか?
質問することがたくさんあることは知っていますが、ファンクターのわかりやすい英語の説明や、簡単な使用例を教えてもらえますか?
私はHaskellを学ぼうとしていて、すべての基本を学んでいます。しかし今、私は行き詰まり、ファンクターに頭を悩ませようとしています。
「ファンクターが1つのカテゴリーを別のカテゴリーに変換する」と読んだことがあります。これは何を意味するのでしょうか?
質問することがたくさんあることは知っていますが、ファンクターのわかりやすい英語の説明や、簡単な使用例を教えてもらえますか?
たまたま書いてしまった
例を使用して質問に答え、その下にタイプをコメントに入れます。
タイプのパターンに注意してください。
fmap
の一般化ですmap
ファンクターは、fmap
関数を提供するためのものです。fmap
のようmap
に動作するので、最初にチェックアウトしましょうmap
:
map (subtract 1) [2,4,8,16] = [1,3,7,15]
-- Int->Int [Int] [Int]
したがって、リスト(subtract 1)
内の関数を使用します。実際、リストの場合fmap
は、まさにそれを行いますmap
。今度はすべてを 10 倍してみましょう。
fmap (* 10) [2,4,8,16] = [20,40,80,160]
-- Int->Int [Int] [Int]
これは、リストに 10 を掛ける関数をマッピングすることと言えます。
fmap
にも取り組んでいますMaybe
他に何ができfmap
ますか?Nothing
2 種類の値を持つ Maybe データ型を使用してみましょうJust x
。(Nothing
は回答を表し、 は回答を得られないことを表すために使用できJust x
ます。)
fmap (+7) (Just 10) = Just 17
fmap (+7) Nothing = Nothing
-- Int->Int Maybe Int Maybe Int
OK、もう一度、Maybe の中fmap
で使用しています。また、他の関数も fmap できます。リストの長さを見つけるので、それを fmap できます(+7)
length
Maybe [Double]
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
いずれかです。場合によっては、 成功または失敗を表すために、または単に 2 つの型の値を 1 つに混合するために、どちらかを使用します。いずれにせよ、Either データ型のファンクタは、値をそのままにします。これは、Right 値を成功したものとして使用している場合に特に意味があります (実際には、型が必ずしも同じではないため、両方で機能させることはできません)。例としてタイプを使用しましょうLeft a
Right b
Right goodvalue
Left errordetails
Right
Left
Either String Int
fmap (5*) (Left "hi") = Left "hi"
fmap (5*) (Right 4) = Right 20
-- Int->Int Either String Int Either String Int
それは(5*)
、Either 内で機能しますが、Either の場合、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
IOで使用するのは特にクールです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))
これはバージョンよりも良くありません。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
のものだったので、 type には を生成するという意味でブール値がありBool
、eg2[4,5,6]
にはInt
s が含まれています。)
fmap :: (a -> b) -> Something a -> Something b
これは、リストSomething
オブ(書かれた[]
)、、、、、および以上のものに対して機能します。これが賢明な方法で機能する場合、それを Functor と呼びます (いくつかのルールがあります - 後で)。fmap の実際の型はMaybe
Either String
Either Int
IO
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
まず、ID ID: 何もしない関数をマップしても、何も変わりません。当たり前のように聞こえますが (多くのルールでそうです)、構造ではなく値の変更のみが許可されてfmap
いると解釈できます。データが変更されただけでなく、そのデータの構造またはコンテキストが変更されたため、、 、 、またはに変換することはできません。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
、関数が公平に、均等に、どこでも、他の変更なしに適用されることを確認することです。それは、優れた、きれいで、明確で、信頼性が高く、再利用可能なものです。
あいまいな説明は、 aFunctor
はある種のコンテナであり、含まれるfmap
ものを変換する関数が与えられた場合、含まれるものを変更できる関連関数であるということです。
たとえば、リストは、fmap (+1) [1,2,3,4]
yieldsなどのこの種のコンテナー[2,3,4,5]
です。
Maybe
fmap toUpper (Just 'a')
yieldsのようなファンクタにすることもできますJust 'A'
。
の一般的なタイプは、fmap
何が起こっているかを非常にきちんと示しています。
fmap :: Functor f => (a -> b) -> f a -> f b
そして、特化したバージョンはそれをより明確にするかもしれません. リストバージョンは次のとおりです。
fmap :: (a -> b) -> [a] -> [b]
そして多分バージョン:
fmap :: (a -> b) -> Maybe a -> Maybe b
Functor
GHCI にクエリを実行することで標準インスタンスに関する情報を得ることができ:i Functor
、多くのモジュールがFunctor
s (および他の型クラス) のインスタンスをさらに定義しています。
ただし、「コンテナー」という言葉をあまり深刻に受け止めないでください。Functor
s は明確に定義された概念ですが、多くの場合、このあいまいなアナロジーで推論できます。
何が起こっているのかを理解するための最善の策は、各インスタンスの定義を読むことです。これにより、何が起こっているのかを直感的に理解できるはずです。そこから、概念の理解を実際に形式化するのは小さなステップにすぎません。追加する必要があるのは、「コンテナ」が実際に何であるか、および各インスタンスが単純な法則のペアを十分に満たしていることを明確にすることです。
ファンクター自体と、ファンクターが適用された型の値との区別を頭の中で区別しておくことが重要です。ファンクター自体は、Maybe
、IO
、またはリスト コンストラクターのような型コンストラクター[]
です。ファンクターの値は、その型コンストラクターが適用された型の特定の値です。たとえばJust 3
、 は type 内の 1 つの特定の値Maybe Int
(その type は type にMaybe
適用されるファンクターですInt
)、putStrLn "Hello World"
は type 内の1 つの特定の値IO ()
、[2, 4, 8, 16, 32]
は type 内の 1 つの特定の値です[Int]
。
ファンクターが適用された型の値は、基本型の値と「同じ」であるが、追加の「コンテキスト」があると考えるのが好きです。人々はファンクターにコンテナの類推をよく使用します。これは、かなりの数のファンクターに対して非常に自然に機能しますが、コンテナーのようなものであるIO
と自分自身に納得させなければならないときは、助けというよりは邪魔になります。(->) r
したがって、Int
が整数値を表す場合、 aMaybe Int
は存在しない可能性のある整数値を表します (「存在しない可能性がある」が「コンテキスト」です)。An[Int]
は、可能な値の数を持つ整数値を表します (これは、リスト モナドの「非決定性」解釈と同じリスト ファンクターの解釈です)。AnIO Int
は、正確な値がユニバース全体に依存する整数値を表します (または、外部プロセスを実行することによって取得できる整数値を表します)。AChar -> Int
は任意の値の整数値ですChar
(「引数として取る関数r
」は任意の型のファンクターです。r
型コンストラクターはファンクターであり、r
Char
(->) Char
Int
(->) Char Int
Char -> Int
中置表記で)。
一般的なファンクターでできる唯一のことはfmap
、型を使用することFunctor f => (a -> b) -> (f a -> f b)
です。fmap
通常の値を操作する関数を、ファンクターによって追加された追加のコンテキストを持つ値を操作する関数に変換します。これが正確に何をするかはファンクターごとに異なりますが、すべてのファンクターで行うことができます。
したがって、Maybe
ファンクターfmap (+1)
は、入力された存在しない可能性のある整数よりも 1 大きい、存在しない可能性のある整数を計算する関数です。リストファンクターfmap (+1)
は、入力された非決定的整数よりも 1 大きい非決定的整数を計算する関数です。IO
ファンクターを使用すると、fmap (+1)
は、その値が外部宇宙に依存する入力整数よりも 1 大きい整数を計算する関数です。(->) Char
ファンクターでは、fmap (+1)
a に依存する整数に 1 を追加する関数です(戻り値にChar
a をフィードすると、元の値に同じものをフィードした場合よりも 1 高くなります)。Char
Char
しかし、一般に、未知の functorf
に対して、fmap (+1)
ある値に適用されるのは、通常の s のf Int
関数の「ファンクター バージョン」です。この特定のファンクターが持つ「コンテキスト」の種類に関係なく、整数に 1 を追加します。(+1)
Int
それ自体でfmap
は、必ずしもそれほど有用ではありません。通常、具体的なプログラムを作成してファンクターを操作する場合、特定のファンクターを操作していて、その特定のファンクターに対してfmap
何をするかをよく考えます。を扱っているとき、私は自分の値を非決定論的な整数とは考えていないことがよくあります。単に整数のリストと考えており、私が考えるのと同じ方法で を考えています。[Int]
[Int]
fmap
map
では、なぜファンクターを気にする必要があるのでしょうか。map
for リスト、applyToMaybe
for Maybe
s、およびapplyToIO
for sだけを持たないのはなぜIO
ですか? そうすれば、誰もが自分が何をしているかを知ることができ、ファンクターのような奇妙な抽象的な概念を理解する必要がなくなります。
鍵となるのは、世の中にはたくさんのファンクターがあるという認識です。ほぼすべてのコンテナー タイプから始めます (したがって、ファンクターとはコンテナーの類推です)。fmap
ファンクターがなくても、それぞれに に対応する操作があります。fmap
演算 (または特定の型に対して呼び出されるもの)だけでアルゴリズムを記述する場合は常にmap
、特定の型ではなくファンクターに関して記述すると、すべてのファンクターで機能します。
また、ドキュメントの形式としても機能します。私のリストの値の 1 つを、あなたが作成したリストを操作する関数に渡すと、さまざまなことができます。しかし、任意のファンクターの値を操作する、あなたが作成した関数にリストを渡すと、関数の実装ではリスト機能を使用できず、ファンクター機能のみを使用できることがわかります。
従来の命令型プログラミングで関数型のものをどのように使用したかを振り返ってみると、その利点がわかります。配列、リスト、ツリーなどのコンテナー タイプには、通常、反復処理に使用するパターンがあります。これはコンテナごとにわずかに異なる場合がありますが、ライブラリは多くの場合、これに対処するための標準的な反復インターフェイスを提供しています。しかし、それらを反復処理するたびに小さな for ループを作成することになり、コンテナ内の各項目の結果を計算してすべての結果を収集する場合は、通常、ロジックに混合してしまいます。新しいコンテナを構築するため。
fmap
は、プログラミングに着手する前に、ライブラリの作成者によって一度だけソートされた、その形式のすべての for ループです。さらに、命令型言語で一貫したコンテナー インターフェイスを設計することとはおそらく関係ないと思われるMaybe
ようなものにも使用できます。(->) r
Haskellでは、ファンクターは「もの」のコンテナーを持つという概念を捉えているため、コンテナーの形状を変更せずにその「もの」を操作できます。
fmap
ファンクターは、通常の関数を取得し、それをあるタイプの要素のコンテナーから別のタイプのコンテナーへの関数に「持ち上げる」ことによって、これを可能にする1つの関数を提供します。
fmap :: Functor f => (a -> b) -> (f a -> f b)
たとえば[]
、リスト型コンストラクターはファンクターです。
> fmap show [1, 2, 3]
["1","2","3"]
Maybe
そして、Map Integer
1のような他の多くのHaskell型構築子もそうです:
> fmap (+1) (Just 3)
Just 4
> fmap length (Data.Map.fromList [(1, "hi"), (2, "there")])
fromList [(1,2),(2,5)]
コンテナの「形状」を変更することは許可されていないことに注意してくださいfmap
。たとえばfmap
、リストの場合、結果には同じ数の要素が含まれ、あなたfmap
の場合は、Just
になることはできませんNothing
。正式にはfmap id = id
、つまりfmap
、恒等関数の場合、何も変更されないことが必要です。
これまで「コンテナ」という用語を使用してきましたが、実際にはそれよりも少し一般的です。たとえば、IO
はファンクターでもあり、その場合の「形状」とはfmap
、IO
アクションで副作用が変わらないことを意味します。実際、どのモナドも関手2です。
圏論では、ファンクターを使用すると、異なるカテゴリー間で変換できますが、Haskellでは、実際には1つのカテゴリーしかなく、多くの場合Haskと呼ばれます。したがって、HaskellのすべてのファンクターはHaskからHaskに変換されるため、エンドファンクター(カテゴリからそれ自体へのファンクター)と呼ばれます。
最も単純な形では、ファンクターはやや退屈です。1回の操作でできることはたくさんあります。ただし、操作の追加を開始すると、通常のファンクターからアプリケーションファンクター、モナドに移行でき、物事はすぐにさらに興味深いものになりますが、それはこの回答の範囲を超えています。
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
。つまり、 type のものfmap g
を type のものに変換する関数です。注意してください、タイプのものもここにも到達できません。この関数は、何らかの形で type のものに作用し、それらを type のものに変換するように作られています。f a
f b
a
b
g :: a -> b
f a
f b
同じであることに注意してくださいf
。もう一方のタイプのみが変更されます。
どういう意味ですか?それは多くのことを意味します。f
通常、ものの「入れ物」と見なされます。そして、これらのコンテナを壊すことなく、これらのコンテナの内部に作用することがfmap g
できます。g
結果は依然として「内部」に含まれており、型クラスは結果Functor
を開いたり、内部を覗いたりする機能を提供しません。不透明なものの中の変化だけが得られます。他の機能は、別の場所から取得する必要があります。
また、これらの「コンテナ」がタイプの「もの」を1つだけ運ぶとは言っていないことに注意してくださいa
。多くの個別の「もの」「内部」が存在する可能性がありますが、すべて同じタイプa
です。
最後に、ファンクターの候補は Functor の法則に従う必要があります。
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, ...
右側」に描ける。、Functor 法則によって。f a, f b, f c, ...
g, h, ...
fmap g, fmap h, ...
id :: a -> a
fmap id
id :: f a -> f a