【问题标题】:DataKind Unions数据种类联盟
【发布时间】:2019-02-18 16:53:27
【问题描述】:

我不确定这是否是正确的术语,但是否可以声明接受数据类型“联合”的函数类型?

例如,我知道我可以做到以下几点:

{-# LANGUAGE DataKinds      #-}
{-# LANGUAGE GADTs          #-}

...

data Shape'
  = Circle'
  | Square'
  | Triangle'

data Shape :: Shape' -> * where
  Circle :: { radius :: Int} -> Shape Circle'
  Square :: { side :: Int} -> Shape Square'
  Triangle
    :: { a :: Int
       , b :: Int
       , c :: Int}
    -> Shape Triangle'

test1 :: Shape Circle' -> Int
test1 = undefined

但是,如果我想采用圆形或方形的形状怎么办?如果我还想为一个单独的函数采用所有形状怎么办?

有没有办法让我定义一组Shape' 来使用,或者让我允许每个数据有多个数据类型定义?

编辑:

工会的使用似乎不起作用:

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds       #-}
{-# LANGUAGE GADTs           #-}
{-# LANGUAGE KindSignatures  #-}
{-# LANGUAGE PolyKinds       #-}
{-# LANGUAGE TypeFamilies    #-}
{-# LANGUAGE TypeOperators   #-}

...

type family Union (a :: [k]) (r :: k) :: Constraint where
  Union (x ': xs) x = ()
  Union (x ': xs) y = Union xs y

data Shape'
  = Circle'
  | Square'
  | Triangle'

data Shape :: Shape' -> * where
  Circle :: { radius :: Int} -> Shape Circle'
  Square :: { side :: Int} -> Shape Square'
  Triangle
    :: { a :: Int
       , b :: Int
       , c :: Int}
    -> Shape Triangle'

test1 :: Union [Circle', Triangle'] s => Shape s -> Int
test1 Circle {} = undefined
test1 Triangle {} = undefined
test1 Square {} = undefined

上面部分编译

【问题讨论】:

    标签: haskell data-kinds


    【解决方案1】:

    您可以(我认为)使用类型族以及ConstraintKindsPolyKinds 以相当干净的方式完成类似的事情:

    type family Union (a :: [k]) (r :: k) :: Constraint where
      Union (x ': xs) x = ()
      Union (x ': xs) y = Union xs y
    
    test1 :: Union [Circle', Triangle'] s => Shape s -> Int
    test1 = undefined
    

    上面的() 是空约束(它就像一个类型类约束的空“列表”)。

    类型族的第一个“方程式”利用了类型族中可用的非线性模式匹配(它在左侧使用了两次x)。类型族还利用了这样一个事实,即如果没有一个案例匹配,它不会给你一个有效的约束。

    您还应该能够使用类型级别的布尔值而不是 ConstraintKinds。这会有点麻烦,我认为最好避免在此处使用类型级别的布尔值(如果可以的话)。

    旁注(我不记得了,我不得不查找这个答案):您可以通过从GHC.Exts 导入Constraint 在范围内。

    编辑:部分禁止无法访问的定义

    这是一个修改,使其(部分)禁止无法访问的定义以及无效调用。它稍微迂回一些,但似乎可行。

    修改Union 以提供* 而不是约束,如下所示:

    type family Union (a :: [k]) (r :: k) :: * where
      Union (x ': xs) x = ()
      Union (x ': xs) y = Union xs y
    

    类型是什么并不重要,只要它有一个你可以模式匹配的居民,所以我返回()类型(单位类型)。

    这就是你将如何使用它:

    test1 :: Shape s -> Union [Circle', Triangle'] s -> Int
    test1 Circle {}   () = undefined
    test1 Triangle {} () = undefined
    -- test1 Square {} () = undefined -- This line won't compile
    

    如果您忘记匹配它(例如,如果您在 () 构造函数上放置一个变量名称,如 x 而不是匹配),则可能会定义一个无法到达的案例。但是,当您实际尝试处理这种情况时,它仍然会在调用站点出现类型错误(因此,即使您在 Union 参数上不匹配,调用 test1 (Square undefined) () 也不会进行类型检查)。

    请注意,Union 参数似乎必须在 Shape 参数之后才能使其工作(无论如何,完全如所述)。

    【讨论】:

    • 很酷的技术!我不知道你可以在这样的类型族中进行非线性匹配。
    • 虽然,x ~ x 不是多余的,因为我们已经处于x 的非线性模式中了吗?似乎() 应该也能正常工作。
    • @luqui 我也这么认为,但如果你使用 () 而不是 x ~ x 出于某种原因它不再阻止你在你的函数中放置无法访问的模式匹配。此外,您必须在模式匹配中具有显式类型注释才能进行“无法访问”检查(使用x ~ x)。我也不确定为什么会这样(可能是 GHC 错误?)。
    • 澄清一点:通过“模式匹配中的显式类型注释”,我的意思是:test2 (Square s :: Shape Square') = ...。而且,就像我说的那样,我不确定这里为什么需要这样做(而且,对我来说,这似乎是某种 GHC 错误的结果)。
    • 哦,我刚刚意识到我有一个旧版本的 GHC (8.4.3)。如果我升级到最新的(8.6.3),那么x ~ x 不再重要(可以替换为()),但由于某种原因,显式类型注释仍然是必要的。
    【解决方案2】:

    这有点糟糕,但我想您可能需要使用 Data.Type.Equality 证明它是圆形还是方形:

    test1 :: Either (s :~: Circle') (s :~: Square') -> Shape s -> Int
    

    现在用户必须给出一个额外的参数(一个“证明术语”)来说明它是哪一个。

    事实上,您可以使用证明术语的想法来“完成”bradm 的解决方案,其中:

    class MyOpClass sh where
        myOp :: Shape sh -> Int
        shapeConstraint :: Either (sh :~: Circle') (sh :~: Square')
    

    现在没有人可以再添加任何实例(除非他们使用undefined,否则会很不礼貌)。

    【讨论】:

    • 此时,这与创建另一个数据类的构造函数允许形状子集不一样吗?我猜的好处是函数类型仍然接受Shape。重复我在另一篇文章中添加的评论,我主要使用它来限制其他数据类型的输入(例如,一种类型只能采用圆形或三角形)。我的 haskell 经验有限,但有没有办法使用类型类指定该约束?
    • @AllanW 是的,例如,您可以在 GADT 中使用一个类。 data Scene where BasicShape :: MyOpClass s => Shape sh -> Scene.
    • @luqui 使用类型类似乎不是 GADT 的正确解决方案。我更喜欢通过定义 data Scene where BasicShape :: Either (s :~: Circle') (s :~: Square') -> Shape sh -> Scene 来“折叠”间接性,因此直接提供见证。
    • @bradm,是的,我可能同意。老实说,这整个问题看起来像是一堆跳铁。我会寻找替代设计...
    • 为什么不Either (Shape Circle') (Shape Square') -> Int
    【解决方案3】:

    你可以使用类型类:

    class MyOpClass sh where
        myOp :: Shape sh -> Int
    
    instance MyOpClass Circle' where
        myOp (Circle r) = _
    
    instance MyOpClass Square' where
        myOP (Square s) = _
    

    我觉得这不是一个特别“完整”的解决方案 - 任何人都可以返回并添加另一个 instance MyOpClass Triangle' - 但我想不出任何其他解决方案。但是,您可能只需不导出类型类即可避免此问题。

    【讨论】:

    • 我的问题主要源于定义其他数据类型。例如,假设我有一个新的数据类,它的构造函数采用圆形或方形。如何使用 typeclass 指定?
    • 我不太明白你的意思。您的意思是要定义一个可以有Shape Circle'Shape Square' 字段但不能有Shape Triangle' 的数据类型?
    • 哦,刚刚看到你对@luqui's answer的评论解释了它。没关系。
    【解决方案4】:

    我注意到的另一个解决方案虽然很冗长,但它是创建一种具有特征布尔值列表的种类。然后,您可以在限制类型时对特征进行模式匹配:

    -- [circleOrSquare] [triangleOrSquare]
    data Shape' =
      Shape'' Bool
              Bool
    
    data Shape :: Shape' -> * where
      Circle :: { radius :: Int} -> Shape (Shape'' True False)
      Square :: { side :: Int} -> Shape (Shape'' True True)
      Triangle
        :: { a :: Int
           , b :: Int
           , c :: Int}
        -> Shape (Shape'' False True)
    
    test1 :: Shape (Shape'' True x) -> Int
    test1 Circle {}   = 2
    test1 Square {}   = 2
    test1 Triangle {} = 2
    

    这里,Triangle 会匹配失败:

        • Couldn't match type ‘'True’ with ‘'False’
          Inaccessible code in
            a pattern with constructor:
              Triangle :: Int -> Int -> Int -> Shape ('Shape'' 'False 'True),
            in an equation for ‘test1’
        • In the pattern: Triangle {}
          In an equation for ‘test1’: test1 Triangle {} = 2
       |
    52 | test1 Triangle {} = 2
       |       ^^^^^^^^^^^
    

    很遗憾,我认为你不能把这个写成记录,这样可能会更清晰,避免特征的排序。

    这可能与类示例结合使用以提高可读性。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-10-04
      • 2015-10-15
      • 1970-01-01
      • 1970-01-01
      • 2013-08-24
      • 2013-03-01
      • 2021-12-28
      • 2011-03-19
      相关资源
      最近更新 更多