我正在尝试学习Haskell,已经掌握了所有基础知识。但是现在我陷入了困境,试图理解functor。
我读到过“functor将一个范畴转换成另一个范畴”。这是什么意思?
我知道这很难,但有人能否给我提供一个通俗易懂的functor解释或者一个简单的用例呢?
我正在尝试学习Haskell,已经掌握了所有基础知识。但是现在我陷入了困境,试图理解functor。
我读到过“functor将一个范畴转换成另一个范畴”。这是什么意思?
我知道这很难,但有人能否给我提供一个通俗易懂的functor解释或者一个简单的用例呢?
我不小心写错了
我将使用示例回答您的问题,并在注释中放置类型。
注意类型中的模式。
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
好的,那么再次重申一下,fmap
在 Maybe 内部使用 (+7)
。我们也可以对其他函数使用 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
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”不达成一致:
我们可以对我们得到的任何东西使用这个技巧。让我们就输入的是“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接受一个以某些值为基础的函数,并将该函数应用于具有或产生这些值的某些内容,从而编辑这些值。(例如,readLn是用来读取Bool的,因此其类型为IO Bool
,其中包含了一个布尔值,因为它产生了一个Bool
,例如2:[4,5,6]
中有Int
。)
fmap :: (a -> b) -> Something a -> Something b
这适用于Something
是List-of
(写作 []
),Maybe
, Either String
, Either Int
, IO
和许多其他情况。 如果以合理的方式使用,则我们将其称为Functor(稍后会有一些规则)。
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
也可以成为一个 Functor
,使得 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
是一个明确定义的概念,但您经常可以用这种模糊的比喻来推理它。
了解正在发生什么的最佳方法就是阅读每个实例的定义,这应该可以给您一些直觉。从那里开始,只需迈出一小步即可真正形式化您对概念的理解。需要添加的是关于我们的“容器”实际上是什么以及每个实例都必须满足一对简单法律的澄清。
IO
的情况)。因此,在开始时,人们可以将其视为附加到值的某种“计算”,而不是包含值的“容器”。 - Anton GuryanovIO
本身不是纯 Haskell。我发现这个解释更清晰:考虑 putStrLn :: String -> IO()
。这个函数接受一个 String
并返回一个 IO()
,可以将其视为一种计算,即在此情况下输出到 stdout
。这里的容器解释不如计算解释清晰。但这只是我的观点。 - Anton GuryanovConst a
函子 :) - is7s[]
与map
合作,对于类型,它给出了[] a = [a]
,对于函数,它给出了map f
。为什么[]不在Hask范畴内被简单地称为态射? 因为在Hask中,态射是作用于值的函数,而不是作用于类型和函数的函子。 - AndrewCMaybe
、IO
或者列表构造器 []
。Functor中的值是应用了该类型构造器的某个特定类型中的某个特定值。例如,Just 3
是类型 Maybe Int
中某个特定值(该类型是将类型构造器 Maybe
应用于类型 Int
),putStrLn "Hello World"
是类型 IO ()
中的某个特定值,[2, 4, 8, 16, 32]
是类型 [Int]
中的某个特定值。IO
或者 (->) r
就像一个容器时,它就变得更加阻碍理解了。因此,如果一个Int
表示一个整数值,那么一个Maybe Int
表示可能不存在的整数值("可能不存在"是上下文)。[Int]
表示具有许多可能值的整数值(这与列表函子的“非确定性”解释相同)。IO Int
表示整数值,其精确值取决于整个宇宙(或者,它表示可以通过运行外部进程获得的整数值)。对于任何Char
值,Char -> Int
都是一个整数值(“以r
为参数的函数”是任何类型r
的函子;对于r
为Char
,(->) Char
是一个函子类型构造器,它应用于Int
变成(->) Char Int
或用中缀符号表示为Char -> Int
)。fmap
,类型为Functor f => (a -> b) -> (f a -> f b)
。 fmap
将在普通值上操作的函数转换为在由函子添加附加上下文的值上操作的函数;对于每个函子,这样做的具体方式不同,但您可以对它们使用fmap
。使用 Maybe
函子,fmap (+1)
是一个用于计算可能不存在的整数,返回它的输入加一后的结果(如果存在)。使用列表函子 fmap (+1)
是一个用于计算非确定性整数的函数,返回它的输入加一后的非确定性整数。对于 IO
函子,fmap (+1)
是一个用于计算依赖于外部环境的整数 input integer-whose-value-depends-on-the-external-universe,并返回其加一后的结果。对于 (->) Char
函子,fmap (+1)
是一个将依赖于 Char
的整数加一的函数(当我把一个 Char
作为返回值的参数时,我得到比将同样的 Char
作为原始值参数时多一的结果)。f
,应用于 f Int
中的某个值的 fmap (+1)
是普通 Int
类型上 (+1)
函数的“函子版本”。它将一添加到这个特定函子的“上下文”中的整数中。fmap
并不一定有很大的用处。通常,当您编写具体的程序并使用函子时,您会使用特定的函子,并且会将 fmap
视为它在该特定函子上所执行的操作。当我使用 [Int]
时,我通常不会将我的 [Int]
值视为非确定性整数,而是将它们视为整数列表,并且我将 fmap
的作用方式视为与 map
相同。那么为什么要使用函数对象?为什么不仅仅为列表使用map
,对于Maybe
使用applyToMaybe
,对于IO
使用applyToIO
呢?这样每个人都会知道它们的作用,而且没有人需要理解函数对象这种奇怪的抽象概念。fmap
的操作,即使我们没有函数对象。每当你仅通过fmap
操作(或map
,或任何适用于特定类型的命名)编写算法时,如果你将其编写为函数对象而不是特定类型,则它适用于所有函数对象。fmap
是你以后会写的所有这种格式的 for 循环,由库作者一次性排序好了,让你在编程之前就可以使用。此外,它还可以与像 Maybe
和 (->) r
这样的东西一起使用,这些东西在命令式语言中可能不会被视为设计一致的容器接口。
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
一个 Just
,它不能变成一个 Nothing
。从正式的角度来说,我们要求 fmap id = id
,也就是说,如果您 fmap
身份函数,什么都不会改变。IO
也是一个函子,在这种情况下,“形状”的意思是 fmap
对于一个 IO
操作不应该改变副作用。事实上,任何单子都是一个函子。Set
不是函子,因为它只能存储 Ord
类型。函子必须能够包含任何类型。由于历史原因,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
。也就是说,fmap g
是一个函数,将类型为 f a
的东西转换为类型为 f b
的东西。请注意,我们不能在这里获取类型为 a
或 b
的东西。函数 g :: a -> b
如何在类型为 f a
的东西上工作,并将它们转换为类型为 f b
的东西。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
和 fmap h
的“电线”在 f a
、f b
和 f c
类型之间存在同样的关系。
或者说,无论在 a, b, c, ...
世界中可以绘制出什么连接图,“右侧”的 f a, f b, f c, ...
世界中都可以通过将函数 g, h, ...
改为函数 fmap g, fmap h, ...
并将函数 id :: a -> a
改为 fmap id
,这些函数本身也只是 id :: f a -> f a
,从而满足函子定律。