在哈斯克尔,什么是“举重”?

我不明白什么是“举重”。在理解什么是“升力”之前,我应该先理解单子吗?(我对单子也完全一无所知)或者有人能用简单的语言给我解释一下吗?

34799 次浏览

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

> 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等。

另一个常见的电梯是 liftControl.Monad.Trans。它将一个单子的单子动作转换为一个单子的单子动作。

一般来说,将 电梯函数/操作“提升”为“包装”类型(这样原始函数就可以“在包装下”工作)。

理解它和单子等的最好方法,以及理解它们为什么有用,可能是编码和使用它。如果您之前编写的代码中有任何您认为可以从中受益的内容(例如,这将使代码更短,等等) ,只要尝试一下,您就会很容易地理解这个概念。

举重更多的是一种设计模式,而不是一个数学概念(尽管我希望这里的某些人现在会通过展示举重是一个类别或什么来反驳我)。

通常您有一些带有参数的数据类型

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

假设您发现 Foo的许多用法都采用数字类型(IntDouble等) ,并且您必须不断编写代码来拆开这些数字,将它们相加或相乘,然后再将它们重新包装起来。您可以通过编写一次取消包装代码来缩短这个过程。这个函数传统上被称为“提升”,因为它看起来像这样:

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 的 (<*>)运算符具有

(<*>) :: 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)

这反过来又可以使用 (<*>)应用到下一个参数,依此类推。所以现在不是每个单位都有一个提升函数,而是一串应用程序,像这样:

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


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

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

此外,提升到任意数目的参数的推广称为 实用主义风格。在理解使用固定数量的参数提升函数之前,不必费心深入研究这个问题。但是当你这样做,给你学个 Haskell有一个很好的章节对此。典型的经典百科全书是另一个描述 函数适用(以及其他类型类; 向下滚动到该文档的右侧章节)的好文档。

希望这个能帮上忙!

根据 Tree<a>0,函数是一些容器(如 Maybe<a>List<a>Tree<a>,它们可以存储另一种类型 a的元素)。对于元素类型 a,我使用了 Java 泛型表示法 <a>,并将元素视为树 Tree<a>上的浆果。有一个函数 fmap,它接受一个元素转换函数 a->b和容器 functor<a>。它将 a->b应用于容器的每个元素,有效地将其转换为 List<a>1。当只提供第一个参数时,a->bfmap等待 functor<a>。也就是说,单独提供 a->b就可以将这个元素级函数转换为在容器上运行的函数 List<a>6。这称为函数的 Tree<a>1。因为容器也被称为 Tree<a>2,函子而不是单子是提升的先决条件。单子和举重是“平行”的。两者都依赖函数概念并执行 List<a>7。区别在于,提升使用 a->b进行转换,而 Monad 要求用户定义 List<a>9。

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

假设您有一个函数,例如 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