仿函数如何在哈斯克尔工作?

Mat*_*sen 44 haskell functional-programming functor

我正在努力学习Haskell,而且我已经完成了所有的基础知识.但是现在我被卡住了,试图让我的头围绕着仿函数.

我读到"仿函数将一个类别转换为另一个类别".这是什么意思?

我知道这有很多问题,但是有人能给我一个关于仿函数的简单英语解释或者一个简单的用例吗?

And*_*ewC 115

我意外地写了一篇

Haskell Functors教程

我将使用示例回答您的问题,我会将这些类型放在评论中.

注意类型中的模式.

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) 的列表.事实上,对于列表,fmapexaclty做什么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数据类型,它有两种类型的值,NothingJust 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东西把东西变成字符串.秘密地说实际类型showShow 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 bLeft 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]Ints.)

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)

但我们通常更换somethingf的简洁.但是编译器完全相同:

fmap :: Functor f => (a -> b) -> f a -> f b
Run Code Online (Sandbox Code Playgroud)

回顾一下这些类型并检查它总是有用的 - Either String Int仔细一点 - 那是什么f时候?

附录:Functor规则是什么,为什么我们有它们?

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不偷偷摸摸地在幕后做任何事情或改变我们没想到的任何事情.它们不是由编译器强制执行的(要求编译器在编译代码之前证明一个定理是不公平的,并且会减慢编译速度 - 程序员应该检查).这意味着你可以作弊,但这是一个糟糕的计划,因为你的代码会产生意想不到的结果.

  • 非常结构化的解释,所有这些都是在没有使用大量其他术语的情况下完成的,那些不理解Functors的人不会知道('参数化'是罪魁祸首,我会说). (2认同)
  • 这是我目前看到的最好的解释。有没有可能你也意外地为 applicative 和 monad 写了一个教程?我愿意付钱去看他们=D (2认同)

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并且许多模块定义了更多Functors(和其他类型类)的实例.

但是,请不要太认真地对待"容器"这个词.Functors是一个明确定义的概念,但你可以经常用这种模糊的类比来推理它.

理解正在发生的事情的最好方法是简单地阅读每个实例的定义,这应该让你对正在发生的事情有直觉.从那里开始,真正形式化您对概念的理解只是一小步.需要补充的是澄清我们的"容器"究竟是什么,并且每个实例都满足一对简单的法则.

  • 在一个_analogy_上分裂头发没有任何意义,在答案中描述为模糊.所有的类比都破裂了,莎拉正确地指出OP朝着更全面,更基于法律的理解的方向发展.容器是一个良好的开端.制片人是下一步.计算上下文非常通用,但开始时过于抽象.(在我看来,作为一名教师.)无论如何,OP要求简单的英语,让我们不要过于理论化. (15认同)
  • 根据我的经验,我可以说,对于仿函数的容器类比使得它们更难理解(特别是当涉及到"IO"时).因此,在开始时,人们可以将它们视为某种附加到值的"计算",而不是包含值的"容器". (3认同)
  • @WillNess @Anton是对的.容器类比只能用于简单.但是,它没有任何明确的定义.考虑例如`Const a`仿函数:) (2认同)

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;具有rChar (->) Char为类型构造这是一个算符,Int(->) Char IntChar -> Int 在中缀符号).

对于一般仿函数fmap,你唯一可以做的就是这种类型Functor f => (a -> b) -> (f a -> f b).fmap将对正常值进行操作的函数转换为函数,该函数对由函数添加的附加上下文的值进行操作; 这对于每个仿函数到底有什么不同,但你可以用它们来完成.

因此,Maybefunctor 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)普通Ints 上函数的"仿函数版" .它在这个特定的仿函数所具有的任何"上下文"中将整数加1.

就其本身而言,fmap不一定有用.通常,当您编写具体程序并使用仿函数时,您正在使用一个特定的仿函数,并且您经常将其fmap视为对特定仿函数所做的任何事情.当我和我一起工作时[Int],我常常不把我的[Int]价值看作是不确定的整数,我只是将它们看作是整数列表,我想到的fmap就像我想的那样map.

那么为什么要费心呢?为什么不只是map对列表,applyToMaybe用于Maybes和applyToIOIOS' 然后每个人都会知道他们做了什么,没有人必须理解奇怪的抽象概念,如仿函数.

关键是认识到那里有很多仿函数; 几乎所有的容器类型开始(因此容器比喻什么函子).fmap即使我们没有仿函数,他们每个人都有一个对应的操作.每当你根据fmap操作(或者map,或者你为特定类型调用的任何东西)编写算法时,如果你用仿函数而不是你的特定类型来编写它,那么它适用于所有仿函数.

它也可以作为一种文档形式.如果我将我的一个列表值移交给您编写的在列表上运行的函数,它可以执行任何操作.但是,如果我将我的列表移交给你编写的函数,该函数在任意仿函数中对值进行操作,那么我知道函数的实现不能使用列表特征,只能使用函子特征.

回想一下如何在传统的命令式编程中使用functorish的东西可能有助于看到它的好处.像数组,列表,树等容器类型通常会有一些用于迭代它们的模式.虽然库通常提供标准的迭代接口来解决这个问题,但它对于不同的容器可能略有不同.但是每次你想迭代它们时你仍然会写一个for循环,当你想要做的是计算容器中每个项目的结果并收集你通常最终混合在逻辑中的所有结果用于构建新容器.

fmap每一个为形式,你永远不会写,整理一劳永逸由库的作家,之前,你甚至坐下来计划的循环.此外,它还可以用于类似的东西Maybe,(->) r并且可能不会被视为与命令式语言中设计一致的容器界面有任何关系.


ham*_*mar 7

在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类型构造函数也是如此,如1MaybeMap Integer1:

> 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,事情会很快变得更有趣,但这超出了这个答案的范围.

1Set不是,因为它只能存储Ord类型.函数必须能够包含任何类型.
2由于历史原因,Functor不是超类Monad,虽然很多人认为应该是.


Wil*_*ess 5

让我们看看类型。

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 ba请注意,我们在这里也无法获取类型的内容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什么关系,通过连接函数和的连线,也存在于和类型之间。cghf af bf cfmap gfmap h

或者,任何可以在世界上“左侧”绘制的连通图,都a, b, c, ...可以在世界上“右侧”绘制,f a, f b, f c, ...方法是将函数g, h, ...变为函数fmap g, fmap h, ...,并将函数本身也变为函数id :: a -> a。,根据函子定律。fmap idid :: f a -> f a