我不明白"lifting"是什么意思。在理解"lift"之前,我应该先了解单子吗?(其实我对单子也一无所知 :) 或者有人能用简单的语言解释一下吗?
我不明白"lifting"是什么意思。在理解"lift"之前,我应该先了解单子吗?(其实我对单子也一无所知 :) 或者有人能用简单的语言解释一下吗?
提取操作更像是一种设计模式而不是数学概念(尽管我预计这里会有人通过展示如何将提取定义为类别或其他内容来驳斥我的说法)。
通常,您会使用带参数的数据类型。就像:
data Foo a = Foo { ...stuff here ...}
假设你发现许多使用Foo
的地方都使用数字类型(如Int
,Double
等),而你不断编写解包这些数字、将它们相加或相乘,然后重新打包的代码。你可以通过编写一次解包和打包的代码来避免这种情况。这个函数传统上被称为“lift”,因为它看起来像这样:
liftFoo2 :: (a -> b -> c) -> Foo a -> Foo b -> Foo c
换句话说,你有一个函数,它接受一个二元函数(例如(+)
运算符)并将其转换为Foos的等效函数。
现在你可以写:
addFoo = liftFoo2 (+)
编辑:更多信息
当然,您可以有liftFoo3
,liftFoo4
等等。 然而,这通常是不必要的。
从观察开始:
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
同样地,还有liftA
和liftA3
。但实际上你不是经常使用它们,因为还有另一个操作符。
(<$>) = 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
lift id == id
和 lift (f . g) == (lift f) . (lift g)
。请注意,本翻译尽可能保持忠实于原意,使语言更易理解,但未进行任何解释或添加额外内容。 - Carlos Scheideggerid
和 .
分别表示某个范畴的恒等箭头和箭头组合。通常在谈论 Haskell 时,所涉及的范畴是“Hask”,其箭头是 Haskell 函数(换句话说,id
和 .
指的是你所熟悉和喜爱的 Haskell 函数)。 - Dan Burtoninstance Functor Foo
,而不是 instance Foo Functor
,对吧?我想自己编辑,但我不是100%确定。 - amalloyPaul和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,其中描述了Functor和Applicative(以及其他类型类;请在该文档中滚动到正确的章节)。希望这可以帮助您!让我们从一个例子开始(为了更清晰的展示,添加了一些空格):
> 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”将一个函数/动作提升到“包装”类型中(因此原始函数可以在“包装”下工作)。
最好的理解方法,以及单子等,以及了解它们为什么有用,可能是编写和使用它。如果您之前编写过任何代码,您认为可以从中受益(即这将使该代码更短等),只需尝试一下,您就可以轻松掌握概念。
我希望提供一个不同的视角来回答这个问题。
假设你有一个函数对象,比如 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
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>
。r
到另一个类型c
的函数(我们使用c
以示差异)是函子。它们并不“包含”任何c
。在这种情况下,fmap是函数组合,它接受一个a -> b
函数和一个r -> a
函数,返回一个新的r -> b
函数。仍然没有容器。此外,如果可以的话,我会再次给最后一句话打分数低的评价。 - BMephfmap
是一个函数,不需要“等待”任何东西;作为 Functor 的“容器”是提升的整个重点。另外,如果说有什么对应于提升的概念,那就是 Monad:Monad 允许您使用已经被提升了一定次数的某些内容,就好像它只被提升了一次 - 这更为人所知的是 _扁平化_。 - BMephb = 5 : a
和 f 0 = 55
f n = g n
,都涉及到伪突变“容器”。此外,列表通常完全存储在内存中,而函数通常存储为计算。但是,记忆化/单态列表并不会在调用之间存储,这两种情况都打破了这个想法。 - semicolon