簡體   English   中英

函子如何在haskell中工作?

[英]How do functors work in haskell?

我正在嘗試學習 Haskell,並且我已經掌握了所有基礎知識。 但現在我被困住了,試圖讓我的頭腦圍繞函子。

我讀過“函子將一個類別轉換為另一個類別”。 這是什么意思?

我知道有很多問題要問,但是誰能給我一個簡單的英文解釋函子或者一個簡單的用例

我不小心寫了一個

Haskell 函子教程

我會用例子回答你的問題,我會把類型放在評論下面。

注意類型中的模式。

fmapmap的泛化

函子用於為您提供fmap函數。 fmap工作方式類似於map ,所以讓我們先看看map

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

因此,它使用的功能(subtract 1)名單 事實上,對於列表, fmapmap完全一樣。 這次讓我們將所有東西乘以 10:

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

我將其描述為映射在列表上乘以 10 的函數。

fmap也適用於Maybe

我還能fmap什么? 讓我們使用 Maybe 數據類型,它有兩種類型的值, NothingJust 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 StringEither 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 的方法之一是在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是列表的的(書面[] MaybeEither StringEither IntIO和過負荷的事情。 如果它以合理的方式工作,我們稱它為函子(有一些規則 - 稍后)。 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那個時候?

附錄:Functor 規則是什么,為什么我們有它們?

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 是一個定義明確的概念,但您通常可以用這個模糊的類比來推理它。

理解正在發生的事情的最好辦法就是閱讀每個實例的定義,這應該會讓您對正在發生的事情有一個直觀的了解。 從那里開始,真正使您對概念的理解正式化只是一小步。 需要添加的是澄清我們的“容器”究竟是什么,每個實例都非常滿足一對簡單的定律。

將函子本身與應用了函子的類型中的值區分開來,這一點很重要。 函子本身是一個類型構造函數,如MaybeIO或列表構造函數[] 函子中的值是應用了該類型構造函數的類型中的某個特定值。 例如Just 3是類型putStrLn "Hello World" Maybe Int中的一個特定值(該類型是應用於類型IntMaybe函子), 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 IntChar -> 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的列表, applyToMaybeMaybe s和applyToIOIO 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 類型構造函數也是如此,例如MaybeMap 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,事情很快就會變得更有趣,但這超出了本答案的范圍。

1Set不是,因為它只能存儲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類型的事物。 請注意,我們無法在這里獲得ab類型的東西。 函數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

這意味着無論abc類型之間通過連接連線存在什么關系,可以說是gh等函數,通過連接fmap g和函數的連線fafbfc類型之間也存在關系。 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM