我不小心写了一个
Haskell 函子教程
我将使用示例回答您的问题,并将类型放在 cmets 中。
注意类型中的模式。
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 做什么?让我们使用 Maybe 数据类型,它有两种类型的值,Nothing 和 Just x。 (您可以使用Nothing 表示未能获得答案,而Just x 表示答案。)
fmap (+7) (Just 10) = Just 17
fmap (+7) Nothing = Nothing
-- Int->Int Maybe Int Maybe Int
好的,fmap 再次使用 (+7) inside Maybe。
我们也可以 fmap 其他函数。 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 类型的值是 Left a 值或 Right b 值。有时我们使用 Either 来表示成功Right goodvalue 或失败Left errordetails,有时只是将两种类型的值混合在一起。无论如何,Either 数据类型的函子仅适用于 Right - 它只留下 Left 值。如果您将 Right 值用作成功的值,这尤其有意义(事实上,我们不会 能够 使其同时适用于两者,因为类型不一定相同)。让我们以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 中工作,但对于 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
在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 给我们的字符串并将它提供给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 ("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] 有 Ints。)
fmap :: (a -> b) -> Something a -> Something b
这适用于Something 是列表(写为[])、Maybe、Either String、Either Int、IO 和大量的东西。如果它以合理的方式工作(有一些规则 - 稍后),我们称它为 Functor。 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 是什么时候?
附录: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 公平、平等地在任何地方应用您的函数,并且没有任何其他更改。这是一个很好、干净、清晰、可靠、可重复使用的东西。