在考虑如何推广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)同构。
答案 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仿函数的基本反例是Maybe
和List
。这些是具有多个构造函数的仿函数:通常,这样的仿函数不是刚性的。
为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'
。我们现在假设给出了q
和f'
。我们想构建类型
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
关于我的措词的一些评论:
我将inject
和eject
的名称更改为fflip
和flap
,主要是因为在我看来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
的重要性,但我并没有想到作为另一堂课的一部分可能有意义。 extractors
和fflip
是可互定义的,例如,可以为搜索/选择单子写出一个相当简洁的实例:
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
没有遵循分配律。
(除了:infuse
在distribute
情况下不是合法的Sel
可以通过基数论证来确定。如果infuse
遵循分配律,我们将为任意两个刚性函子使用infuse . infuse = id
。但是,类似infuse @((->) Bool) @(Sel r)
的结果会导致结果类型的居民少于参数类型;因此,它不可能有左逆。)
在这一点上,有必要使我们清楚地了解Distributive
与Rigid
的区别。假设您的定律是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
如果g
是Rigid
,我们可以朝另一个方向前进,并从停权中恢复函数值:
-- 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
函子的Rigid
和Distributive
都可以通过evert
和revert
进行路由:
-- 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
。用这样的术语,分配律等于要求evert
和revert
是逆的:
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 b
到g (a -> b)
的正方形逆时针旋转的方式等于fflip
(请参见上面fflipEv
的定义)。由于逆时针方式是由三个同构构成的,因此fflip
具有反方向。由于flap
是其左逆(根据严格的定律),因此它也必须是其逆。因此fflip . flap = id
。
其次,假设fflip . flap = id
。同样,从a -> g b
到g (a -> b)
的逆时针方向是fflip
,但是现在我们知道它具有相反的含义,即flap
。因此,组成逆时针方向的每个函数都必须具有逆函数。特别地,revert
必须具有反函数。由于evert
是其右逆(根据严格的定律),因此它也必须是其逆。因此,evert . revert = id
。
以上结果使我们可以精确地确定Rigid
相对于Distributive
的位置。刚性函子是一个可能的分布,只是它仅遵循分布的恒律,而不遵循构成的恒律。以fflip
为逆,使flap
同构,等于将Rigid
升级为Distributive
。
从一元论的角度来看fflip
和flap
,我们可以说刚性单子具有从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
有用。 “共刚性”伪遍历的一个示例是一种具有遍历效果的遍历的数据结构,该遍历可以复制效果,但是在重建应用层下的结构时会丢弃相应的重复元素,从而遵循标识法则-而不是组成一个。
说到Traversable
,Rigid
的结构本身就很有意义:它是自由分布函子的编码。特别是,Traversable
和Rigid
在免费的monad上可以与Understanding Idiomatic Traversals Backwards and Forwards和liftF
进行比较,并且在其他的免费结构上可以与类似的功能进行比较。 (在这种情况下,revert
与Ev
完全相反,表明evert
有多强。缩回在某些情况下更常见,因为它发生在revert
的一般情况。)
最后但并非最不重要的是,还有另一种理解revert
的方法:这意味着多态提取器类型在retract
的意义上代表{{ 1}}对应于evert
,而Distributive
对应于Rigid
。不幸的是,量化使得在Haskell中使用实际的Ev
接口来表达它非常尴尬。 (这是有症状的,我必须让evert
赋予index
其自然的revert
和tabulate
实例。)如果它充当安慰,那将不是一个安慰。无论如何,这都是非常实用的表示形式:如果我已经有了一个多态提取器,则实际上不需要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显示StrongApplicative
是Representable
的概括,因为每个Representable
是StrongApplicative
。尚不清楚是否有StrongApplicative
个Representable
之外的Traversable
。
现在,我们知道Applicative
是用StrongApplicative
来表示的,并且朝一个方向运行。由于Applicative
使Traversable
的松弛趋于同构,所以也许我们想利用这一额外的等价来颠倒class Functor f => Something f
where
unsequence :: StrongApplicative f => f (t a) -> t (f a)
所提供的分配定律:
(->) a
碰巧StrongApplicative
是Representable
,实际上是StrongApplicative
inject
属的一个代表性标本(如果您原谅双关语)函子。因此,我们可以将您的promote
/ promote :: Something f => (a -> f b) -> f (a -> b)
promote = unsequence
操作写为:
StrongApplicative
我们之前提到Representative
是unsequence
函子族的超类。通过检查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 = id
和Traversable
的地方显然是一个Untraverse
和{{1}}本身。
值得一提的是,函子的“分布定律”与单子和共母之间的相互作用已经得到了很好的研究,因此可能与您帖子中与单子有关的讨论有关。