【问题标题】:What is "lifting" in Haskell?Haskell 中的“提升”是什么?
【发布时间】:2011-01-24 15:21:03
【问题描述】:

我不明白什么是“举重”。我应该先了解单子,然后再了解“电梯”是什么? (我也完全不了解 monad :) 或者有人可以用简单的话向我解释一下吗?

【问题讨论】:

标签: haskell functional-programming


【解决方案1】:

起重与其说是一个数学概念,不如说是一种设计模式(尽管我希望这里有人现在会通过展示起重是一个类别或其他东西来反驳我)。

通常你有一些带有参数的数据类型。类似的东西

data Foo a = Foo { ...stuff here ...}

假设您发现Foo 的很多用法都采用数字类型(IntDouble 等),并且您必须不断编写代码来解开这些数字,将它们相加或相乘,然后再将它们包装回去向上。您可以通过编写一次 unwrap-and-wrap 代码来缩短此过程。这个函数传统上被称为“电梯”,因为它看起来像这样:

liftFoo2 :: (a -> b -> c) -> Foo a -> Foo b -> Foo c

换句话说,您有一个函数,它接受两个参数的函数(例如 (+) 运算符)并将其转换为 Foos 的等效函数。

所以现在你可以写了

addFoo = liftFoo2 (+)

编辑:更多信息

你当然可以有liftFoo3liftFoo4等等。然而,这通常不是必需的。

从观察开始

liftFoo1 :: (a -> b) -> Foo a -> Foo b

但这与fmap 完全相同。所以你会写而不是liftFoo1

instance Functor Foo where
   fmap f foo = ...

如果你真的想要完全规律,那么你可以说

liftFoo1 = fmap

如果你可以把Foo 变成一个函子,也许你可以把它变成一个应用函子。事实上,如果你可以写成liftFoo2,那么应用实例是这样的:

import Control.Applicative

instance Applicative Foo where
   pure x = Foo $ ...   -- Wrap 'x' inside a Foo.
   (<*>) = liftFoo2 ($)

Foo 的(&lt;*&gt;) 运算符具有类型

(<*>) :: Foo (a -> b) -> Foo a -> Foo b

它将包装的函数应用于包装的值。因此,如果您可以实现liftFoo2,那么您可以根据它来编写它。或者你可以直接实现它,不用liftFoo2,因为Control.Applicative模块包括

liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c

同样有liftAliftA3。但是你实际上并不经常使用它们,因为还有另一个运算符

(<$>) = fmap

这可以让你写:

result = myFunction <$> arg1 <*> arg2 <*> arg3 <*> arg4

术语 myFunction &lt;$&gt; arg1 返回一个包装在 Foo 中的新函数:

ghci> :type myFunction
a -> b -> c -> d

ghci> :type myFunction <$> Foo 3
Foo (b -> c -> d)

这又可以使用(&lt;*&gt;) 应用于下一个参数,依此类推。因此,现在不是为每个 arity 提供一个提升函数,而是一个菊花链的应用程序,如下所示:

ghci> :type myFunction <$> Foo 3 <*> Foo 4
Foo (c -> d)

ghci: :type myFunction <$> Foo 3 <*> Foo 4 <*> Foo 5
Foo d

【讨论】:

  • 可能值得提醒的是,电梯应遵守标准法律lift id == idlift (f . g) == (lift f) . (lift g)
  • 电梯确实是“一个类别或某事”。 Carlos 刚刚列出了函子定律,其中id. 分别是某个类别的恒等箭头和箭头组合。通常在谈到 Haskell 时,所讨论的类别是“Hask”,其箭头是 Haskell 函数(换句话说,id. 指的是你知道和喜爱的 Haskell 函数)。
  • 这应该是instance Functor Foo,而不是instance Foo Functor,对吧?我会自己编辑,但我不是 100% 确定。
  • 没有 Applicative 的提升是 = Functor。我的意思是你有 2 个选择:Functor 或 Applicative Functor。第一个提升单参数功能,第二个多参数功能。差不多就是这样。正确的?这不是火箭科学:) 听起来就是这样。顺便说一句,感谢您的出色回答!
  • @atc:这是部分应用程序。见wiki.haskell.org/Partial_application
【解决方案2】:

Paul 和 yairchu 都是很好的解释。

我想补充一点,被提升的函数可以有任意数量的参数,并且它们不必是相同的类型。例如,您还可以定义一个 liftFoo1:

liftFoo1 :: (a -> b) -> Foo a -> Foo b

一般情况下,取1个参数的函数的提升被捕获在类型类Functor中,提升操作称为fmap

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

注意与liftFoo1 类型的相似之处。事实上,如果你有liftFoo1,你可以让Foo成为Functor的一个实例:

instance Functor Foo where
  fmap = liftFoo1

此外,将提升到任意数量的参数的泛化称为应用风格。在您掌握了具有固定数量参数的函数的提升之前,请不要费心深入研究。但是当你这样做时,Learn you a Haskell 有一个很好的章节。 Typeclassopedia 是另一个很好的文档,描述了 FunctorApplicative(以及其他类型类;向下滚动到该文档的右侧章节)。

希望这会有所帮助!

【讨论】:

    【解决方案3】:

    让我们从一个例子开始(为了更清晰的展示,添加了一些空白):

    > import Control.Applicative
    > replicate 3 'a'
    "aaa"
    > :t replicate
    replicate        ::         Int -> b -> [b]
    > :t liftA2
    liftA2 :: (Applicative f) => (a -> b -> c) -> (f a -> f b -> f c)
    > :t liftA2 replicate
    liftA2 replicate :: (Applicative f) =>       f Int -> f b -> f [b]
    > (liftA2 replicate) [1,2,3] ['a','b','c']
    ["a","b","c","aa","bb","cc","aaa","bbb","ccc"]
    > ['a','b','c']
    "abc"
    

    liftA2 将普通类型的函数转换为包裹在Applicative 中的相同类型的函数,例如列表、IO 等。

    另一个常见的电梯是 lift 来自 Control.Monad.Trans。它将一个单子的单子动作转换为一个已转换单子的动作。

    一般来说,“提升”提升一个函数/动作为“包装”类型(因此原始函数可以“隐藏”工作)。

    理解这个和 monad 等以及理解它们为什么有用的最好方法可能是编码和使用它。如果您之前编写的代码中有任何您怀疑可以从中受益的东西(即这将使代码更短等),只需尝试一下,您就会轻松掌握这个概念。

    【讨论】:

      【解决方案4】:

      我想写一个答案,因为我想从不同的角度来写它。

      假设您有一个函子,例如Just 4,并且您想在该函子上应用一个函数,例如(*2)。所以你尝试这样的事情:

      main = print $ (*2) (Just 4) 
      

      你会得到一个错误:

      No instance for (Num (Maybe a0)) arising from an operator section
          • In the expression: * 2
      

      好的,失败了。要使(*2)Just 4 一起工作,您可以在fmap 的帮助下提升它。

      fmap的类型签名:

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

      链接: https://hackage.haskell.org/package/base-4.16.0.0/docs/Prelude.html#v:fmap

      fmap 函数接受一个 a -&gt; b 函数和一个仿函数。它将a -&gt; b 函数应用于函子值以产生f b。 换句话说,a -&gt; b 函数正在与仿函数兼容。转换函数以使其兼容的行为是提升

      在 o.o.p.术语这称为适配器

      所以fmap是一个提升函数。

      main = print $ fmap (*2) (Just 4) 
      

      这会产生:

      Just 8
      

      【讨论】:

        【解决方案5】:

        根据this shiny tutorial,函子是一些容器(如Maybe&lt;a&gt;List&lt;a&gt;Tree&lt;a&gt;,可以存储其他类型的元素,a)。我对元素类型a 使用了Java 泛型符号&lt;a&gt;,并将元素视为树上的浆果Tree&lt;a&gt;。有一个函数fmap,它接受一个元素转换函数,a-&gt;b和容器functor&lt;a&gt;。它将a-&gt;b 应用于容器的每个元素,有效地将其转换为functor&lt;b&gt;。当只提供第一个参数时,a-&gt;bfmap 等待 functor&lt;a&gt;。也就是说,单独提供 a-&gt;b 会将这个元素级函数转换为在容器上运行的函数 functor&lt;a&gt; -&gt; functor&lt;b&gt;。这称为函数的提升。因为容器也被称为 a functor,所以 Functors 而不是 Monads 是提升的先决条件。单子与提升有点“平行”。两者都依赖于 Functor 概念并执行f&lt;a&gt; -&gt; f&lt;b&gt;。不同之处在于lifting使用a-&gt;b进行转换,而Monad需要用户定义a -&gt; f&lt;b&gt;

        【讨论】:

        • 我给了你一个分数,因为“函子是一些容器”是巨魔风味的火焰诱饵。示例:从一些r 到一个类型的函数(让我们使用c 来表示多样性)是函子。它们不“包含”任何c。在这种情况下,fmap 是函数组合,采用 a -&gt; b 函数和 r -&gt; a 一个,为您提供一个新的 r -&gt; b 函数。仍然没有容器。另外,如果可以的话,我会在最后一句话中再次标记它。
        • 另外,fmap 是一个函数,不会“等待”任何东西;作为 Functor 的“容器”是提升的全部意义。此外,如果有的话,Monad 是提升的双重概念:Monad 让您可以使用已经被提升了一些正数的东西,就好像它只被提升了一次 - 这更好地称为 flattening i>.
        • @BMeph To wait, to expect, to anticipate 是同义词。我所说的“功能等待”是指“功能预期”。
        • @BMeph 我想说的是,与其将函数视为函子是容器的想法的反例,不如将函数的理智函子实例视为函数不是容器的想法的反例容器。函数是从域到余域的映射,域是所有参数的叉积,余域是函数的输出类型。同样,列表是从 Naturals 到列表内部类型(域 -> 共域)的映射。如果您记住函数或不保留列表,它们会变得更加相似。
        • @BMeph 列表被认为更像容器的唯一原因之一是在许多语言中它们可以被变异,而传统上的函数不能。但在 Haskell 中,即使这也不是一个公平的说法,因为两者都不能被变异,并且两者都可以被复制变异:b = 5 : af 0 = 55f n = g n,都涉及对“容器”进行伪变异。此外,列表通常完全存储在内存中,而函数通常存储为计算。但是,在调用之间不存储的记忆/单一列表都打破了这个想法。
        猜你喜欢
        • 1970-01-01
        • 2013-07-31
        • 2014-07-06
        • 2011-03-23
        • 1970-01-01
        • 2015-11-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多