Mat*_*sen 44 haskell functional-programming functor
我正在努力学习Haskell,而且我已经完成了所有的基础知识.但是现在我被卡住了,试图让我的头围绕着仿函数.
我读到"仿函数将一个类别转换为另一个类别".这是什么意思?
我知道这有很多问题,但是有人能给我一个关于仿函数的简单英语解释或者一个简单的用例吗?
And*_*ewC 115
我意外地写了一篇
我将使用示例回答您的问题,我会将这些类型放在评论中.
注意类型中的模式.
fmap
是一个概括 map
函数用于为您提供fmap
功能.fmap
有点像map
,所以让我们先看看map
:
map (subtract 1) [2,4,8,16] = [1,3,7,15]
-- Int->Int [Int] [Int]
Run Code Online (Sandbox Code Playgroud)
因此,使用函数(subtract 1)
内的列表.事实上,对于列表,fmap
exaclty做什么map
.让我们这次将所有东西乘以10:
fmap (* 10) [2,4,8,16] = [20,40,80,160]
-- Int->Int [Int] [Int]
Run Code Online (Sandbox Code Playgroud)
我将此描述为映射在列表中乘以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
Run Code Online (Sandbox Code Playgroud)
好了,再次fmap
使用(+7)
里面的可能.我们也可以fmap其他功能.length
找到列表的长度,所以我们可以将其fmapMaybe [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
Run Code Online (Sandbox Code Playgroud)
实际上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
Run Code Online (Sandbox Code Playgroud)
另外,回顾一下清单
fmap show [3,4,5] = ["3", "4", "5"]
-- Int->String [Int] [String]
Run Code Online (Sandbox Code Playgroud)
fmap
继续努力 Either something
让我们在略有不同的结构上使用它Either
.类型Either a b
的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
Run Code Online (Sandbox Code Playgroud)
它使得(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
Run Code Online (Sandbox Code Playgroud)
fmap
在IO 上使用它特别酷现在,我最喜欢使用fmap的一种方法是在IO
值上使用它来编辑某些IO操作给我的值.让我们举一个例子,让你输入内容,然后立即打印出来:
echo1 :: IO ()
echo1 = do
putStrLn "Say something!"
whattheysaid <- getLine -- getLine :: IO String
putStrLn whattheysaid -- putStrLn :: String -> IO ()
Run Code Online (Sandbox Code Playgroud)
我们可以用一种让我感觉更整洁的方式写出来:
echo2 :: IO ()
echo2 = putStrLn "Say something"
>> getLine >>= putStrLn
Run Code Online (Sandbox Code Playgroud)
>>
做了一件又一件事,但我喜欢这个的原因是因为>>=
拿了getLine
给我们的String并把它送到putStrLn
了一个String.如果我们想要问候用户怎么办?
greet1 :: IO ()
greet1 = do
putStrLn "What's your name?"
name <- getLine
putStrLn ("Hello, " ++ name)
Run Code Online (Sandbox Code Playgroud)
如果我们想以更整洁的方式写出来,我有点卡住了.我得写
greet2 :: IO ()
greet2 = putStrLn "What's your name?"
>> getLine >>= (\name -> putStrLn ("Hello, " ++ name))
Run Code Online (Sandbox Code Playgroud)
这是不是比更好do
的版本.事实上,do
符号是存在的,所以你不必这样做.但是可以fmap
来救援吗?是的,它可以.("Hello, "++)
是一个我可以通过getLine fmap的函数!
fmap ("Hello, " ++) getLine = -- read a line, return "Hello, " in front of it
-- String->String IO String IO String
Run Code Online (Sandbox Code Playgroud)
我们可以像这样使用它:
greet3 :: IO ()
greet3 = putStrLn "What's your name?"
>> fmap ("Hello, "++) getLine >>= putStrLn
Run Code Online (Sandbox Code Playgroud)
我们可以把这个伎俩放在我们给予的任何东西上.让我们不同意输入"True"或"False":
fmap not readLn = -- read a line that has a Bool on it, change it
-- Bool->Bool IO Bool IO Bool
Run Code Online (Sandbox Code Playgroud)
或者让我们只报告一个文件的大小:
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)
Run Code Online (Sandbox Code Playgroud)
fmap
做了什么,它做了什么?如果您一直在观察类型中的模式并考虑这些示例,那么您将注意到fmap采用了一些适用于某些值的函数,并将该函数应用于某些具有或以某种方式生成这些值的函数,编辑这些值.(例如readLn是为了阅读Bool,所以在类型中IO Bool
有一个布尔值,因为它产生一个Bool
,eg2中[4,5,6]
有Int
s.)
fmap :: (a -> b) -> Something a -> Something b
Run Code Online (Sandbox Code Playgroud)
这个工程的东西是列表的的(书面[]
)Maybe
,Either String
,Either Int
,IO
和在东西负载.如果它以合理的方式工作,我们将其称为Functor(有一些规则 - 稍后).fmap的实际类型是
fmap :: Functor something => (a -> b) -> something a -> something b
Run Code Online (Sandbox Code Playgroud)
但我们通常更换something
用f
的简洁.但是编译器完全相同:
fmap :: Functor f => (a -> b) -> f a -> f b
Run Code Online (Sandbox Code Playgroud)
回顾一下这些类型并检查它总是有用的 - Either String Int
仔细一点 - 那是什么f
时候?
id
是身份功能:
id :: a -> a
id x = x
Run Code Online (Sandbox Code Playgroud)
以下是规则:
fmap id == id -- identity identity
fmap (f . g) == fmap f . fmap g -- composition
Run Code Online (Sandbox Code Playgroud)
首先是身份标识:如果你映射的函数什么都不做,那就不会改变任何东西.这听起来很明显(许多规则都有),但你可以解释fmap
为只允许更改值,而不是结构.fmap
不允许Just 4
进入Nothing
,[6]
进入[1,2,3,6]
或Right 4
进入,Left 4
因为不仅仅是数据发生变化 - 该数据的结构或上下文发生了变化.
当我在处理图形用户界面项目时,我曾经遇到过这个规则 - 我希望能够编辑这些值,但是如果不改变下面的结构,我就无法做到.没有人会真正注意到它的不同之处,因为它具有相同的效果,但是意识到它不遵守仿函数规则让我重新思考我的整个设计,现在它更清晰,更光滑,更快.
其次是构图:这意味着您可以选择是一次fmap一个函数,还是同时fmap它们.如果fmap
单独留下您的值的结构/上下文并且只使用给定的函数编辑它们,它也将适用于此规则.
我们为什么要这些?为了确保fmap
不偷偷摸摸地在幕后做任何事情或改变我们没想到的任何事情.它们不是由编译器强制执行的(要求编译器在编译代码之前证明一个定理是不公平的,并且会减慢编译速度 - 程序员应该检查).这意味着你可以作弊,但这是一个糟糕的计划,因为你的代码会产生意想不到的结果.
Sar*_*rah 56
一个模糊的解释是a 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
Run Code Online (Sandbox Code Playgroud)
专业版可能会更清晰.这是列表版本:
fmap :: (a -> b) -> [a] -> [b]
Run Code Online (Sandbox Code Playgroud)
和Maybe版本:
fmap :: (a -> b) -> Maybe a -> Maybe b
Run Code Online (Sandbox Code Playgroud)
您可以Functor
通过查询GHCI 来获取有关标准实例的信息,:i Functor
并且许多模块定义了更多Functor
s(和其他类型类)的实例.
但是,请不要太认真地对待"容器"这个词.Functor
s是一个明确定义的概念,但你可以经常用这种模糊的类比来推理它.
理解正在发生的事情的最好方法是简单地阅读每个实例的定义,这应该让你对正在发生的事情有直觉.从那里开始,真正形式化您对概念的理解只是一小步.需要补充的是澄清我们的"容器"究竟是什么,并且每个实例都满足一对简单的法则.
Ben*_*Ben 12
重要的是要将一个仿函数本身与一个应用了仿函数的类型中的值区分开来.一个仿函数本身就是一个类型构造类似Maybe
,IO
或列表构造[]
.仿函数中的值是应用了该类型构造函数的类型中的某个特定值.例如,类型Just 3
中的一个特定值Maybe Int
(该类型是Maybe
应用于该类型的仿函数Int
),putStrLn "Hello World"
是类型中的一个特定值IO ()
,并且[2, 4, 8, 16, 32]
是该类型中的一个特定值[Int]
.
我喜欢考虑一个类型中的值,其中一个仿函数被应用为与基类型中的值"相同",但带有一些额外的"上下文".人们经常使用容器类比作为仿函数,这对于相当多的仿函数非常自然地起作用,但是当你不得不说服自己IO
或者(->) r
就像一个容器时,它会变成一种障碍而不是帮助.
因此,如果a Int
表示整数值,则a Maybe Int
表示可能不存在的整数值("可能不存在"是"上下文").An [Int]
表示具有多个可能值的整数值(这与列表仿函数的解释与列表monad的"nondeterminism"解释相同).An IO Int
表示一个整数值,其精确值取决于整个Universe(或者,它表示可以通过运行外部进程获得的整数值).阿Char -> Int
为任何整数值Char
的值("功能以r
作为一个参数"是用于任何类型的函子r
;具有r
如Char
(->) Char
为类型构造这是一个算符,Int
(->) Char Int
Char -> Int
在中缀符号).
对于一般仿函数fmap
,你唯一可以做的就是这种类型Functor f => (a -> b) -> (f a -> f b)
.fmap
将对正常值进行操作的函数转换为函数,该函数对由函数添加的附加上下文的值进行操作; 这对于每个仿函数到底有什么不同,但你可以用它们来完成.
因此,Maybe
functor fmap (+1)
是一个函数,它计算一个可能不存在的整数1,它高于其输入可能不存在的整数.使用list functor fmap (+1)
是一个函数,它计算一个高于其输入非确定性整数的非确定性整数1.使用IO
仿函数,fmap (+1)
函数计算比输入整数高1的整数 - 其值取决于外部宇宙.使用(->) Char
仿函数,fmap (+1)
是一个函数,它将1加到一个取决于a的整数Char
(当我输入一个Char
返回值时,我得到的值比通过将其提供Char
给原始值得高1).
但总的来说,对于一些未知的仿函数f
,fmap (+1)
应用于某些值的f Int
是(+1)
普通Int
s 上函数的"仿函数版" .它在这个特定的仿函数所具有的任何"上下文"中将整数加1.
就其本身而言,fmap
不一定有用.通常,当您编写具体程序并使用仿函数时,您正在使用一个特定的仿函数,并且您经常将其fmap
视为对特定仿函数所做的任何事情.当我和我一起工作时[Int]
,我常常不把我的[Int]
价值看作是不确定的整数,我只是将它们看作是整数列表,我想到的fmap
就像我想的那样map
.
那么为什么要费心呢?为什么不只是map
对列表,applyToMaybe
用于Maybe
s和applyToIO
为IO
S' 然后每个人都会知道他们做了什么,没有人必须理解奇怪的抽象概念,如仿函数.
关键是认识到那里有很多仿函数; 几乎所有的容器类型开始(因此容器比喻什么函子的).fmap
即使我们没有仿函数,他们每个人都有一个对应的操作.每当你根据fmap
操作(或者map
,或者你为特定类型调用的任何东西)编写算法时,如果你用仿函数而不是你的特定类型来编写它,那么它适用于所有仿函数.
它也可以作为一种文档形式.如果我将我的一个列表值移交给您编写的在列表上运行的函数,它可以执行任何操作.但是,如果我将我的列表移交给你编写的函数,该函数在任意仿函数中对值进行操作,那么我知道函数的实现不能使用列表特征,只能使用函子特征.
回想一下如何在传统的命令式编程中使用functorish的东西可能有助于看到它的好处.像数组,列表,树等容器类型通常会有一些用于迭代它们的模式.虽然库通常提供标准的迭代接口来解决这个问题,但它对于不同的容器可能略有不同.但是每次你想迭代它们时你仍然会写一个for循环,当你想要做的是计算容器中每个项目的结果并收集你通常最终混合在逻辑中的所有结果用于构建新容器.
fmap
是每一个为形式,你永远不会写,整理一劳永逸由库的作家,之前,你甚至坐下来计划的循环.此外,它还可以用于类似的东西Maybe
,(->) r
并且可能不会被视为与命令式语言中设计一致的容器界面有任何关系.
在Haskell中,仿函数捕获了具有"东西"容器的概念,这样您就可以在不改变容器形状的情况下操纵"东西".
Functors提供了一个函数,fmap
通过执行常规函数并将其从一种元素的容器"提升"到另一个元素的另一个函数,可以执行此操作:
fmap :: Functor f => (a -> b) -> (f a -> f b)
Run Code Online (Sandbox Code Playgroud)
例如,[]
列表类型构造函数是一个仿函数:
> fmap show [1, 2, 3]
["1","2","3"]
Run Code Online (Sandbox Code Playgroud)
许多其他Haskell类型构造函数也是如此,如1Maybe
和Map Integer
1:
> fmap (+1) (Just 3)
Just 4
> fmap length (Data.Map.fromList [(1, "hi"), (2, "there")])
fromList [(1,2),(2,5)]
Run Code Online (Sandbox Code Playgroud)
请注意,fmap
是不允许改变容器的"形",所以例如,如果你fmap
的清单,其结果具有相同数量的元素,如果你fmap
一个Just
它不能成为一个Nothing
.在形式上,我们要求fmap id = id
,即如果您fmap
是身份功能,则没有任何变化.
到目前为止,我一直在使用术语"容器",但它确实比这更普遍.例如,IO
也是一个仿函数,在这种情况下我们所谓的"形状"是指fmap
一个IO
动作不应该改变副作用.事实上,任何monad都是functor 2.
在类别理论中,仿函数允许您在不同的类别之间进行转换,但在Haskell中我们只有一个类别,通常称为Hask.因此,Haskell中的所有仿函数都从Hask转换为Hask,因此它们就是我们所说的endofunctors(从类别到其自身的仿函数).
最简单的形式,仿函数有点无聊.只需一次操作就可以做到这一点.但是,一旦你开始添加操作,你可以从普通的仿函数到应用仿函数到monad,事情会很快变得更有趣,但这超出了这个答案的范围.
1但Set
不是,因为它只能存储Ord
类型.函数必须能够包含任何类型.
2由于历史原因,Functor
不是超类Monad
,虽然很多人认为应该是.
让我们看看类型。
Prelude> :i Functor
class Functor f where fmap :: (a -> b) -> f a -> f b
Run Code Online (Sandbox Code Playgroud)
但是,这是什么意思?
首先,f
这里是一个类型变量,它代表类型构造函数:f a
是一个类型;a
是代表某种类型的类型变量。
其次,给定一个函数g :: a -> b
,您将得到fmap g :: f a -> f b
。Iefmap g
是一个函数,将 type 的事物转换f a
为 type 的事物f b
。a
请注意,我们在这里也无法获取类型的内容b
。该函数g :: a -> b
以某种方式处理 type 的事物f a
并将它们转换为 type 的事物f b
。
请注意,这f
是相同的。仅其他类型发生变化。
这意味着什么?它可能意味着很多事情。f
通常被视为东西的“容器”。然后,fmap g
能够g
对这些容器的内部进行操作,而无需将其打开。结果仍然被封闭在“内部”,类型类Functor
不为我们提供打开它们或窥视内部的能力。我们得到的只是不透明事物内部的一些转变。任何其他功能都必须来自其他地方。
另请注意,它并没有说这些“容器”只携带一种类型的“东西” a
;它的“内部”可以有许多单独的“事物”,但都是相同类型的a
。
最后,函子的任何候选者都必须遵守函子法则:
fmap id === id
fmap (h . g) === fmap h . fmap g
Run Code Online (Sandbox Code Playgroud)
请注意,两个(.)
运算符的类型不同:
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
Run Code Online (Sandbox Code Playgroud)
这意味着,无论通过连接函数 和 的连线,在 和 类型a
之间存在b
什么关系,通过连接函数和的连线,也存在于和类型之间。c
g
h
f a
f b
f c
fmap g
fmap h
或者,任何可以在世界上“左侧”绘制的连通图,都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
归档时间: |
|
查看次数: |
12134 次 |
最近记录: |