OCaml 的 GADT 和很多类型变量

OCaml's GADT and many type variables

我正在尝试在 OCaml 中模拟纸牌游戏(为简单起见,我们假设它是纸牌游戏)。该游戏的给定状态由 game 类型的值表示。然后我将定义一个函数 moves : game -> move list 来给出给定游戏状态的有效移动列表;函数 apply: game -> move -> game 给出了进行给定移动后的状态。 (此处显示的类型实际上可能会被多态类型替换,如下所述。)

碰巧这个游戏有两种性质不同的走法。在游戏的某些时刻,需要决定出价或不出价。在游戏的其他时间点,玩家只需要打一张牌。例如,将 apply g 应用到 m 应该是错误的,其中 g 需要(非)叫牌动作,而 m 是打牌动作。

我希望这个错误是静态错误。所以我想到了使用 GADT。我是这样开始的:

type card = int * int
type common = { cards : card list }
type play_phase = Play_phase
type bid_phase = Bid_phase
type _ game = Play_game :  common ->  play_phase game | Bid_game :  common ->  bid_phase game
type _ move =
  | Play : card -> play_phase move
  | Bid : bid_phase move
  | NoBid : bid_phase move

let moves : type a. a game -> a move list = function
  | Bid_game _ -> [Bid; NoBid]
  | Play_game _ ->  [Play (0,0)]

到目前为止所有这些类型检查。但是,以下内容不是:

let apply : type a b. (a game * a move) -> b game = function
  | (Bid_game g, _) -> Play_game g
  | (Play_game ({ cards = [] } as g), _) -> Bid_game g
  | (Play_game g, _) -> Play_game g

函数的内容现在是废话,但关键是它需要非平凡的(运行-time)计算来确定新的游戏状态是否需要(非)叫牌移动或打牌动作。在这里,我不知道正确的类型声明。

此外,函数 apply 在正确定义时必须具有类似于以下类型检查的内容:

(* ... *)
let rec loop g (* more parameters *) =
   let ms = moves g in
   let m = (* choose an element of ms somehow *) in
   loop (apply g m) (* more parameters *)
(* ... *)

这可以用 GADT 实现吗?如果不是,是否可以通过使用 first-class 模块对 GADT 进行编码来规避?还是必须求助于对象系统?

(如果这是相关的,我将在使用 js_of_ocaml 编译的代码的最内层循环中使用这些函数。)

编辑:回应 PatJ 的回答:

module type Exist = sig type t val x : t game end

let apply' : type a. a game -> a move -> (module Exist)
  = fun { data = cs }  m ->
  match cs with
  | [] ->
     let module M =  struct
         type t = bid_phase 
         let x = { phase = Bid_phase; data = [] }
       end in
     (module M)
  | cs ->
     let module M = struct
         type t = play_phase
         let x = { phase = Play_phase; data = cs}
       end in
     (module M)

首先,这对于第一次尝试 GADT 来说非常好。您的问题确实是您的 b 类型变量无法静态知道。

现在您可以根据自己的需要使用多种方法来避免这种情况。

最简单的解决方案是创建一个隐藏类型信息的 ADT:

type game2 = P of play_phase game | B of bid_phase game

请注意,您将无法在 game2 值的模式匹配之外访问这些类型。您基本上必须将 play_phase gamebid_phase game 视为两种不同且不兼容的类型。

另一种给您更多回旋余地(但可能不是您想要的)的可能性是将您的数据与类型证明分开:

(* Same types as yours, except for the game definition *)
type _ game_phase = Play_game : play_phase game_phase | Bid_game :  bid_phase game_phase
type 'a game = { data: common; phase: 'a game_phase; }


let moves : type a. a game -> a move list = function
  | { phase = Bid_game; _ } -> [Bid; NoBid]
  | { phase = Play_game; _ } ->  [Play (0,0)]

let apply : type a. (a game * a move) -> common = function
(* ... *)

请注意,第二种方法允许您在不知道我们所处阶段的情况下访问 common。您可能不希望这样。也适用不绑定下一阶段。如果要这样做,则必须将此方法与前面的方法结合使用。

GADT 可能非常令人恼火,但与它们一起工作非常有趣。如您所见,您通常需要有专门用于操作类型信息的构造函数,而无需任何实际数据与之关联。一旦你掌握了这种思维方式,你就可以做一些非常棒的类型错误消息类型安全代码。

编辑:

您现在想使用第一个 class 模块来隐藏类型信息,这不是一个好主意。您获得的结果与使用 game2 技巧完全相同,但语法要痛苦得多。

此外,@Drup 的解决方案比我的好。

@PatJ 的解决方案是隐藏类型并尝试继续这样做。我认为这是一个糟糕的解决方案,因为最终它并没有真正给你任何东西,而是迫使你与存在主义玩捉迷藏。

相反,您应该接受这样一个事实,即您在类型系统中对状态机进行编码,其中游戏是状态,移动是过渡。如果这样做,路径似乎更清晰:转换总是从一种状态到另一种状态:

type card = int * int
type common = { cards : card list }
type play = Play
type bid = Bid
type _ game = Play_game :  common ->  play game | Bid_game :  common ->  bid game
type (_,_) move =
  | Play : card -> (play, play) move
  | StartBid : (play, bid) move
  | Bid : (bid, play) move
  | NoBid : (bid, play) move

type 'a any_move = Ex : ('a, 'b) move -> 'a any_move

let moves : type a. a game -> a any_move list = function
  | Bid_game _ -> [Ex Bid; Ex NoBid]
  | Play_game _ ->  [Ex (Play (0,0))]

let apply : type a b. a game -> (a, b) move -> b game =
  fun g m -> match m, g with
    | Bid, Bid_game g -> Play_game g
    | NoBid, Bid_game g -> Play_game g
    | StartBid, Play_game g -> Bid_game g
    | Play _c, Play_game g -> Play_game g

let rec loop : type a . a game -> _ =
  function g ->
    let ms = moves g in
    let Ex m = List.hd ms (* choose an element of ms somehow *) in
    loop (apply g m) (* more parameters *)

请注意此处输入出价的明确举动。您可以根据其他类型信息决定类型。特别是你不能说 "the game is now bidding because the list of cards is empty" 而不是将卡片列表为空的事实提升到类型级别。

如果你问我,我认为这太过分了,但是嗯。 :p