函数在haskell中如何工作?
我正在努力学习Haskell,而且我正在经历所有的基础知识。 但是现在我陷入了困境,试图让我的脑袋围绕着仿函数。
我读过“一个仿函数将一个类别转换成另一个类别”。 这是什么意思?
我知道有很多要问,但是任何人都可以给我一个简单的英语解释函子或者一个简单的用例吗?
一个模糊的解释是,一个Functor
是某种容器和一个相关的函数fmap
,允许你改变包含的东西,给定一个函数来转换包含的内容。
例如,列表就是这种容器, fmap (+1) [1,2,3,4]
产生[2,3,4,5]
。
Maybe
也可以做一个函子,例如fmap toUpper (Just 'a')
产生fmap toUpper (Just 'a')
Just 'A'
。
fmap
的一般types显示的非常整齐:
fmap :: Functor f => (a -> b) -> fa -> fb
而专业版本可能会更清晰。 这是列表版本:
fmap :: (a -> b) -> [a] -> [b]
和Maybe版本:
fmap :: (a -> b) -> Maybe a -> Maybe b
您可以通过查询GHCI获得关于标准Functor
实例的信息:i Functor
和许多模块定义了Functor
(和其他types的类)的更多实例。
尽pipe如此,请不要太认真地对待“容器”这个词。 Functor
是一个明确的概念,但是你可以用这个模糊的类比来推理它。
理解发生的事情的最好方法是简单地阅读每个实例的定义,这应该让你直观地了解正在发生的事情。 从那里开始真正正式理解你的概念只是一小步。 需要补充的是澄清我们的“容器”究竟是什么,每一个实例都满足一对简单的法则。
我意外地写了一个
Haskell Functors教程
我会用示例来回答你的问题,然后我将下面的types放在注释中。
注意types中的模式。
fmap
是map
一个泛化
函子是给你的fmap
函数。 fmap
像map
fmap
工作,所以让我们先看看map
:
map (subtract 1) [2,4,8,16] = [1,3,7,15] -- Int->Int [Int] [Int]
所以它使用列表中的函数(subtract 1)
。 实际上,对于列表来说, fmap
确实可以实现map
function。 这次我们把所有东西都乘以10
fmap (* 10) [2,4,8,16] = [20,40,80,160] -- Int->Int [Int] [Int]
我将这个描述为映射在列表中乘以10的函数。
fmap
也适用于Maybe
我还能做什么? 让我们使用Maybe数据types,它有两种types的值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其他的function。 length
find一个列表的长度,所以我们可以通过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
来把东西变成string。 暗中show
的实际types是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
。 任何Left a
typesLeft a
值都可以是Left a
值或Right b
值。 有时候我们用Either来表示成功Right goodvalue
或failure Left errordetails
错误Right goodvalue
,有时候只是将两种types的值混合成一个。 无论如何,Either数据types的函子只能在Right
– 它只留下Left
值。 这是有道理的,尤其是如果你使用正确的值作为成功的(事实上,我们将无法使它们都工作,因为types不一定相同)。 让我们使用typesEither 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
上Either Int String
,只要函数在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操作给我的值。 让我们来举个例子,让你input一些东西,然后直接打印出来:
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,并把它送到putStrLn
需要一个string。 如果我们只想迎接用户呢?
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))
这并不比版本更好。 事实上这个符号在那里,所以你不必这样做。 但是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
我们可以把这个把戏放在任何我们给的东西上。 让我们不同意是否input“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
做了什么,它做了什么?
如果你一直在观察types中的模式并思考这些例子,你会注意到fmap接受了一个对某些值起作用的函数,并且将某个函数应用于某些具有或产生这些值的函数,编辑这些值。 (例如,readLn是读取Bool的,所以如果是IO Bool
Bool,那么它就是一个布尔值,因为它产生了一个Bool
,例如2 [4,5,6]
就有Int
。
fmap :: (a -> b) -> Something a -> Something b
这适用于List-of(写入[]
), Maybe
, Either String
, Either Int
, IO
和加载的东西。 如果这种方法合理,我们称之为函子(后面有一些规则)。 fmap的实际types是
fmap :: Functor something => (a -> b) -> something a -> something b
但为了简洁,我们通常用f
来replacesomething
。 尽pipe如此,编译器也是如此:
fmap :: Functor f => (a -> b) -> fa -> fb
回头看看types,并检查它总是有效的 – 关于Either String Int
小心 – 那是什么时候?
附录: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
因为不仅数据发生了变化,数据的结构或上下文也发生了变化。
当我在一个graphics用户界面项目上工作时,我曾经触及过这个规则 – 我想能够编辑这些值,但是我不能在不改变下面的结构的情况下这样做。 没有人会真的注意到它们之间的差异,因为它具有相同的效果,但是意识到它不遵守函子的规则,所以我重新思考了我的整个devise,现在它更干净,更光滑,速度更快。
其次是构图:这意味着你可以select是一次fmap一个函数,还是同时fmap它们。 如果fmap
离开了你的值的结构/上下文,只是用给定的函数编辑它们,它也可以用这个规则。
为什么我们有他们? 为了确保fmap
不会偷偷地在幕后做任何事情或改变我们没有想到的任何事情。 它们不是由编译器强制执行的(要求编译器在编译代码之前certificate一个定理是不公平的,并且会减慢编译速度 – 程序员应该检查)。 这意味着你可以作弊,但这是一个不好的计划,因为你的代码可以给出意想不到的结果。
在头部区分函数本身和应用函子的types的值是非常重要的。 函子本身是一个像Maybe
, IO
或者列表构造函数[]
的types构造函数。 函子中的值是应用了该types构造函数的types中的某个特定值。 例如, Just 3
是Maybe Int
types的一个特定值(该types是应用于Maybe Int
types的Maybe
函数), putStrLn "Hello World"
是typesIO ()
中的一个特定值, [2, 4, 8, 16, 32]
是types[Int]
中的一个特定值。
我喜欢用一个函数来考虑一个函数的值,这个函数和基types的值是“相同的”,但是有一些额外的“上下文”。 人们经常用一个容器来比喻一个函子,这个函数对于很多函子来说很自然地起作用,但是当你不得不说服IO
或(->) r
就像一个容器时,它变得更加困难。
所以如果一个Int
代表一个整数值,那么一个Maybe Int
代表一个可能不存在的整数值(“可能不存在”是“上下文”)。 一个[Int]
表示一个具有许多可能值的整数值(这与列表函子的解释与列表monad的“非确定性”解释相同)。 IO Int
表示整数值,其精确值取决于整个Universe(或者,它表示可以通过运行外部进程获得的整数值)。 一个Char -> Int
是任何Char
值的整数值(“将r
作为参数的函数”是任何typesr
的函子; r
是Char
(->) Char
是types构造函数,它是一个函数到Int
变成(->) Char Int
或Char -> Int
以中缀表示法)。
你可以用一个普通的函子做的唯一事情就是fmap
,types为Functor f => (a -> b) -> (fa -> fb)
。 fmap
将一个运行在正常值上的函数转换成一个函数,该函数对一个函子添加额外的上下文值进行操作; 这对于每个函子来说究竟有什么不同,但是你可以用它们来完成。
因此,使用Maybe
函数fmap (+1)
是计算可能不存在的整数1的函数,其高于其可能不存在的整数。 使用列表函数fmap (+1)
是计算非确定性整数1的函数,它高于input的非确定性整数。 使用IO
fmap (+1)
函数, fmap (+1)
是计算高于其input整数的整数1的函数,其值取决于外部宇宙。 使用(->) Char
函数, fmap (+1)
是一个函数,它将1加到一个取决于Char
的整数上(当我将一个Char
给返回值时,喂相同的Char
到原始值)。
但是一般情况下,对于一些未知的函数f
,应用于f Int
某个值的fmap (+1)
是函数(+1)
在常规Int
的“函子版本”。 它将这个特定的函子所具有的任何“上下文”types的整数加1。
fmap
本身并不一定是有用的。 通常当你编写一个具体的程序并且使用一个函数时,你正在使用一个特定的函数,而且你通常认为fmap
就是它为那个特定函数所做的事情 。 当我使用[Int]
,我经常不会把我的[Int]
值看作非确定的整数,我只是把它们看成是整数列表,我认为fmap
和map
我一样。
那么为什么要用仿函数呢? 为什么不只是有map
的列表, applyToMaybe
Maybe
是s和applyToIO
IO
呢? 那么每个人都会知道他们做了什么,没有人会理解奇怪的抽象概念,如函子。
关键是认识到有很多function , 几乎所有的容器types都是一开始的(因此容器类似于函子的types )。 他们每个人都有一个对应于fmap
的操作,即使我们没有函子。 无论什么时候你只是根据fmap
操作(或者map
,或者你所要求的特定types)来编写一个algorithm,那么如果你用functor而不是你自己的types来编写它,那么它对于所有的函子都是适用的。
它也可以作为文档的一种forms。 如果我将一个列表值赋给一个你在列表上操作的函数,它可以做任何事情。 但是,如果我把我的列表交给你编写的函数,这个函数对任意函子的值进行操作,那么我知道函数的实现不能使用列表特征,只能使用函子特征。
回想一下,如何在传统的命令式编程中使用有趣的东西,可能有助于看到好处。 像数组,列表,树等容器types,通常会有一些你用来遍历它们的模式。 对于不同的容器,它可能会略有不同,尽pipe库通常提供标准的迭代接口来解决这个问题。 但是每当你想迭代它们时,你仍然会写一个for循环,当你想要做的是计算容器中每个项目的结果,并收集你通常最终在逻辑中混合的所有结果随时随地构build新的容器。
fmap
是每一个你永远不会写的forms的循环,在你甚至坐下来编程之前,一次又一次地由图书馆编写者进行sorting。 另外它也可以用于Maybe
和(->) r
,可能不会被认为与命令式语言中devise一致的容器接口有关。
在Haskell中,函数捕获了容器中“东西”的概念,这样就可以在不改变容器形状的情况下操纵这些“东西”。
函子提供了一个函数fmap
,它可以让你做到这一点,通过一个常规的函数,并将它从一个元素types的容器“提升”到一个函数:
fmap :: Functor f => (a -> b) -> (fa -> fb)
例如,列表types构造函数[]
是一个函子:
> fmap show [1, 2, 3] ["1","2","3"]
其他许多Haskelltypes的构造函数也是如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
。 在forms上,我们需要fmap id = id
,也就是说,如果fmap
的身份函数没有任何变化。
到目前为止,我一直在使用术语“容器”,但是它比这更一般。 例如, IO
也是一个fmap
函数,在这种情况下我们所说的“形状”就是IO
动作的fmap
不应该改变副作用。 实际上,任何monad都是一个函子2 。
在类别理论中,函子允许你在不同的类别之间进行转换,但是在Haskell中,我们只有一个类别,通常称为Hask。 因此,Haskell中的所有函数都从Hask转换为Hask,所以它们就是我们所说的endofunctors(从一个类到本身的函子)。
在最简单的forms中,仿函数有点无聊。 只有一个操作,你只能做很多事情。 但是,一旦你开始添加操作,你可以从常规的函子去应用函子monad,事情很快就会变得更有趣,但这超出了这个答案的范围。
1但是Set
不是,因为它只能存储Ord
types。 玩家必须能够包含任何types。
2由于历史的原因, Functor
不是Monad
的超类,尽pipe许多人认为它应该是。
我们来看看types。
Prelude> :i Functor class Functor f where fmap :: (a -> b) -> fa -> fb
但是,这是什么意思?
首先, f
是一个typesvariables,它代表一个types构造函数: fa
是一个types; a
是某种types的typesvariables。
其次,给定一个函数g :: a -> b
,你将得到fmap g :: fa -> fb
。 即fmap g
是一个函数,将fa
types的东西转化为fb
types的东西。 注意,在这里我们不能得到typesa
和b
东西。 函数g :: a -> b
以某种方式在fa
types的东西上工作,并将它们转换为fb
types的东西。
注意f
是一样的。 只有其他types改变。
这意味着什么? 这可能意味着很多东西。 f
通常被看作是东西的“容器”。 然后, fmap g
使g
能够在这些容器的内部作用,而不会打开它们。 结果仍然被封闭在“里面”,typeclass Functor
并没有给我们打开它们的能力,或者偷看到里面。 只是在不透明的东西里面的一些转变就是我们所得到的。 任何其他function将不得不从别的地方来。
还要注意的是,这并不是说这些“容器”只是一个“ a
”typesa
东西; 里面可以有很多单独的“东西”,但是都是同一种typesa
。
最后,任何一个函子的候选人都必须遵守函数法则 :
fmap id === id fmap (f . g) === fmap f . fmap g