ADT是否有明确的子类型名称?

时间:2016-04-17 03:25:51

标签: haskell functional-programming ocaml

我正在寻找将ADT与显式子类型结合起来的数据类型的正确名称。

在我的一个应用程序中,我使用类似于ADT的结构来表示解析树,我在其上执行递归模式匹配。如果我能将ADT与子类型结合起来,我觉得相当方便,如下例所示:

注意:该示例是用Haskell的语法编写的,但这不是Haskell代码。

data Empty = Empty
data Expr = Int Int | Add Expr AddOp Expr
data OptionalExpr =
| Empty  // I want to make Empty a subtype of OptionalExpr
| Expr   // I want to make Expr  a subtype of OptionalExpr

在上面的例子中,我首先定义了两种类型:Empty和Expr。然后我将这两种类型作为OptionalExpr的子类型。我意识到这种数据类型是 uncommon 。显然,Haskell和OCaml都不支持它。但我不了解其他功能语言。

我正在寻找将ADT与显式子类型相结合的东西,而不是像多态变体那样结构暗示的子类型。这个要求有一些理由:

  • 首先,我们想要全有或没有子类型。假设我们希望A成为B的子类型,那么我们永远想要仅包括B中的A的一些变体。要么A是B的子类型,在这种情况下B包括所有的A或A的变体不是B的子类型,在这种情况下B不包括A的变体。我们不允许它们之间的灰色区域。
  • 其次,我们不希望B在任何意义上都是开放的。我们考虑到了一组非常具体的B子类型。我们不想通过实现类型类等来想要成为B的实例。
  • 第三,说类型A有很多变种。我们希望将B类型作为A的超类型。将所有变量复制到B中,正如多态变体所要求的那样,它太麻烦且容易出错。
  • 第四,当我们想要表达的只是一个子类型时,我们不想引入新的值构造函数。在上面的示例中,我们可以将OptionalExpr编写为具有2个值构造函数的ADT,如下所示:data OptionalExpr = EmptyExpr | NonEmptyExpr Expr,或者我们可以使用Maybe,但在我的应用程序中这是不可接受的,因为级别嵌入可能非常深,解构一个深层嵌入的值如(L1 (L2 (L3 (L4 (L5 value_wanted)))))将是一场噩梦。

为了让您了解为何存在此类要求,我将在下面展示一个更具体的示例:

PrimaryExpr = ID | LeftParen Expr RightParen
UnaryExpr = PrimaryExpr | NegateOp PrimaryExpr    // -
MultExpr  = UnaryExpr | MultExpr MultOp UnaryExpr // *
AddExpr   = MultExpr  | AddExpr  AddOp MultExpr   // +
CompExpr  = AddExpr | AddExpr CompOp AddExpr
Expr = CompExpr

上面的示例表示子类型层次结构,并表示诸如AddExpr之类的想法是CompExpr,但CompExpr不是AddExpr。对于这个具体的例子,有些人向我建议我可以用Expr替换UnaryExpr,MultExpr,AddExpr等。也就是说,我可以将所有类型定义为单一类型。丢失类型约束,例如CompExpr不是AddExpr,因为我在这些类型上进行递归模式匹配,我需要静态强制执行此层次结构的约束。

我在文献中寻找的这种数据类型是否有名称?或者我在寻找一些甚至没有意义的东西?如果您认为是这种情况,为什么我要找一些荒谬的东西?谢谢你的任何指示。

编辑:尽管我已经用Haskell的语法编写了上面的代码片段,但我没有在Haskell中编写我的应用程序。我使用自己的语言和自己的数据类型,因此我不受Haskell语义的限制。我正在寻找指向文学中类似概念的指针,因此当我为我的项目撰写报告时,我似乎并没有重新发明新的东西。我尝试了所有我能想到的谷歌关键词,没有任何权利被归还,所以我在这里问。

4 个答案:

答案 0 :(得分:3)

在评论中,您说:

  

我不确定如何使用GADT对子类型层次结构进行编码。如果您认为它是可行的,您是否介意提供一个答案,并举例说明我的示例中给出的类型层次结构如何编码?

因此我在这里回答这个问题。关键的想法是给出一个类型级函数(在宿主语言中,这里是Haskell)来计算子类型关系(目标语言的类型系统,这里是你的自定义EDSL)。为简单起见,我将详细说明子类型关系,但标准类型级编程可用于减少重复并适当提高抽象级别。首先,需要扩展:

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

现在定义子类型关系:

data Level = Primary | Unary | Mult | Add | Comp

type family Subtype a b where
    Subtype Primary a       = True
    Subtype Unary   Primary = False
    Subtype Unary   a       = True
    Subtype Mult    Primary = False
    Subtype Mult    Unary   = False
    Subtype Mult    a       = True
    Subtype Add     Add     = True
    Subtype Add     Comp    = True
    Subtype Add     a       = False
    Subtype Comp    Comp    = True
    Subtype Comp    a       = False

封闭类型系列用于保证客户端(您的第二个属性)无法扩展子类型关系。最后,目标语言术语的GADT可以使用子类型关系作为其构造函数的约束。

data Expr a where
    ID      :: Subtype Primary a ~ True => Expr a
    Paren   :: Subtype Primary a ~ True => Expr b -> Expr a
    Negate  :: Subtype Unary   a ~ True => Expr Unary -> Expr a
    Times   :: Subtype Add     a ~ True => Expr Mult -> Expr Mult -> Expr a
    Plus    :: Subtype Add     a ~ True => Expr Add -> Expr Add -> Expr a
    Compose :: Subtype Comp    a ~ True => Expr Comp -> Expr Comp -> Expr a

请注意,因为Paren的参数是多态的,所以您需要在包含的术语上使用类型注释来表示您希望该术语被视为的子类型层次结构的“级别”。我希望你也需要用你设计的任何语言来做这件事。在ghci中,我们可以询问样本术语的类型:

:t Compose (Times ID ID) (Negate (Paren (Plus ID ID :: Expr Add)))
Compose (Times ID ID) (Negate (Paren (Plus ID ID :: Expr Add)))
  :: (Subtype 'Comp a ~ 'True) => Expr a

我认为这或多或少是你对这个词期望的类型。您还可以看到表达式层次结构是严格执行的,但我敢说错误消息不是100%明确的(因为它是用宿主语言术语而不是目标语言术语编写的):

:t Negate (Plus ID ID)

<interactive>:1:9:
    Couldn't match type ‘'False’ with ‘'True’
    Expected type: 'True
      Actual type: Subtype 'Add 'Unary
    In the first argument of ‘Negate’, namely ‘(Plus ID ID)’
    In the expression: Negate (Plus ID ID)

答案 1 :(得分:2)

我想到了两件事,包括&#34;真实&#34;子类型系统(因此在Haskell中不可用),虽然我不完全确定它们是否符合您的所有要求:

  1. 明确无标记union types,与Ceylon一样,可让您将类型命名为A | B,这是AB的超类型。因此,您只需设置EmptyExpr正常ADT,然后声明同义词type OptionalExpr = Empty | Expr

  2. ADT在Scala中建模的方式,作为密封特征和案例类的层次结构:

    sealed trait OptionalExpr
    case object Empty extends OptionalExpr
    sealed trait Expr extends OptionalExpr
    case class IntExpr(i: Int) extends OptionaExpr
    case class AddExpr(lhs: Expr, op: AddOp, rhs: Expr) extends OptionalExpr
    

    这样,OptionalExprExpr不可扩展(因为特征是密封的),并且在Haskell中的行为大致类似于ADT,但您仍然可以访问&#34;中间&#34;像普通继承层次结构中的类型(与Haskell不同,在Haskell中只有构造函数,它们本身不是类型)。

  3. 当然,这两种情况都需要一种模式匹配的形式来访问值,因为你必须恢复工会的哪一部分&#34;你是。

答案 2 :(得分:2)

Haskell特别适合对您的域进行建模,可能是因为它可以用相当简单的数学模型来描述。至关重要的是,您的第一点意味着子类型关系是well-order。这使你的生活变得非常简单 - 这种模式可能很容易转换为任何类型系统至少与Haskell一样强大的语言。

首先定义一个类型(将被提升到一种类型)来表示你的变体:

data Variant = Primary | Unary | Mult | Add | Comp | Expr 

接下来使用 - 递归数据类型来表示术语中的节点:

data ExprF (k :: Variant -> *) (x :: Variant) where 
  ID_F :: ExprF k 'Primary 
  Paren_F :: k 'Expr -> ExprF k 'Primary  
  Negate_F :: k 'Primary -> ExprF k 'Unary
  Mult_F :: k 'Mult -> k 'Unary -> ExprF k 'Mult 
  Add_F  :: k 'Add -> k 'Mult -> ExprF k 'Add 
  Comp_F :: k 'Add -> k 'Add -> ExprF k 'Comp 

术语的递归出现由附加参数表示。本质上,这只是典型的多项式仿函数表示(即Fix),但具有索引参数。

您的表达式类型是:

data Expr' (x :: Variant) where 
  Expr' :: (x <= y) => Expr x -> Expr' y 

data Expr (x :: Variant) where 
  MkExpr :: ExprF Expr' x -> Expr x 

尚未引入<=类,但它代表您的子类型关系。

如前所述,您的子类型关系是一个良好的顺序,并且通过这种方式,可以为排序中的每个元素分配一个唯一的自然数,以便自然的典型排序尊重您的子类型关系。或者换句话说,有一个注入f : Variant -> Natxy iff f x <= f y的子类型(或严格的子类型iff f x < f y - 这样的表示给你很多一般性)。

所需的注射只是由你的语法给出。注意,每个产品只是它上面的产品的“子类型”(即,具有不应该引入构造函数的右侧)。

data Nat = Z | S Nat 
infixr 0 $ 
type ($) f a = f a 

type family VariantIx (x :: Variant) :: Nat where 
  VariantIx 'Primary = 'Z 
  VariantIx 'Unary = 'S 'Z 
  VariantIx 'Mult = 'S $ 'S 'Z 
  VariantIx 'Add = 'S $ 'S $ 'S 'Z 
  VariantIx 'Comp = 'S $ 'S $ 'S $ 'S 'Z 
  VariantIx 'Expr = 'S $ 'S $ 'S $ 'S $ 'S 'Z 

您需要一个隐式子类型关系(<=),但使用关系的显式证明通常要容易得多,因此隐式版本通常只生成显式证明。为此,您要编写两个声明:

data family (:<=:) (x :: k) (y :: k) 
class (<=) (x :: k) (y :: k) where 
  isLTEQ :: x :<=: y 

自然的实例应该相当明显:

data instance (:<=:) (x :: Nat) y where 
  LT_Z :: 'Z :<=: n 
  LT_S :: n :<=: m -> 'S n :<=: 'S m 
instance 'Z <= n where isLTEQ = LT_Z 
instance (n <= m) => 'S n <= 'S m where isLTEQ = LT_S isLTEQ 

Variant的实例定义了VariantIx

引发的顺序
newtype instance (:<=:) (x :: Variant) y = IsSubtype (VariantIx x :<=: VariantIx y) 
instance (VariantIx x <= VariantIx y) => x <= y where isLTEQ = IsSubtype isLTEQ 

您可能想要一些智能构造函数。如果您使用最近的GHC,您将可以访问模式同义词,但没有必要:

id_ = MkExpr ID_F 
pattern Id = MkExpr ID_F 

pattern Paren e = MkExpr (Paren_F (Expr' e)) 

pattern Neg e = MkExpr (Negate_F (Expr' e)) 

infixl 6 :+ 
pattern (:+) a b = MkExpr (Add_F (Expr' a) (Expr' b)) 

infixl 7 :* 
pattern (:*) a b = MkExpr (Mult_F (Expr' a) (Expr' b)) 

pattern Cmp a b = MkExpr (Comp_F (Expr' a) (Expr' b)) 

和一些简单的例子:

>Id :+ Id :+ Neg Id :* Id
Add_F (Add_F ID_F ID_F) (Mult_F (Negate_F ID_F) ID_F)
>Id :+ Id :* Neg (Id :* Id)

<interactive>:6:13:
    No instance for (('S $ 'S 'Z) <= 'Z) arising from a use of `Neg'

请注意,您也可以用稍微不同的方式编写表达式类型:

data ExprFlip (x :: Variant) where 
  MkExprFlip :: (x <= y) => ExprF ExprFlip x -> ExprFlip y 

这与原文的不同之处在于表达式的最外层类型具有应用于它的子类型关系 - 例如。

pattern Id' = MkExprFlip ID_F 

ExprFlip t时有Id :: Expr 'Primary类型。我看不出他们有什么不同的其他方式,我想这只是一个偏好问题,或者哪些用例最常见。原始表示具有以下优点:输出类型始终是单形的,这在某些情况下可以使类型推断更好,但不会影响表达式的构造。

解决你的四点:

  1. 该模型依赖于设计的子类型关系的语义。
  2. VariantIx并且Variant类型已关闭。 :<=:<=的{​​{1}}或Variant的任何其他实例将与现有的实例(尽可能一般)重叠,因此原则上他们可以定义,尝试使用它们会产生类型错误。
  3. 基本上,您具有自反和传递关系,并且Nat <=实例中一劳永逸地捕获这些属性。更改子类型关系仅限于更改NatVariant
  4. 子类型关系的证明由类型推断构成 - 由VariantIx类构成。由于<=数据类型中的所有索引都是单态的,因此类型检查器将始终能够计算索引的子类型关系。
  5. 完整代码:

    ExprF

答案 3 :(得分:1)

除非我误解,否则多态变体几乎可以做到这一点。然而,“没有标记的联盟”并不是一个很好用的术语(我想大多数人会认为你要求的是C式联盟)。

示例如下所示:

type empty = [`Empty]

type bin_op = Add | Sub

type expr = [`Int of int | `Add of expr * bin_op * expr]

type optional_expr = [empty | expr]

type weird_expr = [expr | `Wierd of expr | `Zonk of string]

请注意,对于OCaml的多态变体,子类型关系是在结构上定义的,而不是在命名类型之间定义。