这个 属性 的函子比 monad 强吗?

Is this property of a functor stronger than a monad?

在思考如何泛化 monad 时,我想出了以下 属性 的函子 F:

inject :: (a -> F b) -> F(a -> b) 

-- 这应该是 a 和 b 中的自然变换。

在没有更好的名字的情况下,如果存在上面显示的自然变换inject,我将函子称为 F bindable

主要问题是,这个 属性 是否已知并有名字,它与函子的其他众所周知的属性(例如,适用性、单子性、指向性、可遍历性)有何关系等)

"bindable" 这个名字的动机来自于以下考虑:假设 M 是一个单子,F 是一个 "bindable" 函子。则有如下自然态射:

fbind :: M a -> (a -> F(M b)) -> F(M b)

这类似于 monadic "bind",

bind :: M a -> (a -> M b) -> M b

除了结果用函子 F 修饰。

fbind 背后的想法是,广义的单子运算不仅可以产生单个结果 M b,还可以产生 "functor-ful" F 个这样的结果。我想表达当 monadic 操作产生多个 "strands of computation" 而不是一个时的情况;每个 "strand of computation" 又是一个单子计算。

注意每个函子 F 都有态射

eject :: F(a -> b) -> a -> F b

与 "inject" 相反。但不是每个函子 F 都有 "inject".

具有 "inject" 的函子示例:F t = (t,t,t)F t = c -> (t,t),其中 c 是常量类型。函子 F t = c(常量函子)或 F t = (c,t) 不是 "bindable"(即没有 "inject")。延续函子 F t = (t -> r) -> r 似乎也没有 inject.

"inject" 的存在可以用不同的方式表述。考虑 "reader" 仿函数 R t = c -> t ,其中 c 是常量类型。 (这个函子是适用的和单子的,但那不是重点。)"inject" 属性 则意味着 R (F t) -> F (R t),换句话说,R 与 F 交换。请注意,这不是与 F 可遍历的要求相同;那本来是 F (R t) -> R (F t),对于任何关于 R 的函子 F 总是满足的。

到目前为止,我已经能够证明 "inject" 对任何单子 M 蕴含 "fbind"。

此外,我展示了每个具有 "inject" 的函子 F 还将具有这些附加属性:

point :: t -> F t

未决问题:

为了稍微改进术语,我建议将这些仿函数称为 "rigid" 而不是 "bindable"。 "rigid"的动机将在下面解释。

定义

如果函子 f 具有如图所示的 inject 方法,则称为 rigid。请注意,每个仿函数都有 eject 方法。

class (Functor f) => Rigid f where
  inject :: (a -> f b) -> f(a -> b)

  eject :: f(a -> b) -> a -> f b
  eject fab x = fmap (\ab -> ab x) fab

"nondegeneracy" 定律必须成立:

eject . inject = id

属性

刚性函子总是指向:

instance (Rigid f) => Pointed f where
  point :: t -> f t
  point x = fmap (const x) (inject id)

如果刚性函子是适用的,那么它自动是单子的:

instance (Rigid f, Applicative f) => Monad f where
  bind :: f a -> (a -> f b) -> f b
  bind fa afb = (inject afb) <*> fa

刚性的 属性 与单子的 属性 不可比(既不弱也不强):如果函子是刚性的,似乎并不意味着它是自动的monadic(虽然我不知道这种情况的具体反例)。如果一个函子是一元的,并不意味着它是刚性的(有反例)。

非刚性单子函子的基本反例是 MaybeList。这些是具有多个构造函数的仿函数:此类仿函数不能是刚性的。

Maybe 实现 inject 的问题是 inject 必须将 a -> Maybe b 类型的函数转换为 Maybe(a -> b)Maybe有两个构造函数。 a -> Maybe b 类型的函数可以 return 不同的构造函数用于 a 的不同值。但是,我们应该构造一个 Maybe(a -> b) 类型的值。如果对于某些 a,给定函数产生 Nothing,我们没有 b,因此我们无法产生总函数 a->b。因此我们不能 return Just(a->b);我们被迫 return Nothing 只要给定函数产生 Nothing 甚至对于 a 的一个值。但是我们无法检查类型为 a -> Maybe b 的给定函数是否为 a 的所有值生成 Just(...)。因此,我们在所有情况下都被迫 return Nothing。这将不满足非退化定律。

所以,如果f t是"fixed shape"的容器(只有一个构造函数),我们就可以实现inject。因此得名 "rigid".

关于刚性比单子性更具限制性的另一种解释是考虑自然定义的表达式

(inject id) :: f(f a -> a) 

其中 id :: f a -> f a。这表明我们可以对任何类型 a 有一个 f-代数 f a -> a,只要它被包裹在 f 中。任何单子都有代数是不正确的;例如,各种 "future" monad 以及 IO monad 描述类型 f a 的计算不允许我们提取类型 a 的值 - 我们不应该即使包装在 f 容器中,也能够拥有类型为 f a -> a 的方法。这表明 "future" monad 和 IO monad 不是刚性的。

属性 严格 比刚度强 的 distributivity 来自 E. Kmett 的一个包裹。如果我们可以像 p (f t) -> f (p t) 中那样为 any 函子 p 交换顺序,则函子 f 是分配的。刚性等同于只能交换 "reader" 函子 r t = a -> t 的顺序。所以,所有的分配函子都是刚性的。

所有分配函子都必须是可表示的,这意味着它们等同于 "reader" 具有某些固定类型 c 的函子 c -> t。然而,并不是所有的刚性函子都是可表示的。一个例子是由

定义的函子 g
type g t = (t -> r) -> t

仿函数 g 不等同于固定类型 c -> t c

结构和例子

不可表示(即不是 "distributive")的刚性函子的更多示例是 a t -> f t 形式的函子,其中 aany contrafunctor 和 f 是刚性函子。同样,笛卡尔积和两个刚性函子的组合也是刚性的。这样,我们可以在函子的指数多项式class内产生许多刚性函子的例子。

我对What is the general case of QuickCheck's promote function?的回答也列出了刚性函子的构造:

  1. f = Identity
  2. 如果 fg 都是刚性的那么函子乘积 h t = (f t, g t) 也是刚性的
  3. 如果 fg 都是刚性的,那么组合 h t = f (g t) 也是刚性的
  4. 如果 f 是刚性的并且 g 是任何逆变函子那么函子 h t = g t -> f t 是刚性的

刚性函子的另一个属性是类型r ()等价于(),即r ()类型只有一个不同的值。此值为 point (),其中 point 是上面为任何刚性函子 r 定义的。 (我有一个证明,但我不会在这里写出来,因为我找不到简单的单行证明。)结果是刚性仿函数必须只有一个构造函数。这马上说明MaybeEitherList等不能死板

与 monad 的联系

如果 f 是一个具有 "composed-outside" 类型的 monad 转换器的 monad,t m a = f (m a),那么 f 是一个刚性函子。

"rigid monads" 可能是刚性函子的子集,因为如果 f 也是刚性单子而不是任意刚性函子(但逆变函子 g 仍然可以是任意的)。但是,我没有任何不是 monad 的刚性仿函数的例子。

刚性单子的最简单示例是 type r a = (a -> p) -> a,即 "search monad"。 (这里p是固定类型。)

为了证明带有 "composed-outside" 变换器 t m a = f (m a) 的单子 f 也有一个 inject 方法,我们考虑变换器 t m a 与外国monad m 被选为 reader monad,m a = r -> a。那么具有正确类型签名的函数inject被定义为

 inject = join @t . return @r . (fmap @m (fmap @f return @m))

选择合适的类型参数。

非退化律从t的单子自然性得出:单子态射m -> Identity(将类型r的值代入reader)是提升到单子态射 t m a -> t Id a。我省略了这个证明的细节。

用例

最后,我发现了刚性函子的两个用例。

第一个用例是考虑刚性仿函数的最初动机:我们希望一次 return 多个单子结果。如果 m 是一个 monad 并且我们想要 fbind 如问题所示,我们需要 f 是刚性的。那么我们可以将fbind实现为

fbind :: m a -> (a -> f (m b)) -> f (m b)
fbind ma afmb = fmap (bind ma) (inject afmb)

我们可以使用 fbind 对任何单子进行 return 多个单子结果(或者,更一般地,一个刚性函子的单子结果)的单子操作 m.

第二个用例是出于以下考虑。假设我们有一个程序 p :: a,它内部使用了一个函数 f :: b -> c。现在,我们注意到函数 f 非常慢,我们想通过用单子 "future" 或 "task" 替换 f 或通常用一些 monad m 的 Kleisli 箭头 f' :: b -> m c。当然,我们希望程序 p 也将成为 monadic:p' :: m a。我们的任务是将p重构为p'.

重构分两步进行:首先,我们重构程序p,使函数f明确成为p的参数。假设这已经完成,所以现在我们有 p = q f where

q :: (b -> c) -> a

其次,我们将f替换为f'。我们现在假设 qf' 已给出。我们想构建类型为

的新程序 q'
q' :: (b -> m c) -> m a

所以p' = q' f'。问题是我们是否可以定义一个通用组合器,将 q 重构为 q'

refactor :: ((b -> c) -> a) -> (b -> m c) -> m a

原来refactor只有在m是刚性函子的情况下才能构造。在尝试实现 refactor 时,我们发现了与我们尝试为 Maybe 实现 inject 时本质上相同的问题:我们得到一个函数 f' :: b -> m c,它可以 return不同的b有不同的单子效应m c,但是我们需要构造m a,它必须代表所有b的相同的单子效应。这不起作用,例如,如果 m 是一个具有多个构造函数的 monad。

如果m是刚性的(我们不需要要求m是一个monad),我们可以实现refactor:

refactor bca bmc = fmap bca (inject bmc)

如果m不死板,我们就不能重构任意程序。到目前为止,我们已经看到continuation monad 是刚性的,但是"future"-like monad 和IO monad 不是刚性的。这再次表明,在某种意义上,严格性 属性 比一元性更强。

这是刚性仿函数的一种可能的表示形式。我冒昧地把你的名字改成了自行车,原因我很快就会知道:

flap :: Functor f => f (a -> b) -> a -> f b
flap u a = ($ a) <$> u 

class Functor g => Rigid g where
    fflip :: (a -> g b) -> g (a -> b)
    fflip f = (. f) <$> extractors

    extractors :: g (g a -> a)
    extractors = fflip id

-- "Left inverse"/non-degeneracy law: flap . fflip = id

instance Rigid ((->) r) where
    fflip = flip

关于我的措辞的一些评论:

  • 我把injecteject改成了fflipflap,主要是因为在我看来,flap 看起来更像是注射,由于这样的事情:

    sweep :: Functor f => f a -> b -> f (a, b)
    sweep u b = flap ((,) <$> u) b
    
  • 我取了flap这个名字from protolude。这是对 flip 的一种演绎,这很合适,因为它是概括它的两种对称方式之一。 (我们可以将函数拉到任意 Functor 之外,如 flap,或者将 Rigid 仿函数拉到函数外部,如 fflip。)

  • extractorsfflip 是可以相互定义的,例如,可以为 search/selection monad 编写这个简洁的实例:

    newtype Sel r a = Sel { runSel :: (a -> r) -> a }
        deriving (Functor, Applicative, Monad) via SelectT r Identity
    
    instance Rigid (Sel r) where
        -- Sel r (Sel r a -> a) ~ ((Sel r a -> a) -> r) -> Sel r a -> a
        extractors = Sel $ \k m -> m `runSel` \a -> k (const a)
    

关于 extractors 的一个重要事实是它产生了以下组合子:

distributeLike :: (Rigid g, Functor f) => f (g a) -> g (f a)
distributeLike m = (<$> m) <$> extractors

distributeLikethe Distributive classdistribute 的更通用版本。一个合法的 distribute 反过来必须遵守以下法律,这些法律与 Traversable 的法律是双重的:

-- Identity law
fmap runIdentity . distribute = runIdentity

-- Composition law
fmap getCompose . distribute = distribute . fmap distribute . getCompose

-- Naturality law (always holds, by parametricity)
-- For any natural transformation t
fmap t . distribute = distribute . t

因为fflipdistributeLike,reader(即函数函子)作为另一个函子,而flapdistribute reader,flap . fflip = idfflip . flap = id 都是...

的特例
-- m :: f (g a)
distributeLike (distributeLike m) = m

...有适当的选择fg。现在,上面的 属性 可以证明等价于以下条件:

  1. distributeLike for g 遵循分配函子的恒等律(顺便说一句,相当于刚性律);

  2. distributeLike for f也遵循分配函子恒等律;

  3. 以下等价条件之一成立:

    一个。 distributeLike for f 遵循分配函子的组合规律;或

    b. extractorsf 提供的所有 f a -> a 功能在 a.

    中是自然的

特别是,由于 flap 是合法的 distributeflap . fflip = id 相当于 g 的恒等律(条件 #2),而 fflip . flap = id,到 f 分配(条件 #1 和 #3)。

(上述条件可以通过根据 extractors 分析 distributeLike . distributeLike = id 来建立,遵循与我在 the "The roadblock, and a detour" 部分中应用于合成法则的策略类似的策略post“每个分配都是可表示的”。)

为了便于说明,让我们考虑 Sel r 的情况。正如您所注意到的,它是严格的但不是分配的,它的 distributeLike 遵循身份法则而不是组合法则。因此,fflip . flap = id 不成立。

关于在类型 class 星座中为 Rigid 寻找位置,我会强调条件 #3b 特别有趣。看来,鉴于 extractors @f :: forall a. f (f a -> a)a 中是完全多态的,它提供 non-natural f a -> a 提取器 f 必须不是严格正的,对应于构造 的“结构和示例”部分中的 #4。缺乏严格的积极性使得 extractors 可以合并 non-naturality(通过 user-supplied 逆变论证),而无需在其定义中明确指定。既然如此,只有非严格正的函子,例如 Sel r,可能是刚性的而不是分配的。

杂项备注

  • 从单子的角度来看 fflipflap,我们可以说刚性单子配备了从 Kleisli 箭头到 [=98= 的内射转换]. non-distributive 刚性单子的一个奇怪的方面是 fflip 是单射而不是满射意味着静态箭头比 Kleisli 箭头多,这是一种非常不寻常的情况。

  • extractors 浓缩了 Distributive 的大部分内容。对于任何分配函子 g,都有一个 g (g a -> a) 值,其中每个位置都用匹配的 g a -> a 自然提取函数填充。对于非分配的刚性函子,这种整洁的对应关系不再成立。例如,对于 Sel r,每个 a -> r 都会产生一个提取器,这通常是不自然的。这最终排除了 distribute/fflip(顺便说一句,tabulate)作为同构。事实上,在处理非严格正的函子时,具有 well-defined 个位置的形状的概念可能会被打破。

  • DistributiveTraversable 是对偶的,关于这两个 class 的事实之间有几个对应关系。 (特别是,Distributive 表示为 Representable,就 reader 函子的同构而言,镜像 ,后者可以表示为 reader 的同构一些 list-like 函子。)既然如此,人们可能想知道类似于 Rigid 的概念是否对 Traversable 有意义。我相信它确实如此,尽管尚不清楚这样一个概念可能会有多大用处。的一个例子co-rigid" pseudo-traversable 是一个数据结构,具有遍历重复效果,但在应用层下重建结构时丢弃相应的重复元素,因此遵循恒等律,但不作文一

我们都很熟悉Traversable类型class,可以归结为:

class Functor t => Traversable t
  where
  sequenceA :: Applicative f => t (f a) -> f (t a)

这利用了 Applicative 仿函数的概念。 Applicative 下的分类概念有一个仅法则 ,如下所示:

-- Laxities of a lax monoidal endofunctor on Hask under (,)
zip :: Applicative f => (f a, f b) -> f (a, b)
zip = uncurry $ liftA2 (,)

husk :: Applicative f => () -> f ()
husk = pure

-- Oplaxities of an oplax monoidal endofunctor on ... (this is trivial for all endofunctors on Hask)
unzip :: Functor f => f (a, b) -> (f a, f b)
unzip fab = (fst <$> fab, snd <$> fab)

unhusk :: f () -> ()
unhusk = const ()

-- The class
class Applicative f => StrongApplicative f

-- The laws
-- zip . unzip = id
-- unzip . zip = id
-- husk . unhusk = id
-- unhusk . husk = id -- this one is trivial

链接的问题及其答案有更多细节,但要点是 StrongApplicatives 为函子模拟了 "fixed size" 的一些概念。这种类型 class 与 Representable 仿函数有一个有趣的联系。作为参考,Representable 是:

class Functor f => Representable x f | f -> x
  where
  rep :: f a -> (x -> a)
  unrep :: (x -> a) -> f a

instance Representable a ((->) a)
  where
  rep = id
  unrep = id

表明 StrongApplicativeRepresentable 的推广,因为每个 Representable 都是 StrongApplicative。是否有任何 StrongApplicative 不是 Representable 尚不清楚。

现在,我们知道 Traversable 是根据 Applicative 制定的,并且朝着一个方向运行。由于 StrongApplicativeApplicative 松弛性提升为同构,也许我们想使用这个额外的条件来反转 Traversable 提供的分配律:

class Functor f => Something f
  where
  unsequence :: StrongApplicative f => f (t a) -> t (f a)

碰巧 (->) aStrongApplicative,实际上是 Representable [=24] 属的代表性标本(请原谅双关语) =] 函子。因此我们可以将您的 inject/promote 操作写为:

promote :: Something f => (a -> f b) -> f (a -> b)
promote = unsequence

我们之前提到过,StrongApplicativeRepresentative 函子家族的超级class。通过检查 unsequence 的类型,很明显,我们对多态应用程序施加的约束越强,实现 unsequence 就越容易(因此得到的 class).

所以在某种意义上,有一个 "detraversable" 仿函数的层次结构,它的流动方向与应用效果的层次结构相反,您可能希望对其进行反遍历。 "inner" 仿函数的层次结构是这样的:

Functor f => Applicative f => StrongApplicative f => Representable x f

detraversable/distributive 仿函数的相应层次结构可能是这样的:

Distributive t <= ADistributive t <= SADistributive t <= RDistributive t

有定义:

class RDistributive t
  where
  rdistribute :: Representable x f => f (t a) -> t (f a)

  default rdistribute :: (SADistributive t, StrongApplicative f) => f (t a) -> t (f a)
  rdistribute = sadistribute

class RDistributive t => SADistributive t
  where
  sadistribute :: StrongApplicative f => f (t a) -> t (f a)

  default sadistribute :: (ADistributive t, Applicative f) => f (t a) -> t (f a)
  sadistribute = adistribute

class SADistributive t => ADistributive t
  where
  adistribute :: Applicative f => f (t a) -> t (f a)

  default adistribute :: (Distributive t, Functor f) => f (t a) -> t (f a)
  adistribute = distribute

class ADistributive t => Distributive t
  where
  distribute :: Functor f => f (t a) -> t (f a)

我们对 promote 的定义可以概括为依赖于 RDistributive(因为 (->) a 本身确实是一个可表示的函子):

promote :: RDistributive f => (a -> f b) -> f (a -> b)
promote = rdistribute

在一个奇怪的事件中,一旦你到达这个层次结构的底部(即 Distributive),你对可逆性的承诺相对于你的需求变得如此强烈以至于唯一的函子你可以自己实现它 Representable。这种分配的、可表示的(因此是刚性的)函子的一个例子是对:

data Pair a = Pair { pfst :: a, psnd :: a }
  deriving Functor

instance RDistributive Pair
instance SADistributive Pair
instance ADistributive Pair
instance Distributive Pair
  where
  distribute x = Pair (pfst <$> x) (psnd <$> x)

当然如果你对"inner functor"的多态有强烈的需求,比如Representable x fRDistributive,这样的情况就成为可能:

newtype Weird r a = Weird { runWeird :: (a -> r) -> a }
  deriving Functor

instance RDistributive (Weird r)
  where
  rdistribute = fmap unrep . promoteWeird . rep
    where
    promoteWeird :: (x -> Weird r a) -> Weird r (x -> a)
    promoteWeird f = fmap (. f) $ Weird $ \k m -> m `runWeird` \a -> k (const a)

TODO:检查所有其他刚性函子示例在层次结构中的位置(如果有的话)。

正如我所说的,我还没有仔细考虑过它,所以也许这里的那些对刚性仿函数概念投入了一些思考的人可以立即在其中找到漏洞。或者,也许它会让我还看不到的东西落到实处。

可能值得考虑这些不可遍历类型class的一些规律。一个明显的暗示自己的是 sequence . unsequence = idunsequence . sequence = id 只要函子同时支持 TraversableUntraverse.

还值得一提的是,"distributive law" 仿函数与 monad 和 comonads 的交互得到了很好的研究,因此这可能与您帖子中与 monad 相关的讨论有一些相关性。