Ocaml 在另一个类型声明中选择一个类型的子类型

Ocaml selecting a type's subtype in another type declaration

给定两个类型声明,我想在另一个类型声明中使用一个类型声明的子类型。例如,假设我有红色、蓝色、黄色三种颜色,在制作另一种类型时我将如何具体引用每个子类型?此示例并非针对我的问题,而是对我面临的问题的简化。我试过下面的例子,直接引用红色等。我也试过红色,即:

type colour =
| Red
| Blue
| Yellow

type shape =
| Rectangle * Red
| Square * Yellow

请注意上面我是如何尝试为矩形强制使用红色颜色类型和为正方形强制使用黄色颜色类型的,我该怎么做呢?

如果您真的想将 RectangleSquare 限制为仅一种颜色,则不需要表示该颜色——这会是多余的。但我假设你问的是比这更笼统的问题。

OCaml 不支持对此类变体进行子类型化。您不能创建仅具有 Red 作为其可能值的新类型,或仅具有 RedYellow.

但是,所谓的 "polymorphic variants" 支持子类型化。你可以有这样的东西:

# type rby = [ `Red | `Blue | `Yellow ];;
type rby = [ `Blue | `Red | `Yellow ]
# type r = [ `Red ];;
type r = [ `Red ]
# type y = [`Yellow ];;
type y = [ `Yellow ]
# type shape = Rectangle of r | Square of y;;
type shape = Rectangle of r | Square of y
# Rectangle `Yellow;;
Error: This expression has type [> `Yellow ]
       but an expression was expected of type r
       The second variant type does not allow tag(s) `Yellow
# Rectangle `Red;;
- : shape = Rectangle `Red

请注意,OCaml 不会自动推断子类型关系。您需要使用 :> 表示法明确要求它们。

根据我的经验,多态变体会为您的代码增加很多复杂性。所以我建议只有在它们确实以其他方式使事情变得更好时才使用它们。

(我还要补充一点,您的类型 colour 或多或少与 C 或 Java 中的枚举完全相同。因此,您的要求并不完全清楚。没有C 中的方式或 Java 创建一个新类型,该类型仅包含从枚举中选择的几个值。)

在 OCaml 中,一种限制一组值的方法是通过模块系统来实现。模块是类型定义、命名值和子模块的集合。每个模块都公开一个接口,用于保证值的格式正确。

在此处的示例中,我们将有一个 Rectangle 模块,它会提供一个 create 函数,让您只能创建红色矩形。我们还有一个 Square 模块,它同样只会让用户创建黄色方块。

type color =
  | Red
  | Blue
  | Yellow

module type Shape = sig
  type t
  val create : unit -> t
  val get_color : t -> color
end

module Rectangle : Shape = struct
  type t = unit
  let color = Red
  let create () = ()
  let get_color () = color
end

module Square : Shape = struct
  type t = unit
  let color = Yellow
  let create () = ()
  let get_color () = color
end

type shape =
  | Rectangle of Rectangle.t
  | Square of Square.t

let get_color shape =
  match shape with
  | Rectangle x -> Rectangle.get_color x
  | Square x -> Square.get_color x

现在我们有了矩形和正方形,每个都有自己的约束。形状是红色矩形或黄色正方形。这是由模块接口 Shape 保证的,它恰好由 RectangleSquare 模块共享(但不是必须)。此模块接口强制您使用 create 函数来创建类型 t 的对象。请注意,Rectangle.tSquare.t 是不同的类型,不能互换使用,即使模块 RectangleSquare 具有相同的接口。

如果您在这一点上没有迷路并且想进一步使用此解决方案,我建议您查看 private 关键字,它允许以只读方式公开类型详细信息。

这是一个幻像类型的例子:

type colour = Red | Blue | Yellow                                                                                                                                                                                                                                                                                            
type shape  = Rectangle | Square


module ColouredShape : sig
  (* Type parameterized by 'a, just for the type system. 'a does not appear in the 
    right hand side *)

  (* Dummy types, used as labels in the phantom type *)
  type 'a t
  type red
  type yellow

  val make_red    : shape ->    red t
  val make_yellow : shape -> yellow t

  val make_rectangle : unit ->    red t
  val make_square    : unit -> yellow t

  val f :     'a t -> colour
  val g :    red t -> colour
  val h : yellow t -> colour

end
=
struct

  type 'a t = shape * colour
  type red
  type yellow

  let make_red    s = (s, Red)
  let make_yellow s = (s, Yellow)

  let make_rectangle ()  = make_red    Rectangle
  let make_square    ()  = make_yellow Square

  let f x = snd x
  let g x = snd x
  let h x = snd x

end



open ColouredShape
open Printf

let _ =
  let rectangle = make_rectangle () in
  let square    = make_square () in
  let c = f square in
  printf "%b\n" (c = Red);

  let c = f rectangle in
  printf "%b\n" (c = Red);

  let c = g square in
  printf "%b\n" (c = Red);

  let c = g rectangle in
  printf "%b\n" (c = Red);

  let c = h square in
  printf "%b\n" (c = Red);

  let c = h rectangle in
  printf "%b\n" (c = Red)

正如预期的那样,编译器拒绝了代码:

let c = g square
          ^^^^^^
Error: This expression has type ColouredShape.yellow ColouredShape.t
       but an expression was expected of type
         ColouredShape.red ColouredShape.t
       Type ColouredShape.yellow is not compatible with type
         ColouredShape.red

这个答案是在 glennsl 的帮助下写的: