仿函数的这个属性是否比monad更强?

时间:2016-09-22 21:56:28

标签: haskell functional-programming monads functor category-theory

在考虑如何推广monad时,我想出了一个仿函数F的以下属性:

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

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

如果没有更好的名称,如果存在上面显示的自然转换inject,我会将仿函数F bindable 称为。

主要的问题是,这个属性是否已知并且有一个名称,它是如何与仿函数的其他众所周知的属性相关的(例如,应用,monadic,尖头,可遍历等)

名称的动机"可绑定"来自以下考虑:假设M是monad而F是"可绑定"仿函数。然后有一个具有以下自然态射:

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

这类似于monadic" bind",

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

除了结果用仿函数F。

装饰

fbind背后的想法是,一般化的monadic操作不仅可以产生单个结果M b,而且可以产生一个" functor-ful" F这样的结果。我想表达一个monadic操作产生几个计算线的情况"而不仅仅是一个;每个"计算链"再次成为monadic计算。

请注意,每个仿函数F都具有态射

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

与#34;注入"相反。但并非每个仿函数F都有"注入"。

具有"注入":F t = (t,t,t)F t = c -> (t,t)的仿函数示例,其中c是常量类型。函数F t = c(常量仿函数)或F t = (c,t)不是"可绑定的" (即没有"注入")。继续仿函数F t = (t -> r) -> r似乎也没有inject

"注入"可以用不同的方式制定。考虑一下"读者"仿函数R t = c -> t其中c是常量类型。 (这个仿函数是适用的和monadic,但这不是重点。)"注入"然后,属性意味着R (F t) -> F (R t),换句话说,R与F通勤。注意,这与F可以遍历的要求不同;那将是F (R t) -> R (F t),对于任何关于R的函子F总是满意。

到目前为止,我能够证明"注入"暗示" fbind"任何monad M。

另外,我展示了每个有"注入"还将具有以下附加属性:

  • 指出

point :: t -> F t

  • 如果F是"可绑定"然后应用F也是monad

  • 如果F和G是"可绑定"那么配对仿函数F * G(但不是F + G)

  • 也是如此
  • 如果F是"可绑定"和A是任何一个profunctor然后(pro)仿函数G t = A t -> F t是可绑定的

  • 身份仿函数可绑定。

打开问题:

  • 属于"可绑定"相当于其他一些众所周知的属性,还是通常不考虑的仿函数的新属性?

  • 是否有其他属性的仿函数" F"从"注入"?

  • 的存在开始
  • 我们是否需要任何针对"注入"的法律,这会有用吗?例如,我们可以要求R(F t)在一个或两个方向上与F(R t)同构。

3 个答案:

答案 0 :(得分:9)

为了稍微改进一下术语,我建议把这些算子称为“刚性”#34;而不是"可绑定"。说"刚性"的动机将在下面解释。

仿函数f如果具有inject方法,则称为刚性。请注意,每个仿函数都有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

非简并法则#34;必须坚持:

eject . inject = id

始终指出一个严格的仿函数:

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

如果一个刚性的仿函数是适用的那么它就是自动的monadic:

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

刚性的属性与monadic属性不具有可比性(既不弱也不强):如果一个仿函数是刚性的,它似乎并不是因为它是自动monadic(虽然我不知道本案的具体反例)。如果一个仿函数是monadic,那就不是说它是刚性的(有反例)。

非严格的monadic仿函数的基本反例是MaybeList。这些是具有多个构造函数的仿函数:通常,这样的仿函数不是刚性的。

inject实施Maybe的问题是,inject必须将a -> Maybe b类型的函数转换为Maybe(a -> b),而Maybe有两个构造函数。类型a -> Maybe b的函数可以为a的不同值返回不同的构造函数。但是,我们应该构造一个Maybe(a -> b)类型的值。如果对于某些a,给定的函数会生成Nothing,我们就不会生成b,因此我们无法生成总函数a->b。因此我们无法返回Just(a->b);只要给定函数产生Nothing,即使对于Nothing的一个值,我们也被迫返回a。但我们无法检查a -> Maybe b类型的给定函数是否为Just(...)的所有值生成a。因此,在所有情况下我们都被迫返回Nothing。这不符合非简并法则。

因此,如果inject是&#34;固定形状&#34;的容器,我们可以实施f t (只有一个构造函数)。因此名称&#34;刚性&#34;。

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

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

其中id :: f a -> f a。这表明我们可以为任何类型f a -> a设置f-algebra a,只要它包含在f中。任何单子都有代数是不正确的;例如,各种未来&#34; monads以及IO monad描述类型f a的计算,它们不允许我们提取类型a的值 - 我们不应该有类型的方法f a -> a即使包裹在f - 容器内。这表明&#34;未来&#34; monad和IO monad不是僵硬的。

一个严格强大的属性是来自E. Kmett的一个包的distributivity。如果我们可以在f中为任何仿函数p (f t) -> f (p t)交换订单,则仿函数p是分布式的。刚性与只能与“读者”交换订单相同。仿函数r t = a -> t。所以,所有的分配函子都是僵化的。

所有分配仿函数都必须具有代表性,这意味着它们等同于&#34;读者&#34;有一些固定类型c -> t的仿函数c。然而,并非所有刚性函子都是可表示的。一个例子是由

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

仿函数g不等同c -> t,其类型为c

不可表示的刚性仿函数的其他示例(即不是&#34;分布式&#34;)是a t -> f t形式的仿函数,其中a任何反向函数而f是一个严格的函子。此外,笛卡尔积和两个刚性仿函数的组合也是刚性的。通过这种方式,我们可以在指数多项式类的函子中生成许多刚性函子的例子。

我对What is the general case of QuickCheck's promote function?的回答还列出了刚性仿函数的结构。

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

第一个用例是考虑刚性仿函数的原始动机:我们希望一次返回几个monadic结果。如果m是monad,我们希望问题中显示fbind,那么我们需要f来保持僵化。然后我们可以将fbind实现为

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

对于任何monad fbind,我们可以使用m进行monadic操作,返回多个monadic结果(或者更常见的是,monadic结果的刚性函数)。

第二个用例是出于以下考虑而增长的。假设我们有一个内部使用函数p :: a的程序f :: b -> c。现在,我们注意到函数f非常慢,我们希望通过将f替换为monadic&#34; future&#34;来重构程序。对于某些monad f' :: b -> m c,或者&#34;任务&#34;,或者通常使用Kleisli箭头m。当然,我们希望程序p也将成为monadic:p' :: m a。我们的任务是将p重构为p'

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

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时,我们发现与我们尝试为inject实施Maybe时基本相同的问题:我们获得了一个可以返回不同monadic效果的函数f' :: b -> m c对于不同的m c b,我们需要构建m a,这必须代表所有b的相同的monadic效果。例如,如果m是具有多个构造函数的monad,则无法工作。

如果m严格(我们不需要m为monad),我们就可以实施refactor

refactor bca bmc = fmap bca (inject bmc)

如果m不严格,我们就无法重构任意程序。到目前为止,我们已经看到延续monad是僵化的,但是&#34;未来&#34; -like monad和IO monad并不是僵化的。这再次表明,在某种意义上,刚性比一性更强。

答案 1 :(得分:2)

我最近一直在做一些实验,以更好地了解Distributive。令人高兴的是,我的结果似乎与your rigid functors紧密相关,从而使两者都清晰可见。

首先,这是刚性函子的一种可能表示。我冒昧地给您的名字加上了一些名称,出于以下原因,我很快就会去:

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函子拉到函数外部。)

  • 我在玩extractors的过程中首先意识到了Distributive的重要性,但我并没有想到作为另一堂课的一部分可能有意义。 extractorsfflip是可互定义的,例如,可以为搜索/选择单子写出一个相当简洁的实例:

    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)
    

每个分配函子都是严格的:

fflipDistrib :: Distributive g => (a -> g b) -> g (a -> b)
fflipDistrib = distribute @_ @((->) _)
-- From this point on, I will pretend Rigid is a superclass of Distributive.
-- There would be some tough questions about interface ergonomics if we were
-- writing this into a library. We don't have to worry about that right now,
-- though.

从另一个方向来看,我们可以编写一个使用distribute来模仿Rigid签名的函数:

infuse :: (Rigid g, Functor f) => f (g a) -> g (f a)
infuse u = (<$> u) <$> extractors

infuse不是distribute。如您所注意到的,有一些刚性的函子不是分布式的,例如Sel。因此,我们必须得出结论,在一般情况下,infuse没有遵循分配律。

(除了:infusedistribute情况下不是合法的Sel可以通过基数论证来确定。如果infuse遵循分配律,我们将为任意两个刚性函子使用infuse . infuse = id。但是,类似infuse @((->) Bool) @(Sel r)的结果会导致结果类型的居民少于参数类型;因此,它不可能有左逆。)

星座中的一个地方

在这一点上,有必要使我们清楚地了解DistributiveRigid的区别。假设您的定律是flap . fflip = id,则直觉表明fflip . flap = id的同构的另一半Distributive可能成立。检查该假设需要绕过Distributive

Distributive(和Rigid)的另一种表示形式,其中distribute(或fflip)通过函数函子进行分解。更具体地说,任何类型为g a的函子值都可以转换为采用forall x. g x -> x提取器的CPS暂停:

-- The existential wrapper is needed to prevent undue specialisation by GHC.
-- With pen and paper, we can leave it implicit.
data Ev g a where
    Ev :: ((g x -> x) -> a) -> Ev g a

-- Existential aside, this is ultimately just a function type.
deriving instance Functor (Ev g)

-- Morally, evert = flip id
evert :: g a -> Ev g a
evert u = Ev $ \e -> e u

如果gRigid,我们可以朝另一个方向前进,并从停权中恢复函数值:

-- Morally, revert = flip fmap extractors
revert :: Rigid g => Ev g a -> g a
revert (Ev s) = s <$> extractors

Ev g本身就是Distributive,无论g是什么-毕竟,它只是一个函数:

-- We need unsafeCoerce (yikes!) because GHC can't be persuaded that we aren't
-- doing anything untoward with the existential.
-- Note that flip = fflip @((->) _)
instance Rigid (Ev g) where
    fflip = Ev . flip . fmap (\(Ev s) -> unsafeCoerce s)

-- Analogously, flap = distribute @((->) _)
instance Distributive (Ev g) where
    distribute = Ev . flap . fmap (\(Ev s) -> unsafeCoerce s) 

此外,任意fflip / distribute函子的RigidDistributive都可以通过evertrevert进行路由:

-- fflip @(Ev g) ~ flip = distribute @((->) _) @((->) _)
fflipEv :: Rigid g => (a -> g b) -> g (a -> b)
fflipEv = revert . fflip . fmap evert

-- distribute @(Ev g) ~ flap = distribute @((->) _) _
distributeEv :: (Rigid g, Functor f) => f (g a) -> g (f a) 
distributeEv = revert . distribute . fmap evert
实际上,

revert就足以实现Distributive。用这样的术语,分配律等于要求evertrevert是逆的:

revert . evert = id  -- "home" roundtrip, right inverse law
evert . revert = id  -- "away" roundtrip, left inverse law

这两个往返分别对应于两个非自由分配律:

fmap runIdentity . distribute = runIdentity                               -- identity
fmap getCompose . distribute = distribute . fmap distribute . getCompose  -- composition

distribute . distribute = id文档中所述的Data.Distributive要求最终等于这两个定律,再加上自然性。)

更早之前,我推测涉及fflip的同构:

flap . fflip = id  -- "home" roundtrip, left inverse Rigid law  
fflip . flap = id  -- "away" roundtrip, would-be right inverse law

可以直接验证,刚性定律flap . fflip = id等同于另一个“本地”往返行程revert . evert = id。另一个方向比较棘手。所谓的同构可以像这样链接:

                        g (a -> b)        
    {fflip => <= flap}              {evert => <= revert}
a -> g b                                                   Ev g (a -> b)
    {fmap evert => <= fmap revert} {distribute => <= distribute}
                             a -> Ev g b

让我们假设严格的定律成立。我们想证明fflip . flap = id当且仅当evert . revert = id时,因此我们必须处理两个方向:

  • 首先,让我们假设evert . revert = id。从a -> g bg (a -> b)的正方形逆时针旋转的方式等于fflip(请参见上面fflipEv的定义)。由于逆时针方式是由三个同构构成的,因此fflip具有反方向。由于flap是其左逆(根据严格的定律),因此它也必须是其逆。因此fflip . flap = id

  • 其次,假设fflip . flap = id。同样,从a -> g bg (a -> b)的逆时针方向是fflip,但是现在我们知道它具有相反的含义,即flap。因此,组成逆时针方向的每个函数都必须具有逆函数。特别地,revert必须具有反函数。由于evert是其右逆(根据严格的定律),因此它也必须是其逆。因此,evert . revert = id

以上结果使我们可以精确地确定Rigid相对于Distributive的位置。刚性函子是一个可能的分布,只是它仅遵循分布的恒律,而不遵循构成的恒律。以fflip为逆,使flap同构,等于将Rigid升级为Distributive

其他备注

  • 从一元论的角度来看fflipflap,我们可以说刚性单子具有从Kleisli箭头到static arrows的内射转换。使用分布式monad,转换将升级为同构,这是Applicative and Monad are equivalent for Reader的概括。

  • extractors凝聚了Distributive的许多内容。对于任何分布函子g,都有一个g (g a -> a)值,其中每个位置都填充有一个匹配的g a -> a提取函数。准确地说,当我们从Distributive移到Rigid时,我们失去了这种保证,即位置和提取器将匹配,并且无所不能地重建适当的函子形状。在这种情况下,值得在此答案的开头重新研究extractors的{​​{1}}实现。任何Sel函数都对应一个a -> r提取器,这意味着通常会有大量我们无法枚举的提取器,因此我们必须以非同构Sel r a -> a和{ {1}}(事后看来,在fflip的实现中出现的infuse已经把游戏输了)。感觉有点像缺少函数的const实例。 (不过,在这种情况下,如果该函数的域类型是可枚举的Data.Universe样式,则有一种作弊方法。我不确定{{1 }}。

  • 我主要通过镜像对偶类extractors的{​​{3}}的工作原理,获得了关于Traversable的{​​{1}}同构的结果。 (Bird等人撰写的shape-and-contents decomposition是探讨形状和内容主题的非常易读的论文)。尽管最好单独发表一篇文章来详细介绍,但这里至少有一个问题值得提出:类似于Sel的概念对revert有意义吗?我相信确实如此,尽管我的感觉是它听起来没有Distributive有用。 “共刚性”伪遍历的一个示例是一种具有遍历效果的遍历的数据结构,该遍历可以复制效果,但是在重建应用层下的结构时会丢弃相应的重复元素,从而遵循标识法则-而不是组成一个。

  • 说到TraversableRigid的结构本身就很有意义:它是自由分布函子的编码。特别是,TraversableRigid在免费的monad上可以与Understanding Idiomatic Traversals Backwards and ForwardsliftF进行比较,并且在其他的免费结构上可以与类似的功能进行比较。 (在这种情况下,revertEv完全相反,表明evert有多强。缩回在某些情况下更常见,因为它发生在revert的一般情况。)

  • 最后但并非最不重要的是,还有另一种理解revert的方法:这意味着多态提取器类型在retract的意义上代表{{ 1}}对应于evert,而Distributive对应于Rigid。不幸的是,量化使得在Haskell中使用实际的Ev接口来表达它非常尴尬。 (这是有症状的,我必须让evert赋予index其自然的reverttabulate实例。)如果它充当安慰,那将不是一个安慰。无论如何,这都是非常实用的表示形式:如果我已经有了一个多态提取器,则实际上不需要Representable来提取值。

答案 2 :(得分:0)

我们都熟悉Traversable类型类,可以将其归结为以下内容:

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

这利用了Applicative仿函数的概念。 Applicative之下的分类概念只有一个法律strengthening如下:

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

链接的问题及其答案有更多详细信息,但要点是StrongApplicative为仿函数建​​模了一些“固定大小”的概念。该类型类与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

argument by @Daniel Wagner显示StrongApplicativeRepresentable的概括,因为每个RepresentableStrongApplicative。尚不清楚是否有StrongApplicativeRepresentable之外的Traversable

现在,我们知道Applicative是用StrongApplicative来表示的,并且朝一个方向运行。由于Applicative使Traversable的松弛趋于同构,所以也许我们想利用这一额外的等价来颠倒class Functor f => Something f where unsequence :: StrongApplicative f => f (t a) -> t (f a) 所提供的分配定律:

(->) a

碰巧StrongApplicativeRepresentable,实际上是StrongApplicative inject属的一个代表性标本(如果您原谅双关语)函子。因此,我们可以将您的promote / promote :: Something f => (a -> f b) -> f (a -> b) promote = unsequence 操作写为:

StrongApplicative

我们之前提到Representativeunsequence函子族的超类。通过检查unsequence的类型,很明显,我们对多态应用程序施加的约束越强,实现Functor f => Applicative f => StrongApplicative f => Representable x f 越容易(因此,所得类的实例越多)。

因此,从某种意义上说,存在着“可逆”函子的层次结构,它与应用效果层次结构的流动方向相反,您可能希望对它们进行横越。 “内部”函子的层次结构如下:

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) 。这种分布的,可表示的(因而是刚性的)函子的一个例子是成对的函子:

Representable x f

当然,如果您强烈要求多态的“内部函子”,例如RDistributive中的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) ,则可能发生以下情况:

sequence . unsequence = id

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

正如我说过的,我并没有非常仔细地考虑它,所以也许这里的那些对刚性函子概念投入了一些思考的人会立即在其中挖洞。或者,也许它使事情落在我看不见的地方。

可能有必要考虑一下这些未遍历类型类的一些法律。函子同时支持unsequence . sequence = idTraversable的地方显然是一个Untraverse和{{1}}本身。

值得一提的是,函子的“分布定律”与单子和共母之间的相互作用已经得到了很好的研究,因此可能与您帖子中与单子有关的讨论有关。

相关问题