2

如何定义一个我们可以添加“功能”而不会遇到重叠实例的环境?

假设我们有以下数据类型和类型类:

type Name = String

data Fruit = Orange | Pear | Apple

data Vegetable = Cucumber | Carrot | Spinach

data Legume = Lentils | Chickpeas | BlackEyedPeas

class HasFruit e where
    getFruit :: e -> Name -> Maybe Fruit

class HasVegetable e where
    getVegetable :: e -> Name -> Maybe Vegetable

class HasLegume e where
    getLegume :: e -> Name -> Maybe Legume

现在我们想定义几个需要环境中某些成分的函数:

data Smootie

mkSmoothie :: (HasFruit e, HasVegetable e) => e -> Smootie
mkSmoothie = undefined

data Salad

mkSalad :: (HasVegetable e, HasLegume e) => e -> Salad
mkSalad = undefined

我们定义了一些实例Has*

instance HasFruit [Fruit] where
    getFruit = undefined

instance HasVegetable [Vegetable] where
    getVegetable = undefined

instance HasLegume [Legume] where
    getLegume = undefined

最后,我们想定义一个准备冰沙和沙拉的函数:

cook :: (Smootie, Salad)
cook = let ingredients = undefined in
    (mkSmoothie ingredients, mkSalad ingredients)

现在第一个问题是,将什么作为成分传递给上面定义的实例可以使用?我的第一个解决方案是使用元组:

instance HasFruit e0 => HasFruit (e0, e1, e2) where
    getFruit (e0, _, _) = getFruit e0

instance HasVegetable e1 => HasVegetable (e0, e1, e2) where
    getVegetable (_, e1, _) = getVegetable e1

instance HasLegume e2 => HasLegume (e0, e1, e2) where
    getLegume (_, _, e2) = getLegume e2

cook :: (Smootie, Salad)
cook = let ingredients = ([Orange], [Cucumber], [BlackEyedPeas]) in
    (mkSmoothie ingredients, mkSalad ingredients)

这虽然很麻烦,但很有效。但是现在假设我们决定添加一个mkStew,这需要一些HasMeat实例。然后我们必须更改上面的所有实例。此外,如果我们想单独使用mkSmothie,我们不能直接通过([Orange], [Cucumber]),因为没有为它定义实例。

我可以定义:

data Sum a b = Sum a b

和实例:

instance HasFruit e0 => HasFruit (Sum e0 e1) where
    getFruit (Sum e0 _) = getFruit e0

instance HasVegetable e1 => HasVegetable (Sum e0 e1) where
    getVegetable (Sum _ e1) = getVegetable e1

instance HasLegume e1 => HasLegume (Sum e0 e1) where
    getLegume (Sum _ e1) = getLegume e1

但是以下方法不起作用(没有实例HasVegetable [Legume]):

cook1 :: (Smootie, Salad)
cook1 = let ingredients = Sum [Orange] (Sum [Cucumber] [BlackEyedPeas]) in
    (mkSmoothie ingredients, mkSalad ingredients)

而且这个实例会重叠!

instance HasVegetable e0 => HasVegetable (Sum e0 e1) where
    getVegetable (Sum e0 e1) = getVegetable e0

有没有办法以优雅的方式解决这个问题?

4

1 回答 1

3

当前Sum实例的问题是我们不知道我们正在寻找的对象是在左边还是在右边。

这是计划:环境的每个组件都应该声明它提供的功能,以便我们可以搜索它。

这个答案的要点

声明能力

随着环境的组合,我们将需要一个(类型级别的)数据结构来承载不同部分的功能。我们将使用二叉树,因此我们可以保留组件的结构。

-- Tree of capabilities (ingredient categories)
data Tree a = Leaf a | Node (Tree a) (Tree a)

与环境关联的功能通过此类型族声明。

type family Contents basket :: Tree *

type instance Contents [Fruit] = 'Leaf Fruit
type instance Contents [Vegetable] = 'Leaf Vegetable
type instance Contents [Legume] = 'Leaf Legume

-- Pair of environments
data a :& b = a :& b  -- "Sum" was confusing

-- The capabilities of a pair are the pair of their capabilities.
type instance Contents (a :& b) = 'Node (Contents a) (Contents b)

-- e.g., Contents ([Fruit] :& [Vegetable]) = 'Node ('Leaf Fruit) ('Leaf Vegetable)

查找能力

就像开头提到的,当遇到 pair 时:&,我们需要判断是在 left 还是 right 组件中找到能力。因此,我们从一个函数(在类型级别)开始,True如果可以在树中找到该功能,则该函数返回。

type family In (x :: *) (ys :: Tree *) :: Bool where
  In x (Leaf y) = x == y
  In x (Node l r) = In x l || In x r

type family x == y :: Bool where
  x == x = 'True
  x == y = 'False

Has班级_

这个类现在有一个超类约束:我们正在寻找的能力确实是可用的。

class (In item (Contents basket) ~ 'True)
      => Has item basket where
  get :: basket -> Name -> Maybe item

这似乎是多余的,因为如果找不到该功能,实例解析无论如何都会失败,但是精确的超类约束有好处:

  • 防止错误:如果缺少某些东西,编译器会更早地抱怨;

  • 一种文档形式,通知我们何时可能存在实例。

叶实例

instance Has Fruit [Fruit] where
  get = (...)

instance Has Vegetable [Vegetable] where
  get = (...)

instance Has Legume [Legume] where
  get = (...)

我们不需要编写像Has Fruit [Vegetable];这样的可疑实例。我们实际上不能:它们会与超类约束相矛盾。

实例(:&)

我们需要推迟到一个新的类,PairHas它将区分In两边谓词的结果,以确定放大环境的哪个部分。

instance PairHas item a b (In item (Contents a)) (In item (Contents b))
         => Has item (a :& b) where
  get = getPair

同样,我们使超类约束对PairHas. inAandinB只能分别用In item (Contents a)and实例化In item (Contents b),它们的析取应该是True,意思是item至少可以在其中一个中找到。

class ( In item (Contents a) ~ inA
      , In item (Contents b) ~ inB
      , (inA || inB) ~ 'True)
      => PairHas item a b inA inB where
  getPair :: (a :& b) -> Name -> Maybe item

当然,我们有两个实例分别向左和向右移动,使用递归Has约束(注意,Has通过它自己的超类约束提供一个相等性)。

instance ( Has item a
         , In item (Contents b) ~ 'False)
         => PairHas item a b 'True 'False where
  getPair (a :& _) = get a

instance ( In item (Contents a) ~ 'False
         , Has item b)
         => PairHas item a b 'False 'True where
  getPair (_ :& b) = get b

如果双方能力相同怎么办?我们将认为这是一个错误,并要求用户通过其他机制显式隐藏其中一项重复功能。我们可以使用TypeError在编译时打印自定义错误消息。我们也可以默认选择任何一方。

instance (TypeError (Text "Duplicate contents")  -- can be more descriptive
         , In item (Contents a) ~ 'True
         , In item (Contents b) ~ 'True)
         => PairHas item a b 'True 'True where
  getPair = undefined

我们还可以为双方都为假的情况编写自定义错误消息。这有点令人惊讶,因为这与超类约束相矛盾(inA || inB) ~ 'True,但该消息确实会被打印出来,所以我们不会抱怨。

instance ( TypeError (Text "Not found")  -- can be more descriptive
         , In item (Contents a) ~ 'False
         , In item (Contents b) ~ 'False
         , 'False ~ 'True)
         => PairHas item a b 'False 'False where
  getPair = undefined

让我们做饭

现在我们可以安全地编写cook

cook :: (Smootie, Salad)
cook = let ingredients = [Orange] :& [Cucumber] :& [BlackEyedPeas] in
  (mkSmootie ingredients, mkSalad ingredients)

如果您复制或忘记某些成分,您还可以查看会发生什么

cook :: (Smootie, Salad)
cook = let ingredients = [Orange] :& [Cucumber] :& [BlackEyedPeas] :& [Pear] in
  (mkSmootie ingredients, mkSalad ingredients)

-- error: Duplicate contents

cook :: (Smootie, Salad)
cook = let ingredients = [Orange] :& [Cucumber] in
  (mkSmootie ingredients, mkSalad ingredients)

-- error: Not found
于 2018-03-29T01:08:55.653 回答