注意,前面的意见!
Profunctors 是一个有点过火的抽象。IMO 我们应该首先谈论类别;在实践中,大多数profunctors是类别,但反之则不然。profunctor 类可能有有效的用途,但它实际上受到更多限制并且与Hask类别相关联。我更喜欢通过讨论其箭头构造函数是最后一个参数中的 Hask -functors 和pænultimate参数中的逆变Hask -functors 的类别来明确这一点。是的,这有点拗口,但这就是重点:这实际上是一个非常具体的情况,而且通常事实证明你真的只需要一个不太具体的类别。
具体来说,Cartesian
更自然地被认为是一类类别,而不是 profunctors:
class Category k => Cartesian k where
swap :: k (a,b) (b,a)
(***) :: k a b -> k a' b' -> k (a,a') (b,b')
哪个允许
first :: Cartesian k => k a b -> k (a,c) (b,c)
first f = f *** id
second :: Cartesian k => k a b -> k (c,a) (c,b)
second f = id *** f
这是与类别无关的id
。(您也可以根据 、 和 来定义和,***
但这与IMO 的纠缠很不协调。)second
first
second f=swap.first f.swap
f***g=first f.second g
为了了解为什么我更喜欢这种方式,而不是使用 profunctor,我想给出一个不是profunctor 的简单示例:线性映射。
newtype LinearMap v w = LinearMap {
runLinearMap :: v->w -- must be linear, i.e. if v and w are finite-dimensional
-- vector spaces, the function can be written as matrix application.
}
这不是一个 profunctor:尽管您可以使用这个特定的实现 write dimag f g (LinearMap a) = LinearMap $ dimap f g a
,但这不会保持线性。然而,它是一个笛卡尔类别:
instance Category LinearMap where
id = LinearMap id
LinearMap f . LinearMap g = LinearMap $ f . g
instance Cartesian LinearMap where
swap = LinearMap swap
LinearMap f *** LinearMap g = LinearMap $ f *** g
好吧,这看起来很微不足道。为什么这很有趣?好吧,线性映射可以有效地存储为矩阵,但从概念上讲,它们主要是函数。因此,将它们与函数类似地处理是有意义的;在这种情况下,.
有效地实现了矩阵乘法并将块对角矩阵***
放在一起,所有这些都以类型安全的方式进行。
显然,您也可以使用不受限制的函数来完成所有这些操作,因此instance Cartesian (->)
真的很简单。但是我给出了线性映射的例子来激励这个Cartesian
类可以做一些没有它就不必微不足道的事情。
Star
是真正有趣的地方。
newtype Star f d c = Star{runStar :: d->f c}
instance Monad f => Category (Star f) where
id = Star pure
Star f . Star g = Star $ \x -> f =<< g x
instance Monad f => Cartesian (Star f) where
swap = Star $ pure . swap
Star f *** Star g = Star $ \(a,b) -> liftA2 (,) (f a) (g b)
Star
是kleisli 类别的前身,您可能听说过它是使用一元计算链接的一种方法。那么让我们直接来看一个IO
例子:
readFile' :: Star IO FilePath String
readFile' = Star readFile
writeFile' :: Star IO (FilePath,String) ()
writeFile' = Star $ uncurry writeFile
现在我可以做类似的事情
copyTo :: Star IO (FilePath, FilePath) ()
copyTo = writeFile' . second readFile'
我为什么要这样做?关键是我已经将 IO 操作链接在一起,而没有使用任何方式来查看/修改传递的数据的接口。这对于安全应用程序可能很有趣。(我只是编造了这个例子;我确信可以找到不那么做作的例子。)
无论如何,到目前为止,我还没有真正回答过这个问题,因为您不是在问笛卡尔类别,而是在问强 profunctors。不过,它们确实提供了几乎相同的界面:
class Profunctor p => Strong p where
first' :: p a b -> p (a, c) (b, c)
second' :: p a b -> p (c, a) (c, b)
因此我也可以做出微小的改变
copyTo :: Star IO (FilePath, FilePath) ()
copyTo = writeFile' . second' readFile'
保留基本相同的示例,但使用Strong
而不是Cartesian
. 不过,我仍在使用该Category
组合物。而且我相信如果没有任何组合,我们将无法构建非常复杂的示例。
最大的问题变成了:为什么要使用 profunctor 接口,而不是基于类别的接口?哪些问题必须在没有组合的情况下完成?答案几乎在于Category
:Star
我不得不对Monad f
. 对于 profunctor 实例,这不是必需的:这些只需要Functor f
. 因此,对于作为强仿函数的大多数重点示例Star
,您将需要查看不是应用程序/单子的基本仿函数。与此类函子相关的一个重要应用是 Van Laarhoven透镜,并且这些的内部实现确实可能为强大的profunctors提供了最有见地的例子。每当我浏览镜头库的来源时,我都会感到头晕目眩,但我认为一个非常有影响力的实例是Strong Indexed。