为了更好地表达术语,我建议将这些函数子称为“刚性”而不是“可绑定”的。下面将解释称之为“刚性”的动机。
定义
如果一个函子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
"非退化性"定律必须得到遵守:
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
具有刚性特性的函子与具有单子特性的函子无法比较(既不强于也不弱于):如果一个函子具有刚性特性,则似乎不能自动推出它是单子特性(尽管我不知道此情况的具体反例)。如果一个函子具有单子特性,则不能推出它具有刚性特性(存在反例)。
不具有刚性特性的单子函子的基本反例是Maybe和List。这些是具有多个构造函数的函子:这样的函子不能是刚性的。
实现Maybe的inject函数的问题在于,inject必须将类型为a->Maybe b的函数转换为Maybe(a -> b) ,而Maybe有两个构造函数。类型为a->Maybe b的函数可以针对a的不同值返回不同的构造函数。然而,我们应该构造一个类型为Maybe(a -> b)的值。如果对于某些a,给定的函数产生Nothing,则我们没有b,因此无法产生完全函数a->b 。因此,我们不能返回Just(a->b);只要给定的函数对于任何一个a的值产生Nothing,我们就被迫返回Nothing。但是,我们无法检查类型为a->Maybe b的给定函数是否对所有a的值产生Just(...)。因此,在所有情况下,我们都被迫返回Nothing。这将不符合非退化性法则。
因此,我们只有在f t是具有“固定形状”的容器(仅具有一个构造函数)时才能实现inject。因此称为“刚性”。
另一个解释为什么刚性比单子更严格的方法是考虑自然定义的表达式。
(inject id) :: f(f a -> a)
id :: f a -> f a
表示我们可以对任何类型a
进行f-代数f a -> a
,只要它被包含在f
中。并不是所有的monad都有代数;例如,各种“future”monad以及IO
monad描述了类型为f a
的计算,这些计算不允许我们提取类型为a
的值——即使它们被包装在f
-容器中,我们也不应该能够拥有类型为f a -> a
的方法。这表明“future”monads和IO
monad不是刚性的。
比刚性更严格的一个特性是来自E. Kmett的软件包之一的distributivity。如果我们可以交换任何functor p
的顺序,就像p (f t) -> f (p t)
一样,则functor f
是分配的。刚性与仅与“reader” functor r t = a -> t
相关的顺序互换相同。因此,所有分配的functors都是刚性的。
所有分配的functors必须是可表示的,这意味着它们等价于具有某个固定类型c
的“reader” functor c -> t
。然而,并非所有刚性的functors都是可表示的。一个例子是由下列定义的functor g
:
type g t = (t -> r) -> t
函子g
与固定类型c
的c -> t
不等价。
构造和示例
更多非可表示(即非“分配”的)刚性函子的示例包括形如a t -> f t
的函子,其中a
是任意反变函子,f
是刚性函子。此外,笛卡尔积和两个刚性函子的组合仍然是刚性的。通过这种方式,我们可以在指数-多项式类别中产生许多刚性函子的示例。
我的回答What is the general case of QuickCheck's promote function?也列出了刚性函子的构造:
f = Identity
- 如果
f
和g
都是刚性的,则函子乘积h t = (f t, g t)
也是刚性的
- 如果
f
和g
都是刚性的,则组合h t = f (g t)
也是刚性的
- 如果
f
是刚性的且g
是任何反变函子,则函子h t = g t -> f t
是刚性的
刚性函子的另一个特性是类型r()
等价于()
,即类型r()
只有一个不同的值。这个值是point()
,其中point
对于任何刚性函子r
都被定义在上面。这表明刚性函子必须只有一个构造函数。这立即表明Maybe
、Either
、List
等不能是刚性的。
与单子的联系
如果f
是具有“外部组合”型单子转换器t m a = f (m a)
的单子,则f
是刚性函子。
“刚性单子”可能是刚性函子的一个子集,因为构造4仅在f
也是刚性单子而不是任意刚性函子时才产生刚性单子(但反变函子g
仍然可以是任意的)。但是,我没有任何刚性函子的例子,它不是单子。
最简单的刚性单子例子是
type r a = (a -> p) -> a
,也称为“搜索单子”。(这里的
p
是一个固定类型。)
为了证明具有“外部组合”变换器
t m a = f(m a)
的单子
f
也具有
inject
方法,我们考虑将外部单子
m
选择为读取器单子,
m a = r -> a
。然后,正确类型签名的函数
inject
定义如下:
inject = join @t . return @r . (fmap @m (fmap @f return @m))
通过适当选择类型参数,可以实现非退化规律。
非退化规律源于t
的自然性:将单子态态射m→Identity
(将类型r
的值替换为读取器中的值)提升为单子态态射t m a→t Id a
。此证明的详细信息略去。
使用案例
最后,我找到了两个刚性函子的使用案例。
第一个用例是考虑刚性函子的原始动机:我们希望一次返回多个单子结果。如果 m
是单子态,且我们想要像问题中所示的那样拥有 fbind
,则需要 f
是刚性的。然后,我们可以实现以下的 fbind
fbind :: m a -> (a -> f (m b)) -> f (m b)
fbind ma afmb = fmap (bind ma) (inject afmb)
我们可以使用
fbind
来进行单子操作,返回多个单子结果(或者更普遍地说,是一个刚性函子的单子结果集合),对于任何单子
m
。
第二种用例源于以下考虑。假设我们有一个程序
p :: a
,它在内部使用函数
f :: b -> c
。现在,我们注意到函数
f
非常慢,我们想通过将
f
替换为单子“future”或“task”,或者通常情况下替换为某个单子
m c
的Kleisli箭头
f' :: b -> m c
来重构程序。当然,我们期望程序
p
也变成单子形式:
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
原来只有在
m
是刚性函子的情况下才能构造
refactor
。在试图实现
refactor
时,我们发现与我们尝试为
Maybe
实现
inject
时基本相同的问题:我们得到一个函数
f' :: b -> m c
,它可以为不同的
b
返回不同的单调效果
m c
,但我们需要构造
m a
,它必须代表所有
b
的相同单调效果。如果
m
是具有多个构造函数的单子,则无法工作。
如果
m
是刚性的(我们不需要要求
m
是单子),我们可以实现
refactor
:
refactor bca bmc = fmap bca (inject bmc)
如果
m
不是刚性的,我们就无法重构任意程序。到目前为止,我们已经知道延续单子是刚性的,但“类似于未来”的单子和
IO
单子不是刚性的。这再次表明,刚性在某种程度上是比单子性更强的属性。
F (a->b) -> a -> F b
都成立,而不仅仅是可遍历的函子。 - winitzkiinject id :: IO (IO a -> a)
的东西。那将是非常危险的,实际上为纯代码提供对unsafePerformIO
的访问,只要最终从IO中调用它就可以了(因为我们是从“main”开始的)。基本上,我们只需要做main = do upIO <- inject id ; print (pureF upIO 12) ; ...
就可以让纯类型的pureF
运行副作用。这很可怕。 - chiIdentity
和(->) a
。因此,我怀疑这比Monad
要强得多。 - Jonathan CastDistributive f
可以让你使用distribute :: Functor g => g (f a) -> f (g a)
函数,从Haskell类型的角度来看比你的函数更强大。我不知道它是否满足您的范畴论法则。每个“可表示”functor都是分配的,并且文档中指出,反过来也成立数学上。 - dfeuer