【问题标题】:How to map over Applicative form?如何映射申请表?
【发布时间】:2011-05-17 13:51:35
【问题描述】:

我想在 Applicative 表单上映射。

类地图函数的类型如下:

mapX :: (Applicative f) => (f a -> f b) -> f [a] -> f [b]

用作:

result :: (Applicative f) => f [b]
result = mapX f xs
  where f  :: f a -> f b
        f = ...
        xs :: f[a]
        xs = ...

作为这篇文章的背景,我尝试参考 Paul Haduk 的“The Haskell School of Expression”用 Applicative 风格编写流体模拟程序,我想用 Applicative 风格表达模拟如下:

x, v, a :: Sim VArray
x = x0 +: integral (v * dt)
v = v0 +: integral (a * dt)
a = (...calculate acceleration with x v...)

instance Applicative Sim where
  ...

其中 Sim 类型表示模拟计算的过程,VArray 表示向量数组 (x,y,z)。 X, v a 分别是位置、速度和加速度的数组。

在定义 a 时会出现在 Applicative 形式上的映射。


我找到了我的问题的一个答案。

毕竟,我的问题是“如何提升高阶函数(如 map :: (a -> b) -> [a] -> [b]) 到 Applicative 世界?​​”和答案 我发现是“使用提升的一阶函数构建它们。”

例如,“mapX”是用提升的一阶函数定义的 (headA, tailA, consA, nullA, condA) 如下:

mapX :: (f a -> f b) -> f [a] -> f [b]
mapX f xs0 = condA (nullA xs0) (pure []) (consA (f x) (mapA f xs))
 where
   x = headA xs0
   xs = tailA xs0

headA = liftA head

tailA = liftA tail

consA = liftA2 (:)

nullA = liftA null

condA b t e = liftA3 aux b t e
  where aux b t e = if b then t else e

【问题讨论】:

  • 您的第一个代码块中不应该有f :: f a -> f b(不是f :: (f a -> f b) -> f [a] -> f [b])吗?还是我误会了什么?
  • 谢谢!我错了 f 的类型。
  • 顺便问一下,你能勾勒出你想象的a 的样子吗?我认为您可以通过不需要mapX 的方式来做到这一点。
  • a 的草图如下(不准确): a = liftA sum $ mapX aux $ liftA2 neighbors (x!i) nbr where aux :: f Int -> f Vector3 aux i = 。 ..“liftA2 邻居 (x!i) nbr”的类型是“f [Int]”
  • 您可以通过编辑将代码添加到您的帖子中吗?并在您使用时填写辅助功能?谢谢。

标签: haskell applicative


【解决方案1】:

首先,我认为您提出的类型签名没有多大意义。给定一个应用列表f [a],没有通用的方法可以将其转换为[f a]——因此不需要f a -> f b 类型的函数。为了理智起见,我们将该函数简化为 a -> f b(将其转换为另一个函数是微不足道的,但前提是 f 是一个单子)。

所以现在我们想要:

mapX :: (Applicative f) => (a -> f b) -> f [a] -> f [b]

现在立即想到的是traverse,它是 mapM 的概括。 Traverse,专门用于列表:

traverse :: (Applicative f) => (a -> f b) -> [a] -> f [b]

关闭,但没有雪茄。同样,我们可以将遍历提升到所需的类型签名,但这需要一个 monad 约束:mapX f xs = xs >>= traverse f

如果您不介意 monad 约束,这很好(实际上您可以使用 mapM 更直接地做到这一点)。如果您需要将自己限制为应用程序,那么这应该足以说明为什么您提议的签名实际上是不可能的。

编辑:根据进一步的信息,这是我开始解决潜在问题的方法。

-- your sketch
a = liftA sum $ mapX aux $ liftA2 neighbors (x!i) nbr 
   where aux :: f Int -> f Vector3
   -- the type of "liftA2 neighbors (x!i) nbr" is "f [Int]

-- my interpretation
a = liftA2 aux x v
    where
       aux :: VArray -> VArray -> VArray 
       aux xi vi = ...

如果你不能像这样写 aux —— 作为一个从某个时间点的位置和速度到加速度的纯函数,那么你就有更大的问题......

这是一个关于原因的直观草图。流应用函子获取一个值并随着时间的推移将其提升为一个值——一个值序列或值流。如果您可以随时间访问某个值,则可以派生它的属性。所以速度可以用加速度来定义,位置可以用速度来定义,等等。伟大的!但是现在您想根据位置和速度来定义加速度。也很棒!但是在这种情况下,您不应该需要根据随时间变化的速度来定义加速度。为什么,你可能会问?因为随着时间的推移速度是所有加速度是开始。因此,如果您根据dv 定义a,并根据v 定义integral(a),那么您就有了一个闭环,并且您的方程没有正确确定——即使给定初始条件,也有,无限多的解决方案,或者根本没有解决方案。

【讨论】:

  • 谢谢!但是,作为对 Antal S-Z 的评论,我曾经尝试使“newtype Sim a = Sim [a]”类型为 Monad 的实例,但是,我无法使其满足 Monad 定律。有什么想法吗?
  • 从计算结果的角度来看,你的草图非常正确,而我在这个问题中的挑战是在 Haskell 之上构建一个描述物理模拟的 DSL。我认为“Sim a”类型是一些物理量的时间演化的抽象,我想在完全应用或单子世界中编写所有模拟代码。因此,每个时间点都应该对“Sim a”类型的用户隐藏。理想情况下,lambda calculous 可以在“Sim a”世界中构建是件好事。
【解决方案2】:

如果我正在考虑这个问题,你不能只使用应用函子来做到这一点;你需要一个单子。如果您有一个Applicative(称为f),您可以使用以下三个功能:

fmap  :: (a -> b) -> f a -> f b
pure  :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b

那么,给定一些f :: f a -&gt; f b,你能用它做什么?好吧,如果您有一些xs :: [a],那么您可以将其映射到:map (f . pure) xs :: [f b]。如果你改为使用fxs :: f [a],那么你可以改为使用fmap (map (f . pure)) fxs :: f [f b]1 但是,你被困在这一点上。你想要一些[f b] -&gt; f [b] 类型的函数,可能还有f (f b) -&gt; f b 类型的函数;但是,您不能在应用函子上定义这些(edit: 实际上,您可以 定义前者;请参阅编辑)。为什么?好吧,如果你看看fmappure&lt;*&gt;,你会发现你无法摆脱(或重新排列)f 类型的构造函数,所以一旦你有了[f a] ,你被困在那种形式中了。

幸运的是,这就是 monad 的用途:可以说,可以“改变形状”的计算。如果你有一个 monad m,那么除了上述之外,你还有两个额外的方法(returnpure 的同义词):

(>>=) :: m a -> (a -> m b) -> m b
join  :: m (m a) -> m a

虽然 join 仅在 Control.Monad 中定义,但它与 &gt;&gt;= 一样基本,有时可以更清晰地思考。 现在我们可以定义您的[m b] -&gt; m [b] 函数或m (m b) -&gt; m b。后者只是join;前者是sequence,来自Prelude。因此,使用 monad m,您可以将 mapX 定义为

mapX :: Monad m => (m a -> m b) -> m [a] -> m [b]
mapX f mxs = mxs >>= sequence . map (f . return)

但是,这是一种奇怪的定义方式。前奏中还有几个关于 monad 的有用函数:mapM :: Monad m =&gt; (a -&gt; m b) -&gt; [a] -&gt; m [b],相当于mapM f = sequence . map f;和(=&lt;&lt;) :: (a -&gt; m b) -&gt; m a -&gt; m b,相当于flip (&gt;&gt;=)。使用这些,我可能会将mapX 定义为

mapX :: Monad m => (m a -> m b) -> m [a] -> m [b]
mapX f mxs = mapM (f . return) =<< mxs

编辑: 实际上,我的错误:正如 John L 在评论中指出的那样,Data.Traversable(它是一个基本包)提供了函数 sequenceA :: (Applicative f, Traversable t) =&gt; t (f a) =&gt; f (t a);由于[]Traversable 的一个实例,您可以 对应用函子进行排序。尽管如此,你的类型签名仍然需要join=&lt;&lt;,所以你仍然卡住了。我可能会建议重新考虑您的设计;我认为sclv 的想法可能是正确的。


1:map (f . pure) &lt;$&gt; fxs,使用 Control.Applicative 中 fmap&lt;$&gt; 同义词。

【讨论】:

  • 听起来很棒,如果我使用 monad 而不是 Applicative,我可以得到我想要的!但是,我将 Sim 类型定义为“newtype Sim a = Sim [a]”,并且曾经尝试使其成为 Monad 的实例,但是,我无法使其满足 Monad 定律。 Sim 类型能否满足 Monad 定律?或者是否有任何其他数据结构可以满足我的目的?
  • 另外,我发现这篇文章 (patternsinfp.wordpress.com/2010/12/31/stream-monad) 显示“流单子”,但在计算时间上实现效率非常低...
  • @masayuki:当然:列表是普通 Haskell 中的单子。如果你有普通列表,那么return x = [x](=&lt;&lt;) = flip concatMap。如果您有无限列表,那么该帖子就是您想要的。我也必须同意 sclv 的评论——这似乎是一个奇怪的类型签名。
  • Sim 是一个无限列表,定义为“return x = Sim $ repeat x”
  • @Antal S-Z:可以定义Applicative f =&gt; [f b] -&gt; f [b]。这是Data.Traversible.sequenceA。不幸的是,即使使用sequenceA,我也看不出需要join 来解决这个问题。
【解决方案3】:

这是ghci 中的一个会话,我按照您想要的方式定义mapX

Prelude> 
Prelude> import Control.Applicative
Prelude Control.Applicative> :t pure
pure :: Applicative f => a -> f a
Prelude Control.Applicative> :t (<*>)
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
Prelude Control.Applicative> let mapX fun ma = pure fun <*> ma
Prelude Control.Applicative> :t mapX
mapX :: Applicative f => (a -> b) -> f a -> f b

但我必须补充一点,fmap 更好用,因为Functor 的表达力不如Applicative(这意味着使用fmap 会更频繁)。

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

编辑: 哦,你还有mapX的其他签名,无论如何,你可能指的是我建议的那个(fmap)?

【讨论】:

  • 感谢您的回复,但我要的不是 fmap。对不起。
  • 您缺少的是f [a] -&gt; f [b],而不是f a -&gt; f b。一开始我也错过了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多