[英]How do functors work in haskell?
我正在嘗試學習 Haskell,並且我已經掌握了所有基礎知識。 但現在我被困住了,試圖讓我的頭腦圍繞函子。
我讀過“函子將一個類別轉換為另一個類別”。 這是什么意思?
我知道有很多問題要問,但是誰能給我一個簡單的英文解釋函子或者一個簡單的用例?
我不小心寫了一個
我會用例子回答你的問題,我會把類型放在評論下面。
注意類型中的模式。
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
什么? 讓我們使用 Maybe 數據類型,它有兩種類型的值, Nothing
和Just x
。 (您可以使用Nothing
表示無法得到答案,而Just x
表示答案。)
fmap (+7) (Just 10) = Just 17
fmap (+7) Nothing = Nothing
-- Int->Int Maybe Int Maybe Int
OK,如此反復, fmap
使用(+7)
的也許里面。 我們也可以 fmap 其他函數。 length
找到列表的長度,因此我們可以將其 fmap 到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 ab
Left a
值是Left a
值或Right b
值。 有時我們使用Either 來表示成功的Right goodvalue
或失敗的Left errordetails
,有時只是將兩種類型的值混合在一起。 無論如何,Either 數據類型的函子只適用於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*)
在兩者內工作,但對於兩者,只有Right
值會發生變化。 但是我們可以在Either Int String
上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 的方法之一是在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
給我們的 String 並將它提供給接受 String 的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
我們可以在我們得到的任何東西上使用這個技巧。 讓我們不同意是否輸入了“真”或“假”:
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
,例如2 [4,5,6]
有Int
s 在里面。)
fmap :: (a -> b) -> Something a -> Something b
這個作品Something
是列表的的(書面[]
Maybe
, Either String
, Either Int
, IO
和過負荷的事情。 如果它以合理的方式工作,我們稱它為函子(有一些規則 - 稍后)。 fmap 的實際類型是
fmap :: Functor something => (a -> b) -> something a -> something b
但為簡潔起見,我們通常用f
替換something
。 不過對於編譯器來說都是一樣的:
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
只允許更改值,而不允許更改結構。 fmap
不允許將Just 4
轉換為Nothing
,或將[6]
轉換為[1,2,3,6]
,或將Right 4
轉換為Left 4
因為不僅僅是數據發生了變化 - 該數據的結構或上下文發生了變化。
我在處理圖形用戶界面項目時曾遇到過這條規則——我希望能夠編輯這些值,但如果不更改下面的結構,我就無法做到。 沒有人會真正注意到差異,因為它具有相同的效果,但意識到它不遵守函子規則讓我重新思考我的整個設計,現在它更干凈、更流暢、更快。
其次是組合:這意味着您可以選擇是一次 fmap 一個函數,還是同時 fmap 兩個函數。 如果fmap
考慮值的結構/上下文,而只是使用給定的函數對其進行編輯,則它也適用於此規則。
數學家有一個秘密的第三條規則,但我們在 Haskell 中不稱它為規則,因為它看起來就像一個類型聲明:
fmap :: (a -> b) -> something a -> something b
例如,這會阻止您將函數僅應用於列表中的第一個值。 該定律由編譯器強制執行。
為什么我們有它們? 確保fmap
不會在幕后偷偷摸摸地做任何事情或改變我們沒想到的任何事情。 它們不是由編譯器強制執行的(要求編譯器在編譯您的代碼之前證明定理是不公平的,並且會減慢編譯速度 - 程序員應該檢查)。 這意味着您可以稍微欺騙法律,但這是一個糟糕的計划,因為您的代碼可能會產生意想不到的結果。
Functor 的法則是確保fmap
公平地、平等地、無處不在地應用你的函數,並且沒有任何其他變化。 這是一個很好的、干凈的、清晰的、可靠的、可重復使用的東西。
一個模糊的解釋是,一個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]
和Maybe版本:
fmap :: (a -> b) -> Maybe a -> Maybe b
您可以通過使用:i Functor
查詢 GHCI 來獲取有關標准Functor
實例的信息,許多模塊定義了更多Functor
實例(和其他類型類)。
不過,請不要太認真對待“容器”這個詞。 Functor
s 是一個定義明確的概念,但您通常可以用這個模糊的類比來推理它。
理解正在發生的事情的最好辦法就是閱讀每個實例的定義,這應該會讓您對正在發生的事情有一個直觀的了解。 從那里開始,真正使您對概念的理解正式化只是一小步。 需要添加的是澄清我們的“容器”究竟是什么,每個實例都非常滿足一對簡單的定律。
將函子本身與應用了函子的類型中的值區分開來,這一點很重要。 函子本身是一個類型構造函數,如Maybe
、 IO
或列表構造函數[]
。 函子中的值是應用了該類型構造函數的類型中的某個特定值。 例如Just 3
是類型putStrLn "Hello World"
Maybe Int
中的一個特定值(該類型是應用於類型Int
的Maybe
函子), putStrLn "Hello World"
是類型IO ()
中的一個特定值,而[2, 4, 8, 16, 32]
是[Int]
類型中的一個特定值。
我喜歡將應用了函子的類型中的值視為與基類型中的值“相同”,但具有一些額外的“上下文”。 人們經常使用容器來比喻函子,這對相當多的函子來說很自然,但是當你不得不說服自己IO
或(->) r
就像一個容器時,它變得更像是一種障礙而不是幫助。
因此,如果Int
表示整數值,則Maybe Int
表示可能不存在的整數值(“可能不存在”是“上下文”)。 [Int]
表示具有多個可能值的整數值(這與列表函子的解釋與列表單子的“非確定性”解釋相同)。 一個IO Int
代表一個整數值,其精確值取決於整個 Universe(或者,它代表一個可以通過運行外部進程獲得的整數值)。 Char -> Int
是任何Char
值的整數值(“以r
作為參數的函數”是任何類型r
的函子;使用r
作為Char
(->) Char
是類型構造函數,它是一個函子,它應用to Int
變成(->) Char Int
或Char -> Int
中綴表示法)。
您可以使用通用函子做的唯一事情是fmap
,其類型為Functor f => (a -> b) -> (fa -> fb)
。 fmap
將一個對正常值進行操作的函數轉換為一個對值進行操作的函數,該函數具有由函子添加的附加上下文; 對於每個函子來說,這到底做什么是不同的,但是你可以對所有函子都這樣做。
因此,使用Maybe
函子fmap (+1)
是計算一個可能不存在的整數比其輸入的可能不存在整數高 1 的函數。 使用列表函子fmap (+1)
是計算比其輸入非確定性整數高 1 的非確定性整數的函數。 對於IO
函子, fmap (+1)
是計算比輸入整數高 1 的整數的函數,其值取決於外部宇宙。 使用(->) Char
函子, fmap (+1)
是將 1 添加到取決於Char
的整數的函數(當我將Char
給返回值時,我得到的值比我得到的值高 1將相同的Char
給原始值)。
但一般來說,對於某些未知的函子f
,應用於f Int
某個值的fmap (+1)
是普通Int
上函數(+1)
“函子版本”。 它在這個特定函子具有的任何類型的“上下文”中將整數加 1。
就其本身而言, fmap
不一定有用。 通常,當您編寫一個具體的程序並使用一個函fmap
,您正在使用一個特定的函子,並且您經常將fmap
視為它為該特定函子所做的任何事情。 當我工作[Int]
我經常沒有想到我的[Int]
值不確定性的整數,我只是認為他們作為整數列表,我想的fmap
以同樣的方式,我認為的map
。
那么為什么要為函子煩惱呢? 為什么不只是有map
的列表, applyToMaybe
的Maybe
s和applyToIO
為IO
S' 然后每個人都會知道他們在做什么,而沒有人需要理解像函子這樣奇怪的抽象概念。
關鍵是認識到那里有很多函子; 幾乎所有的容器類型開始(因此容器比喻什么函子)。 它們中的每一個都有一個對應於fmap
的操作,即使我們沒有函子。 每當您僅根據fmap
操作(或map
,或為您的特定類型調用它的任何內容)編寫算法時,如果您根據函子而不是您的特定類型編寫它,那么它適用於所有函子。
它也可以作為一種文檔形式。 如果我將我的一個列表值傳遞給您編寫的對列表進行操作的函數,它可以做很多事情。 但是,如果我將我的列表交給您編寫的對任意函子中的值進行操作的函數,那么我知道您的函數的實現不能使用列表功能,只能使用函子功能。
回想一下在傳統的命令式編程中如何使用函數式的東西可能有助於看到好處。 諸如數組、列表、樹等容器類型通常會有一些用於迭代它們的模式。 對於不同的容器,它可能略有不同,盡管庫通常提供標准迭代接口來解決這個問題。 但是每次你想迭代它們時,你最終還是要寫一個小 for 循環,當你想要做的是計算容器中每個項目的結果並收集所有結果時,你通常最終會在邏輯中混合用於隨時構建新容器。
fmap
是您將永遠編寫的那種形式的每個for 循環,在您坐下來編程之前,由庫編寫者一勞永逸地排序。 此外,它還可以與Maybe
和(->) r
類的東西一起使用,這些東西可能不會被視為與在命令式語言中設計一致的容器接口有任何關系。
在 Haskell 中,函子捕獲了擁有“東西”容器的概念,這樣您就可以在不改變容器形狀的情況下操作“東西”。
函子提供了一個函數fmap
,它可以讓你做到這一點,通過獲取一個常規函數並將它“提升”到一個函數,從一種元素的容器到另一種元素:
fmap :: Functor f => (a -> b) -> (f a -> f b)
例如, []
,列表類型構造函數,是一個函子:
> fmap show [1, 2, 3]
["1","2","3"]
許多其他 Haskell 類型構造函數也是如此,例如Maybe
和Map Integer
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
也是一個函子,在這種情況下我們所說的“形狀”是指IO
操作上的fmap
不應該改變副作用。 事實上,任何 monad 都是函子2 。
在范疇論中,函子允許你在不同范疇之間進行轉換,但在 Haskell 中我們只有一個范疇,通常稱為 Hask。 因此 Haskell 中的所有函子都從 Hask 轉換為 Hask,所以它們就是我們所說的內函子(函子從類別到自身)。
在最簡單的形式中,函子有點乏味。 只需一項操作,您就可以做很多事情。 但是,一旦開始添加操作,您就可以從常規函子到應用函子再到 monad,事情很快就會變得更有趣,但這超出了本答案的范圍。
1但Set
不是,因為它只能存儲Ord
類型。 函子必須能夠包含任何類型。
2由於歷史原因, Functor
不是Monad
的超類,盡管很多人認為它應該是。
讓我們來看看類型。
Prelude> :i Functor
class Functor f where fmap :: (a -> b) -> f a -> f b
但是,這是什么意思?
首先, f
在這里是一個類型變量,它代表一個類型構造函數: fa
是一個類型; a
是代表某種類型的類型變量。
其次,給定一個函數g :: a -> b
,你會得到fmap g :: fa -> fb
。 即fmap g
是一個函數,將fa
類型的事物轉換為fb
類型的事物。 請注意,我們無法在這里獲得a
或b
類型的東西。 函數g :: a -> b
以某種方式處理fa
類型的事物並將它們轉換為fb
類型的事物。
注意f
是相同的。 只有其他類型會發生變化。
那是什么意思? 它可能意味着很多事情。 f
通常被視為東西的“容器”。 然后, fmap g
使g
能夠作用於這些容器的內部,而不會打開它們。 結果仍然包含在“內部”中,類型類Functor
沒有為我們提供打開它們或窺視內部的能力。 我們得到的只是在不透明的事物中進行一些轉換。 任何其他功能都必須來自其他地方。
還要注意,它並不是說這些“容器”只攜帶一個類型為a
“東西”; 它的“內部”可以有許多單獨的“事物”,但都是相同的類型a
。
最后,任何一個函子的候選人都必須遵守函子定律:
fmap id === id
fmap (h . g) === fmap h . fmap g
注意這兩個(.)
運算符的類型是不同的:
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
等函數,通過連接fmap g
和函數的連線, fa
、 fb
和fc
類型之間也存在關系。 fmap h
。
或者,任何可以在“左側”繪制的連通圖,在a, b, c, ...
世界中,都可以在“右側”繪制,在fa, fb, fc, ...
世界中通過將函數g, h, ...
更改為函數fmap g, fmap h, ...
,並將函數id :: a -> a
更改為fmap id
其本身也只是id :: fa -> fa
,由函子定律。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.