一.Functor は箱のよう?
箱の比喩
一般的な Functor クラスのインスタンスはどうやら箱(またはコンテナと呼ぶ)に例えることができます。例えば Maybe/Either、List([]):
> fmap (+1) (Just 3)
Just 4
> fmap (+1) (Right 3)
Right 4
> fmap (+1) [1, 2, 3]
[2,3,4]
fmap を通じて関数をコンテナ内の値に作用させ、新しい値を入れた同種のコンテナを取得します。I/O Action もこのように理解できます:
> fmap (++"!") getLine
abc
"abc!"
I/O Action クラスのコンテナの特殊な点は、コンテナ内の値が不確定で、外部入力に依存し、ユーザーの入力、ファイル読み取り、さらにはシステム環境から直接取得(例えば乱数シード)する可能性があることです。しかし確かなのは、I/O Action というコンテナには値が入っており(その値がどこから来ようと)、fmap はその値に関数を作用させ、同様に新しい値を入れた I/O Action を取得できることです
至此、箱の比喩は依然として適切です:純粋環境下のコンテナは木製の宝箱で、中には確定不変のものが入っており、不純環境下のコンテナは人食い宝箱で、中に何が入っているか分からない。下図の通り(六一剛過、少し遊んでみます):
[caption id="attachment_1722" align="alignnone" width="423"]
functor and box[/caption]
関数も Functor クラスのインスタンス?!
では、すべての Functor クラスのインスタンスはこのように理解できるのでしょうか?
instance Functor (Const m) -- Defined in 'Data.Functor.Const'
instance Functor (Either a) -- Defined in 'Data.Either'
instance Functor [] -- Defined in 'GHC.Base'
instance Functor Maybe -- Defined in 'GHC.Base'
instance Functor IO -- Defined in 'GHC.Base'
instance Functor ((->) r) -- Defined in 'GHC.Base'
instance Functor ((,) a) -- Defined in 'GHC.Base'
(注意:簡単にするため、上記に列挙したのは一般的な Functor インスタンスのみで、Applicative に属する 3 つの特殊な Functor インスタンスを除外しました)
其它は特に何もありませんが、((->) r) は少し奇妙に見え、関数定義(r map to something)のように見えます。定義を見てみましょう:
instance Functor ((->) r) where
fmap = (.)
((->) r) は確かに Functor クラスのインスタンスで、実装された fmap は関数合成(.)です:
(.) :: (b -> c) -> (a -> b) -> a -> c
b to c をマップする関数と a to b をマップする関数を受け取り、後者の出力を前者の入力に接続し、a to c をマップする関数を返します。これは私たちが知っている関数合成ですが、Functor とどのような関係があるのでしょうか?
まず Functor の fmap のタイプは:
fmap :: Functor f => (a -> b) -> f a -> f b
((->) r) も Functor インスタンスなので、f を ((->) r) に置き換えます:
fmap :: (a -> b) -> (->) r a -> (->) r b
最後に -> を慣習的な中置形式に変換します:
fmap :: (a -> b) -> (r -> a) -> (r -> b)
これは、関数合成(.)ではありませんか?
(.) :: (b -> c) -> (a -> b) -> a -> c
したがって、関数も Functor クラスのインスタンスです
P.S.では、((->) r) はなぜこんなに奇妙に見えるのでしょうか?Functor class は要求します:
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
f は具体的なタイプパラメータを 1 つ受け取るタイプ(* -> *)でなければならず、-> は:
(->) :: * -> * -> *
パラメータが 1 つ多いので、まず 1 つ埋めて、醜い (->) r が得られます(r は単なる形式パラメータ名で、a でも b でも構いません)
より適切な比喩
関数は、確かに箱とは考えにくいです。想像力が非常に豊富であれば、生化箱(魔斯拉)、または坩堝(魔女の森の新しいカード)などの内容を変化させられる箱、嗯、試験管 と考えられます
関数レベルの fmap は関数合成で、map a to b の関数に対して、map b to c のマッピングを行い、map a to c の新しい関数を取得します:
instance Functor ((->) r) where
fmap = (.)
(.) :: (b -> c) -> (a -> b) -> a -> c
以前の箱の比喩と比較:
fmapを通じて関数をコンテナ内の値に作用させ、新しい値を入れた同種のコンテナを取得
私たちが発明した生化箱に代入すると:fmap を通じて(生化)箱を(生化)箱に作用させ、新しい(生化)箱を取得
この 3 つの「(生化)箱」をどのように理解すればよいのでしょうか?
map a to b と map b to c を 2 本の試験管と考え、試験管が接続できるならば、無理やり通じます:
-- 試験管 ab は水を赤く変える
a -> b
-- 試験管 bc は赤水を青く変える
b -> c
-- 試験管 bc で ab にマッピングを行うのは、ab の底に穴を開け、bc 試験管にはめること
(b -> c) . (a -> b)
-- より長い新しい試験管 ac を取得し、水を青く変える作用
a -> c
なぜ試験管(または生化箱)に例えるのでしょうか?変換を指し、変化を表現したいからです。私たちが理解する箱には、このような変換作用の意味が欠けているため、この比喩は適切ではありません
したがって、関数文脈の Functor に対して
箱の比喩はそれほど適切ではなく、functors は実際には computation に似ています。function を computation に map over すると、その function でマッピングされた computation が得られます
上記の説明は非常に適切で、computation はデータ変換であり、変換はマッピングができ、マッピングを行う方法は合成 です
より正確な表現は functors は計算文脈(computational context)です。この文脈はこの computation が値を持つ可能性、または失敗する可能性(
MaybeとEither aのように)、または複数の値を持つ可能性(lists のように)などがあります。
したがって、箱と呼ばずに、計算文脈 と呼び、fmap はこの計算文脈に変換の層を追加する(マッピングを行う)ことに相当します
Lifting
もう一度 fmap のタイプ定義を見ましょう:
fmap :: Functor f => (a -> b) -> f a -> f b
map a to b の関数と Functor インスタンス a を入力し、別の Functor インスタンス b を返します。特に何もありません
別の姿勢でもう一度見ましょう:
fmap :: Functor f => (a -> b) -> (f a -> f b)
map a to b の関数を入力し、別の関数を返します。この関数の作用も map a to b ですが、Functor の文脈内にあります(パラメータと戻り値がすべて Functor に包まれています)、何か意味がありそうです
ある関数を別の環境下の対応する関数に変換することを、lifting(提升?適切な翻訳が見つかりません)と呼びます:
Lifting is a concept which allows you to transform a function into a corresponding function within another (usually more general) setting.
この例を見ましょう:
> replicate 3 'a'
"aaa"
> :t replicate
replicate :: Int -> a -> [a]
> :t liftA2 replicate
liftA2 replicate :: (Applicative f) => f Int -> f a -> f [a]
> (liftA2 replicate) [1,2,3] ['a','b','c']
["a","b","c","aa","bb","cc","aaa","bbb","ccc"]
> :t liftA2
liftA2 :: (Applicative f) => (a -> b -> c) -> (f a -> f b -> f c)
その中で liftA2 が行うことは lifting で、通常の関数(replicate)に基づいて機能が類似した新しい関数を作成し、新しい関数は別の環境(Applicative 文脈)に適用できます:
-- 通常の関数
Int -> a -> [a]
-- lift 一下
f Int -> f a -> f [a]
したがって、lift は通常の関数が f の文脈内で正常に動作できるようにする便利なものです
P.S.類似の lift 関数は合計 3 つあります:
liftA :: Applicative f => (a -> b) -> f a -> f b
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
liftA3 :: Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d
より多くのパラメータは <$> と <*> で秒速で定義できます。以下の Applicative instances 小節の (->) r 部分を参照
二.Functor laws
以前に言及しました:
Functor を実装する際にはいくつかのルールに従う必要があります。例えば List 要素の順序が変化しないことを望み、二分探索木がその構造性質を保持することを望むなど
([typeclass を深く理解する_Haskell ノート 4](/articles/深入 typeclass-haskell 笔记 4/) から引用)
したがって functor laws の作用は fmap を制約し、マッピング結果がいくつかの性質を保持するようにすることです:
functor laws を遵守すれば、それに対して
fmapを行っても余計なことはせず、単に関数でマッピングするだけであることがわかります
合計 2 条のルールがあります:
-
fmap id = id -
fmap (f . g) = fmap f . fmap g
P.S.2 条目は fmap (f . g) F = fmap f (fmap g F) と書くこともでき、合成を削除すると理解しやすくなります
1 条目、functor に対して map id を行う場合、得られる新しい functor は元のものと完全に同じであるべきです
2 条目、2 つの関数を合成して結果を functor に map over した結果は、まず 1 番目の関数を functor に map over し、次に 2 番目の関数を 1 番目のステップで得られた functor の結果に map over した結果と完全に同じであるべきです
(組み込みの)Functor クラスのインスタンスはすべてこれら 2 条のルールを満たします。例えば:
> fmap id (Just 3)
Just 3
> fmap id Nothing
Nothing
> fmap ((+1) . (*2)) (Just 3)
Just 7
> fmap (+1) . fmap (*2) $ Just 3
Just 7
しかし手動で実装した Functor インスタンスは必ずしもそうではありません。これら 2 条のルールは道徳的制約のみで、強制的なチェックはないため、カスタム Functor インスタンスを実装する際には自発的に遵守するよう注意すべきです
三.Applicative functors
名前から強化版の Functor とわかります。では、どこが強いのでしょうか?
Functor は map over できるものを 1 つのクラスに圏定し、Functor インスタンスに対して fmap を使用し、通常の関数を Functor の計算文脈に作用させることができます
十分に強力そうですが、いくつかの特殊なシーンがあります。例えば:
> :t fmap (+) (Just 3)
fmap (+) (Just 3) :: Num a => Maybe (a -> a)
これは何でしょうか?Maybe に関数が入っています(つまり Just (+3))。では、この関数をどのように取り出して使用すればよいのでしょうか?
例えば Just 2 に作用させたい場合、このようにします:
> let (Just f) = (Just (+3)) in fmap f (Just 2)
Just 5
まずパターンマッチで (+3) を取り出し、次に Just 2 に対して (+3) マッピングを行います。fmap だけでは Functor に包まれた関数を別の Functor に包まれた値に作用させることができないためです
では、あらゆる Functor に有効な汎用パターン はあるのでしょうか?これ(Functor 内の関数を別の Functor 内の値に作用させること)を完了するのを助けるものは?
あります。それが Applicative です:
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
(Applicative から引用)
まず Functor インスタンスでなければならないため、Applicative は特殊な Functor です。したがって Applicative functors とも呼ばれます
2 つのインターフェース pure と <*> を定義します
pureは通常の値をデフォルトの context 下に置き、最小の context ですが依然としてその値を含みます
どのように理解すればよいでしょうか?例を見ましょう:
> pure 1 :: [Int]
[1]
> pure 1 :: Maybe Int
Just 1
> pure 1 :: IO Int
1
List 而言、最小の context は [](空 List)なので、1 を入れて [1] を得ます。Maybe の場合、Nothing には値を保存する能力がなく、context は Just でなければならないため、Just 1 です。I/O Action の場合、もちろん return 1 です(return を通じて値を I/O Action に入れます)
<*> の作用は:
It applies the wrapped function to the wrapped value
这正是我们想要的、Functor 内の関数を別の Functor 内の値に作用させる
したがって、Applicative の Functor に対する強化は <*> 関数に現れ、強化方式はこれらの Functor インスタンスにすべて <*> を実装させ、Functor 内の関数を別の Functor 内の値に作用させることをサポートすることです
2 つの利点をもたらします。1 つ目は多パラメータ関数に対してより友好的であることです:
通常の functor の場合、(単一パラメータ)関数をこの functor に map over することしかできません。しかし applicative functor があれば、複数の functor に(多パラメータ)関数を適用できます
2 つ目は Functor の結合を許可することです(fmap は 1 回計算して Functor を得ると終了ですが、<*> を通じて引き続き演算を続けられます):
applicative functor は面白いだけでなく実用的で、異なる種類の計算を結合することを許可します。I/O 計算、non-deterministic な計算、失敗する可能性のある計算など。
<$>と<*>を使用して、任意の数の applicative functors に対して通常の関数を動作させることができます。
例えば:
> (+) <$> (Just 1) <*> (Just 2)
Just 3
> (\a b c -> a + b + c) <$> (Just 1) <*> (Just 2) <*> (Just 3)
Just 6
> (+3) <$> ((+) <$> (Just 1) <*> (Just 2))
Just 6
四.Applicative instances
Applicative クラスには多くのインスタンスがあります:
instance Monoid m => Applicative (Const m)
-- Defined in 'Data.Functor.Const'
instance Applicative (Either e) -- Defined in 'Data.Either'
instance Applicative ZipList -- Defined in 'Control.Applicative'
instance Monad m => Applicative (WrappedMonad m)
-- Defined in 'Control.Applicative'
instance Control.Arrow.Arrow a => Applicative (WrappedArrow a b)
-- Defined in 'Control.Applicative'
instance Applicative [] -- Defined in 'GHC.Base'
instance Applicative Maybe -- Defined in 'GHC.Base'
instance Applicative IO -- Defined in 'GHC.Base'
instance Applicative ((->) a) -- Defined in 'GHC.Base'
instance Monoid a => Applicative ((,) a) -- Defined in 'GHC.Base'
Maybe
instance Applicative Maybe where
pure = Just
Nothing <*> _ = Nothing
(Just f) <*> something = fmap f something
Maybe タイプ而言、値を演算に参加させる最小の context は Just something で、Nothing からは関数を取り出せないため、結果は必ず Nothing です。左側が Nothing でない場合、パターンマッチで関数 f を取り出し、fmap を通じて右側の Maybe インスタンス(something)に作用させます
List
instance Applicative [] where
pure x = [x]
fs <*> xs = [f x | f <- fs, x <- xs]
pure f は [f] で、[f] <*> xs は左側の各関数を右側の各値に適用します
P.S.pure f <*> xs は実際には fmap f xs と等価であることが簡単に見つかります。これも Applicative laws の 1 つです
IO
instance Applicative IO where
pure = return
a <*> b = do
f <- a
x <- b
return (f x)
pure の対応する実装は return で、値を I/O Action に包み、IO 演算に参加できるようにします。<*> が行うことは、左右両側の I/O Action からそれぞれ関数と値を取り出し、演算を完了した後 return で結果を包むことです
(->) r
instance Applicative ((->) r) where
pure x = (\_ -> x)
f <*> g = \x -> f x (g x)
これは少し奇妙に見え、pure は定数を返す関数を生成し、<*> は左右両側の関数を合成します
例えば:
(+) <$> (+3) <*> (*100)
その中で*<$> は中置版の fmap* です。以下の通り:
infixl 4 <$>
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<$>) = fmap
<*> と <$> はすべて infixl 4(中置左結合、優先順位は 4)なので、展開プロセスはこの通り:
(+) <$> (+3) <*> (*100)
=(fmap (+) (+3)) <*> (*100)
=((.) (+) (+3)) <*> (*100)
=((+) . (+3)) <*> (*100)
=\x -> ((+) . (+3)) x ((*100) x)
=\x -> (+) ((+3) x) ((*100) x)
つまり:
f1 <$> f2 <*> f3
=\x -> f1 (f2 x) (f3 x)
2 つの applicative functor を
<*>に与え��と、新しい applicative functor が得られます。したがって 2 つの関数を渡せば、新しい関数が得られます
したがって f1 <$> f2 <*> f3 の実際の効果は:f2 と f3 の結果をパラメータとして f1 を呼び出す関数を作成することです。したがって:
f1 <$> f2 <*> f3 <*> ... <*> fn
=\x -> f1 (f2 x) (f3 x) ... (fn x)
P.S.f1 <$> f2 <*> f3 という固定パターンにはツール関数があり、liftA2 と呼びます:
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
liftA2 f a b = f <$> a <*> b
liftA2は通常の 2 項関数を受け取り、それを 2 つの functor 上で動作できる関数にアップグレードします
これがいわゆる lifting(アップグレード?)です
ZipList
List と比較:
instance Applicative [] where
pure x = [x]
fs <*> xs = [f x | f <- fs, x <- xs]
ZipList の実装は以下の通り:
instance Applicative ZipList where
pure x = ZipList (repeat x)
ZipList fs <*> ZipList xs = ZipList (zipWith (\f x -> f x) fs xs)
P.S.ZipList は Control.Applicative モジュールに位置し、ZipList が存在する理由は、List に別の異なる Applicative 実装を再度与えることができないためです
pure は実際には無限長の ZipList を生成します。これは zipWith の結果が 2 つの List のうち短い方に基づくためです。したがって、x が正常に演算に参加できるように(もう一方の任意の長さの List を満たすために)、ZipList 而言、ZipList (repeat x) が最小の context です
<*> は左側から関数 List を取り出し、右側からデータ List を取り出し、2 つの List の要素を 1 つずつペアにしてマッピング(zipWith)を行います
左側の関数 List に同じ関数のみがある場合、この関数で右側の List にマッピングを行うのと同等です:
> getZipList $ pure (+1) <*> (ZipList [1, 2, 3])
[2,3,4]
P.S.その中で getZipList :: ZipList a -> [a] は getter(詳細は [タイプ_Haskell ノート 3 | Record](/articles/类型-haskell 笔记 3/#articleHeader8) を参照)で、中の List を取り出すために使用します
別の花样:
> getZipList $ (+) <$> ZipList [1, 2, 3] <*> (ZipList [1, 2, 3])
[2,4,6]
> getZipList $ max <$> ZipList [1, 3, 4, 5] <*> ZipList [2, 0]
[2,3]
五.Applicative laws
同様に、Applicative もいくつかのルールに従う必要があります:
pure f <*> x = fmap f x
pure id <*> v = v
pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
pure f <*> pure x = pure (f x)
u <*> pure y = pure ($ y) <*> u
pure を通じて通常の関数 f が Functor 演算に参加できるようにするため、以下の通り:
pure f <*> x = fmap f x
pure id <*> v = v
pure f <*> pure x = pure (f x)
<*> を通じて左側の Functor 中の関数が右側の Functor 中の値に作用できるようにするため、以下の通り:
-- $固定パラメータ位置
u <*> pure y = pure ($ y) <*> u
-- (.) 結合性を変更
pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
組み込みの Applicative インスタンスはすべてこれらのルールに従いますが、同様に道徳的制約のみで、手動で Applicative インスタンスを実装する際には自発的に遵守する必要があります
Applicative style
<$> と <*> を通じて非常に優雅な呼び出しスタイル を達成できます。例えば:
> (++) <$> Just "johntra" <*> Just "volta"
Just "johntravolta"
関数呼び出しと比較:
> (++) "johntra" "volta"
"johntravolta"
I/O シーンでより明確です:
myAction = do
a <- getLine
b <- getLine
return $ a ++ b
対応する applicative style:
myAction = (++) <$> getLine <*> getLine
非常に優雅で、Functor レベルの演算と通常の演算が形式的にほとんど差異がなくなります(形式的に演算が所在する context の差異を消除します)
コメントはまだありません