【问题标题】:How do functors work in haskell?函子如何在 Haskell 中工作?
【发布时间】:2012-10-19 13:06:55
【问题描述】:

我正在尝试学习 Haskell,并且我已经完成了所有基础知识。但现在我被困住了,试图让我的头脑围绕函子。

我读过“函子将一个类别转换为另一个类别”。这是什么意思?

我知道有很多问题要问,但是谁能给我一个简单的英语函子解释或者一个简单的用例

【问题讨论】:

标签: haskell functional-programming functor


【解决方案1】:

我不小心写了一个

Haskell 函子教程

我将使用示例回答您的问题,并将类型放在 cmets 中。

注意类型中的模式。

fmapmap 的泛化

函子是为了给你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 数据类型,它有两种类型的值,NothingJust 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

让我们在一个稍微不同的结构上使用它,EitherEither 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

&gt;&gt; 做一件又一件的事情,但我喜欢这样的原因是因为&gt;&gt;= 接受了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 是列表(写为[])、MaybeEither StringEither IntIO 和大量的东西。如果它以合理的方式工作(有一些规则 - 稍后),我们称它为 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 允许更改值,而不是结构。不允许fmapJust 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 公平、平等地在任何地方应用您的函数,并且没有任何其他更改。这是一个很好、干净、清晰、可靠、可重复使用的东西。

【讨论】:

  • 编辑需要至少 6 处更改,因此请改为评论:fmap (* 10) 输出中有一个错字,其中 10 应该是 20。很好的迷你教程:)
  • @drumfire 谢谢你 - 已修复。 :)
  • 结构非常好的解释,所有这些都没有使用大量其他不了解 Functors 的人不会知道的术语(我想说,“参数化”是罪魁祸首) .
  • 这是迄今为止我看到的最好的解释。有没有可能你也无意中写了一个 applicative 和 monad 的教程?我愿意支付任何费用才能看到他们=D
  • @AndrewC 鞠躬!非常感谢您为提出如此清晰明了的解释所做的努力!
【解决方案2】:

一个模糊的解释是 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

而且专门的版本可能会更清楚。这是列表版本:

fmap :: (a -> b) -> [a] -> [b]

还有 Maybe 版本:

fmap :: (a -> b) -> Maybe a -> Maybe b

您可以通过使用:i Functor 查询GHCI 来获取有关标准Functor 实例的信息,并且许多模块定义了更多Functors(和其他类型类)的实例。

不过,请不要把“容器”这个词看得太重。 Functors 是一个定义明确的概念,但您通常可以通过这个模糊的类比来推断它。

要了解正在发生的事情,最好的办法就是简单地阅读每个实例的定义,这应该会让您对正在发生的事情有一个直觉。从那里开始,您只需迈出一小步就可以真正正式地理解您对这个概念的理解。需要补充的是澄清我们的“容器”到底是什么,并且每个实例都非常满足一对简单的规律。

【讨论】:

  • 根据我的经验,我可以说,函子的容器类比使它们更难理解(尤其是在涉及IO 时)。因此,一开始,可以将它们视为某种附加到值的“计算”,而不是包含值的“容器”。
  • IO 本身并不是纯 Haskell。我发现这个解释更清楚:考虑putStrLn :: String -&gt; IO ()。这个函数接受String 并返回一个IO (),这可以作为一个计算,在这种情况下输出到stdout。这里的容器解释不如计算解释清楚。但这只是我的看法。
  • @WillNess @Anton 是正确的。容器类比只能用于简单。但是,它的定义并不明确。例如考虑Const a函子:)
  • 在答案中描述为模糊的 类比 是没有意义的。所有的类比都被打破了,莎拉正确地将 OP 指向更全面、更基于法律的理解的方向。容器是一个好的开始。生产者是下一步。计算上下文非常通用,但开始时太抽象了。 (在我作为老师的看法。)无论如何,OP要求简单的英语,让我们不要太理论。
  • 在类别理论中,函子通常被可视化为类别之间的箭头。为什么列表构造函数[] 被认为是函子?这是否意味着我可以将[] 可视化为Hask -&gt; Hask 之间的函子箭头?这是否进一步意味着作为函子[] 将诸如a 之类的对象映射到[a]?但是,[] 函子映射态射意味着什么?为什么不将[] 简单地称为 Hask 范畴内的态射?
【解决方案3】:

在头脑中区分函子本身和应用函子的类型中的值是很重要的。函子本身是一个类型构造函数,如MaybeIO 或列表构造函数[]。仿函数中的值是应用了该类型构造函数的类型中的某个特定值。例如Just 3Maybe Int 类型中的一个特定值(该类型是应用于Int 类型的Maybe 函子),putStrLn "Hello World"IO () 类型中的一个特定值,[2, 4, 8, 16, 32][Int] 类型中的一个特定值。

我喜欢将应用函子的类型中的值视为与基本类型中的值“相同”,但具有一些额外的“上下文”。人们经常将容器类比用于函子,这对于很多函子来说非常自然,但是当你不得不说服自己 IO(-&gt;) r 就像一个容器时,它变得更像是一个障碍而不是帮助。

所以如果Int 代表一个整数值,那么Maybe Int 代表一个可能不存在的整数值(“可能不存在”是“上下文”)。 [Int] 表示具有多个可能值的整数值(这与列表函子的“非确定性”解释相同)列表单子)。 IO Int 表示一个整数值,其精确值取决于整个 Universe(或者,它表示可以通过运行外部进程获得的整数值)。 Char -&gt; Int 是任何Char 值的整数值(“以r 作为参数的函数”是任何类型r 的函子;rChar (-&gt;) Char 是类型构造函数这是一个函子,应用于Int 变为(-&gt;) Char IntChar -&gt; Int 中缀表示法)。

general 函子唯一能做的就是fmap,类型为Functor f =&gt; (a -&gt; b) -&gt; (f a -&gt; f b)fmap 将一个对正常值进行操作的函数转换为一个对具有由仿函数添加的附加上下文的值进行操作的函数;这对每个仿函数的具体作用是不同的,但您可以对所有仿函数进行。

因此,Maybe 函子 fmap (+1) 是计算可能不存在整数 1 的函数,该整数比其输入的可能不存在整数高 1。使用列表仿函数fmap (+1) 是计算比其输入非确定整数高1 的非确定整数的函数。使用IO 函子,fmap (+1) 是计算比其输入整数高 1 的整数的函数——其值取决于外部宇宙。使用(-&gt;) Char 函子,fmap (+1) 是一个将 1 加到依赖于 Char 的整数的函数(当我将 Char 提供给返回值时,我得到的值比我得到的值高 1将相同的Char 输入到原始值)。

但一般来说,对于某些未知函子f,应用于f Int 中某个值的fmap (+1) 是普通Ints 上的函数(+1) 的“函子版本”。在这个特定函子所具有的任何“上下文”中,它将整数加 1。

就其本身而言,fmap 不一定有用。通常,当您编写一个具体的程序并使用函子时,您正在使用一个特定的函子,并且您经常将fmap 视为它为该特定函子所做的任何事情。当我使用[Int] 时,我通常不会将我的[Int] 值视为非确定性整数,我只是将它们视为整数列表,并且我想到fmap 的方式与我对@ 的看法相同987654371@.

那么,为什么还要使用函子呢?为什么不把map 用于列表,applyToMaybe 用于Maybes,applyToIO 用于IOs?然后每个人都会知道他们在做什么,而没有人需要理解函子等奇怪的抽象概念。

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

它也可以作为一种文档形式。如果我将我的列表值之一交给您编写的对列表进行操作的函数,它可以做很多事情。但是,如果我将列表交给您编写的对任意函子中的值进行操作的函数,那么我知道您的函数的实现不能使用列表功能,只能使用函子功能。

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

fmap 是您将编写的那种形式的每个 for 循环,由库编写者在您坐下来编程之前一劳永逸地排序。此外,它还可以与 Maybe(-&gt;) r 之类的东西一起使用,这可能与在命令式语言中设计一致的容器接口没有任何关系。

【讨论】:

    【解决方案4】:

    在 Haskell 中,函子捕捉到拥有“东西”容器的概念,这样您就可以在不改变容器形状的情况下操纵这些“东西”。

    Functors 提供了一个函数 fmap,通过获取常规函数并将其从一种元素的容器“提升”到另一个函数,您可以做到这一点:

    fmap :: Functor f => (a -> b) -> (f a -> f b) 
    

    例如[],列表类型构造函数,是一个函子:

    > fmap show [1, 2, 3]
    ["1","2","3"]
    

    还有许多其他 Haskell 类型的构造函数,例如 MaybeMap Integer1

    > fmap (+1) (Just 3)
    Just 4
    > fmap length (Data.Map.fromList [(1, "hi"), (2, "there")])
    fromList [(1,2),(2,5)]
    

    注意fmap不允许改变容器的“形状”,所以如果你fmap一个列表,结果有相同数量的元素,如果你fmap一个@987654331 @它不能变成Nothing。在形式上,我们要求fmap id = id,即如果您fmap 是身份函数,则没有任何变化。

    到目前为止,我一直在使用“容器”一词,但实际上它比这更笼统。例如,IO 也是一个函子,在这种情况下,我们所说的“形状”是指 fmapIO 动作上不应该改变副作用。事实上,任何 monad 都是 functor2

    在范畴论中,仿函数允许你在不同的范畴之间进行转换,但在 Haskell 中我们实际上只有一个范畴,通常称为 Hask。因此,Haskell 中的所有函子都从 Hask 转换为 Hask,所以它们就是我们所说的 endofunctors(从一个类别到自身的函子)。

    以最简单的形式,函子有点无聊。只需一项操作,您就可以做很多事情。但是,一旦开始添加操作,您就可以从常规函子到应用函子再到 monad,事情很快就会变得更有趣,但这超出了本答案的范围。

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

    【讨论】:

      【解决方案5】:

      让我们看看类型。

      Prelude> :i Functor
      class Functor f where fmap :: (a -> b) -> f a -> f b
      

      但这意味着什么?

      首先f在这里是一个类型变量,它代表一个类型构造函数:f a是一个类型; a 是代表某种类型的类型变量。

      其次,给定一个函数g :: a -&gt; b,你会得到fmap g :: f a -&gt; f b。 IE。 fmap g 是一个函数,将 f a 类型的事物转换为 f b 类型的事物。请注意,我们不能在这里得到ab 类型的东西。函数g :: a -&gt; b 以某种方式处理f a 类型的事物并将它们转换为f b 类型的事物。

      注意f 是一样的。只有另一种类型发生变化。

      这是什么意思?这可能意味着很多事情。 f 通常被视为东西的“容器”。然后,fmap g 使g 能够作用于这些容器的内部,而不会将它们打开。结果仍然包含在“内部”中,类型类Functor 没有为我们提供打开它们或窥视内部的能力。我们得到的只是不透明事物内部的一些转变。任何其他功能都必须来自其他地方。

      还请注意,并不是说这些“容器”只携带一个a 类型的“东西”;它可以有许多单独的“事物”“内部”,但都是相同类型的a

      最后,函子的任何候选者都必须服从the Functor laws

      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
      

      这意味着 abc 类型之间存在任何关系,通过连接线可以说像 g 这样的函数 em> 和 h,也存在于 f af bf c 类型之间,通过连接 fmap g 函数的电线 fmap h

      或者,在a, b, c, ... 世界中,可以在“左侧”绘制任何连接图,在f a, f b, f c, ... 世界中,可以通过将函数g, h, ... 更改为“在右侧”绘制函数fmap g, fmap h, ...,并将函数id :: a -&gt; a 更改为fmap id,根据函子定律,它们本身也只是id :: f a -&gt; f a

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-03-08
        • 1970-01-01
        • 1970-01-01
        • 2017-11-27
        • 2019-11-05
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多