【发布时间】:2011-01-24 15:21:03
【问题描述】:
我不明白什么是“举重”。我应该先了解单子,然后再了解“电梯”是什么? (我也完全不了解 monad :) 或者有人可以用简单的话向我解释一下吗?
【问题讨论】:
-
也许有用,也许没用:haskell.org/haskellwiki/Lifting
标签: haskell functional-programming
我不明白什么是“举重”。我应该先了解单子,然后再了解“电梯”是什么? (我也完全不了解 monad :) 或者有人可以用简单的话向我解释一下吗?
【问题讨论】:
标签: haskell functional-programming
起重与其说是一个数学概念,不如说是一种设计模式(尽管我希望这里有人现在会通过展示起重是一个类别或其他东西来反驳我)。
通常你有一些带有参数的数据类型。类似的东西
data Foo a = Foo { ...stuff here ...}
假设您发现Foo 的很多用法都采用数字类型(Int、Double 等),并且您必须不断编写代码来解开这些数字,将它们相加或相乘,然后再将它们包装回去向上。您可以通过编写一次 unwrap-and-wrap 代码来缩短此过程。这个函数传统上被称为“电梯”,因为它看起来像这样:
liftFoo2 :: (a -> b -> c) -> Foo a -> Foo b -> Foo c
换句话说,您有一个函数,它接受两个参数的函数(例如 (+) 运算符)并将其转换为 Foos 的等效函数。
所以现在你可以写了
addFoo = liftFoo2 (+)
编辑:更多信息
你当然可以有liftFoo3、liftFoo4等等。然而,这通常不是必需的。
从观察开始
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 的(<*>) 运算符具有类型
(<*>) :: 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)
这又可以使用(<*>) 应用于下一个参数,依此类推。因此,现在不是为每个 arity 提供一个提升函数,而是一个菊花链的应用程序,如下所示:
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)。
id 和. 分别是某个类别的恒等箭头和箭头组合。通常在谈到 Haskell 时,所讨论的类别是“Hask”,其箭头是 Haskell 函数(换句话说,id 和 . 指的是你知道和喜爱的 Haskell 函数)。
instance Functor Foo,而不是instance Foo Functor,对吧?我会自己编辑,但我不是 100% 确定。
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 是另一个很好的文档,描述了 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。它将一个单子的单子动作转换为一个已转换单子的动作。
一般来说,“提升”提升一个函数/动作为“包装”类型(因此原始函数可以“隐藏”工作)。
理解这个和 monad 等以及理解它们为什么有用的最好方法可能是编码和使用它。如果您之前编写的代码中有任何您怀疑可以从中受益的东西(即这将使代码更短等),只需尝试一下,您就会轻松掌握这个概念。
【讨论】:
我想写一个答案,因为我想从不同的角度来写它。
假设您有一个函子,例如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 函数和一个仿函数。它将a -> b 函数应用于函子值以产生f b。
换句话说,a -> b 函数正在与仿函数兼容。转换函数以使其兼容的行为是提升。
在 o.o.p.术语这称为适配器。
所以fmap是一个提升函数。
main = print $ fmap (*2) (Just 4)
这会产生:
Just 8
【讨论】:
根据this shiny tutorial,函子是一些容器(如Maybe<a>、List<a> 或Tree<a>,可以存储其他类型的元素,a)。我对元素类型a 使用了Java 泛型符号<a>,并将元素视为树上的浆果Tree<a>。有一个函数fmap,它接受一个元素转换函数,a->b和容器functor<a>。它将a->b 应用于容器的每个元素,有效地将其转换为functor<b>。当只提供第一个参数时,a->b、fmap 等待 functor<a>。也就是说,单独提供 a->b 会将这个元素级函数转换为在容器上运行的函数 functor<a> -> functor<b>。这称为函数的提升。因为容器也被称为 a functor,所以 Functors 而不是 Monads 是提升的先决条件。单子与提升有点“平行”。两者都依赖于 Functor 概念并执行f<a> -> f<b>。不同之处在于lifting使用a->b进行转换,而Monad需要用户定义a -> f<b>。
【讨论】:
r 到一个类型的函数(让我们使用c 来表示多样性)是函子。它们不“包含”任何c。在这种情况下,fmap 是函数组合,采用 a -> b 函数和 r -> a 一个,为您提供一个新的 r -> b 函数。仍然没有容器。另外,如果可以的话,我会在最后一句话中再次标记它。
fmap 是一个函数,不会“等待”任何东西;作为 Functor 的“容器”是提升的全部意义。此外,如果有的话,Monad 是提升的双重概念:Monad 让您可以使用已经被提升了一些正数的东西,就好像它只被提升了一次 - 这更好地称为 flattening i>.
To wait, to expect, to anticipate 是同义词。我所说的“功能等待”是指“功能预期”。
b = 5 : a 和 f 0 = 55f n = g n,都涉及对“容器”进行伪变异。此外,列表通常完全存储在内存中,而函数通常存储为计算。但是,在调用之间不存储的记忆/单一列表都打破了这个想法。