为什么Setter有Traversable约束?

6
当你查看lens-family-core包中的setting时,你会发现它的类型是Identical f => ((a -> b) -> s -> t) -> LensLike f s t a b,而Identical被定义为class (Traversable f, Applicative f) => Identical f
我理解它需要ApplicativeIdentical,因为它使用了pureextract,但我不确定为什么它还需要Traversable
setting :: Identical f => ((a -> b) -> s -> t) -> LensLike f s t a b
setting sec f = pure . sec (extract . f)

我还发现Setterlens包中通过Settable有一个Traversable约束。所以我猜一个setter通常需要Traversable
为什么一个setter通常需要Traversable
2个回答

4

设置器实际上不需要 Traversable,但是你仍然强制执行这个约束,因为 Identical / Settable 是只允许微不足道的实例的类。也就是说,要么是 Identity 或者同构于它的东西,而所有这些显然也都是可遍历的。

请注意,你总是可以编写

idenTraverse :: (Identical t, Applicative g) => (a -> g b) -> t a -> g (t b)
idenTraverse f = fmap pure . f . extract

这等价于标准的 traverse。(实际上这只需要 Functor g,顺带一提。)

明确要求标准的 Traversable 接口可以构建更干净、更一致的层次结构,而不会真正改变您使用设置器的能力或限制。


4

无论是来自lensSettable还是来自lens-familyIdentical实例都必须与Identity同构。(关于为什么必须这样,请参见hao对Control.Lens.Setter是否将类型包装在函子中是多余的?的回答,特别是其中最后的警告。)现在,如果我们的目标仅仅是确保一个函子与Identity同构,我们不一定需要为此设置一个全新的类只是为了这个目的,因为以下约束就足够了:

type Setter s t a b = forall f. (Distributive f, Lone f) => (a -> f b) -> s -> f t

在这里,Lone 是一个子类的 Traversable,用于持有恰好一个值的函子,它不依赖于 Applicative
class Traversable f => Lone f where
    sequenceL :: Functor m => f (m a) -> m (f a)
    traverseL :: Functor m => (a -> m b) -> f a -> m (f b)

分配函子与函数同构,孤立函子与对同构。如果一个函子既是分配的又是孤立的,那么它必须同构于函数函子和对函子。这只有在函数的参数类型和对的固定类型成分都是(同构于)()的情况下才可能,这又意味着该函子必须同构于Identity

在这种方式中为Setter安排约束并不陌生于在lens中看到的光学类型同义词的风格。事实上,它会让人想起Getter如何依赖于FunctorContravariant的组合来确保它使用的函子对其参数是虚拟的。然而,一个不方便之处是在生态系统中没有Lone的规范实现。由于lens无论如何都必须定义该类,最好设置一个特殊目的的类,比如Settable,这样做的好处是使签名和类型错误稍微更加自解释。
鉴于“Settable”实质上是“Distributive”和“Lone”的替代品,将其作为“Distributive”和“Traversable”的子类是有道理的。这样做可以明确该类的来源,其中“Lone”只是离“Traversable”更进一步的限制。虽然“lens-family-core”省略了“Distributive”约束(可能是为了避免依赖“distributive”包),但我们可以从对“Identical”定义的注释中感受到类似的动机。
-- It would really be much better if comonads was in tranformers
class (Traversable f, Applicative f) => Identical f where
  extract :: f a -> a
孤立函子都是共函子,而在原则上,给Identical添加一个Comonad超类将允许不重新定义extract,同时突出了Identical函子的不同方面。
最后,有几点需要注意。首先,你可能已经注意到我到目前为止还没有提到Applicative约束。尽管在实践中,这个约束对于使pure在setting的实现中可用是必要的,但从某种意义上说,它并不像其他约束那样重要。分配函子必然是applicative的,并且Distributive之所以没有Applicative作为超类,仅仅是因为(<*>)在分配接口中表达起来很笨拙。另一方面,pure本身可以直接根据其自身的术语进行实现:
pureD :: Distributive g => a -> g a
pureD = cotraverse getConst . Const 

其次,值得一提的是,有一种更简洁的方法来指定一个Functor(即一个Hask自函子)必须与Identity同构:使用the Adjunction class来说明它必须是自己的伴随。因此,Setter可能会变成这样:
type Setter s t a b = forall f. Adjunction f f => (a -> f b) -> s -> f t

(顺便说一下,“分配”函子恰好是正确的伴随自函子,而“孤立”函子则恰好是左伴随的。)
然而,这个约束远非显而易见,并且“伴随”并不是一个广泛使用的类别,所以放弃“可设置”的选择并转而选择它似乎并不是一个非常有吸引力的选项。

3
你在我实际提问之前就回答了我计划中的大部分问题。谢谢! - snak
3
你在我实际提问之前就回答了我计划要问的大部分问题。谢谢! - snak
3
你在我实际提问之前就回答了我计划问的大部分问题。谢谢! - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接