什么是Haskell中的“lifting”?

155

我不明白"lifting"是什么意思。在理解"lift"之前,我应该先了解单子吗?(其实我对单子也一无所知 :) 或者有人能用简单的语言解释一下吗?


10
也许有用,也许没有:http://www.haskell.org/haskellwiki/Lifting - kennytm
5个回答

200

提取操作更像是一种设计模式而不是数学概念(尽管我预计这里会有人通过展示如何将提取定义为类别或其他内容来驳斥我的说法)。

通常,您会使用带参数的数据类型。就像:

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

假设你发现许多使用Foo的地方都使用数字类型(如IntDouble等),而你不断编写解包这些数字、将它们相加或相乘,然后重新打包的代码。你可以通过编写一次解包和打包的代码来避免这种情况。这个函数传统上被称为“lift”,因为它看起来像这样:

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

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

现在你可以写:

addFoo = liftFoo2 (+)

编辑:更多信息

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

从观察开始:

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

但那正好和 fmap 一模一样。因此,你可以使用 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中的类型为

(<*>) :: 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 <$> arg1这个术语返回一个包装在Foo中的新函数:

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

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

接下来可以使用(<*>)将这个函数应用到下一个参数,以此类推。因此,现在你不需要为每个元数编写一个lift函数,而只需像这样拥有一个应用链:

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

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

29
值得提醒的是,电梯应该遵守标准定律 lift id == idlift (f . g) == (lift f) . (lift g)。请注意,本翻译尽可能保持忠实于原意,使语言更易理解,但未进行任何解释或添加额外内容。 - Carlos Scheidegger
15
升降机确实是“一种范畴或某种东西”。卡洛斯刚刚列出了函子定律,其中 id. 分别表示某个范畴的恒等箭头和箭头组合。通常在谈论 Haskell 时,所涉及的范畴是“Hask”,其箭头是 Haskell 函数(换句话说,id. 指的是你所熟悉和喜爱的 Haskell 函数)。 - Dan Burton
3
这应该写成 instance Functor Foo,而不是 instance Foo Functor,对吧?我想自己编辑,但我不是100%确定。 - amalloy
2
没有 Applicative 的提升等同于 Functor。我的意思是你有两个选择:Functor 或者 Applicative Functor。第一个可以提升单参数函数,第二个可以提升多参数函数。基本上就是这样了。对吧?这并不是什么高深的科学 :) 只是听起来像而已。顺便说一下,感谢你的好回答! - jhegedus
2
@atc:这是部分应用。请参阅https://wiki.haskell.org/Partial_application。 - Paul Johnson
显示剩余10条评论

42

Paul和yairchu都给出了不错的解释。

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

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

一般来说,接收一个参数的函数的提升被捕获在类型类Functor中,提升操作被称为fmap

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

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

instance Functor Foo where
  fmap = liftFoo1
此外,将lifting推广到任意数量的参数被称为applicative style。在掌握了使用固定数量的参数提升函数之后再深入研究此概念。但是一旦您掌握了这个概念,Learn you a Haskell中有一章关于此主题的良好介绍。另一个很好的文档是Typeclassopedia,其中描述了FunctorApplicative(以及其他类型类;请在该文档中滚动到正确的章节)。希望这可以帮助您!

26

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

> 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中的lift。它将一个单一Monad动作转换为一个转换后Monad的动作。

一般来说,“lift”将一个函数/动作提升到“包装”类型中(因此原始函数可以在“包装”下工作)。

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


2

我希望提供一个不同的视角来回答这个问题。

假设你有一个函数对象,比如 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 -> b 函数和一个 functor。它将 a -> b 函数应用于 functor 值,以生成 f b。 换句话说,a -> b 函数被使与 functor 兼容。将函数转换为与其兼容的操作称为lifting

在面向对象编程中,这被称为adapter

因此,fmap 是一个 lifting 函数。

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

这将产生以下结果:
Just 8

0
根据这个耀眼的教程,一个functor是一些容器(像Maybe<a>, List<a> 或者 Tree<a>)可以存储另一种类型a 的元素。我使用了Java泛型符号<a>来表示元素类型a,并将这些元素看作是树Tree<a>上的浆果。有一个函数fmap,它需要一个元素转换函数a->b和一个容器functor<a>。它将a->b应用于容器的每个元素,有效地将其转换为functor<b>。当只提供第一个参数a->b时,fmap等待functor<a>。也就是说,仅提供a->b就将这个元素级别的函数转换成了在容器上操作的函数functor<a> -> functor<b>。这被称为函数的提升。由于容器也被称为functor,因此Functors而不是Monads是提升的前提条件。Monads与提升类似。两者都依赖于Functor的概念,并执行f<a> -> f<b>。不同之处在于提升使用a->b进行转换,而Monad则要求用户定义a -> f<b>

5
我给你打了个分数低的评价,因为“一个函子是一些容器”听起来像是故意挑衅的言论。例如:从某个类型r到另一个类型c的函数(我们使用c以示差异)是函子。它们并不“包含”任何c。在这种情况下,fmap是函数组合,它接受一个a -> b函数和一个r -> a函数,返回一个新的r -> b函数。仍然没有容器。此外,如果可以的话,我会再次给最后一句话打分数低的评价。 - BMeph
1
此外,fmap 是一个函数,不需要“等待”任何东西;作为 Functor 的“容器”是提升的整个重点。另外,如果说有什么对应于提升的概念,那就是 Monad:Monad 允许您使用已经被提升了一定次数的某些内容,就好像它只被提升了一次 - 这更为人所知的是 _扁平化_。 - BMeph
2
@BMeph “等待”,“期望”,“预期”是同义词。当我说“函数等待”时,我的意思是“函数预期”。 - Val
2
@BMeph 我认为,与其将函数视为反例,证明函子是容器的想法不成立,不如将函数的合理函子实例视为反例,证明函数也可以是容器的想法不成立。函数是从域到余域的映射,其中域是所有参数的乘积,余域是函数的输出类型。同样,列表是从自然数到列表的内部类型的映射(域->余域)。如果你记忆函数或者不保留列表,它们变得更加相似。 - semicolon
2
@BMeph 列表被认为更像一个容器的原因之一是,在许多语言中,它们可以被改变,而传统上函数则不能。但在 Haskell 中,这也不是一个公平的说法,因为两者都不能被改变,并且都可以进行复制-突变:b = 5 : af 0 = 55 f n = g n,都涉及到伪突变“容器”。此外,列表通常完全存储在内存中,而函数通常存储为计算。但是,记忆化/单态列表并不会在调用之间存储,这两种情况都打破了这个想法。 - semicolon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接