简体   繁体   English

OCaml的GADT和许多类型变量

[英]OCaml's GADT and many type variables

I'm trying to model a card game in OCaml (let's assume that it is a solitaire game for the sake of simplicity). 我正在尝试在OCaml中模拟纸牌游戏(为了简单起见,我们假设它是一个单人纸牌游戏)。 A given state of this game is represented by a value of type game . 该游戏的给定状态由类型game的值表示。 Then I will define a function moves : game -> move list that gives the list of valid moves for the given state of the game; 然后我将定义一个函数moves : game -> move list ,给出给定游戏状态的有效移动列表; and a function apply: game -> move -> game gives the state after making the given move. 并且功能apply: game -> move -> game在给定移动后给出状态。 (The types presented here may actually be replaced by polymorphic ones, as explained below.) (这里提供的类型实际上可能被多态的类型替换,如下所述。)

It so happens that there are two qualitatively different kinds of moves in this game. 碰巧在这个游戏中有两种质量上不同的动作。 At some points of the game, one needs to decide to bid or not to bid. 在游戏的某些方面,需要决定是否出价。 At the other points of the game, one simply needs to play a card. 在游戏的其他方面,人们只需要打牌。 It should be an error to apply apply g to m where g requires a (non-)bidding move and m is a card-playing move, for instance. apply g应用于m应该是一个错误,其中g需要(非)出价移动, m是一个纸牌移动,例如。

I would like this error to be a static one. 我希望这个错误是一个静态错误。 So I thought of using a GADT. 所以我想到了使用GADT。 I started like this: 我开始是这样的:

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)]

All of these type-check so far. 所有这些类型检查到目前为止。 The following, however, does not: 但是,以下内容不是:

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

The content of the function is a nonsense now, but the point is that it requires nontrivial (run-time) computation to determine whether the new game state requires a (non-)bidding move or a card-playing move. 该功能的内容现在是无稽之谈,但关键在于它需要非平凡(运行时)计算来确定新游戏状态是否需要(非)出价移动或纸牌移动。 Here, I don't know the correct type declaration. 在这里,我不知道正确的类型声明。

Also, the function apply , when defined correctly, has to have something like the following type-check: 此外,正确定义时,函数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 *)
(* ... *)

Is this possible with a GADT? 这可能与GADT有关吗? If not, can that be circumvented by encoding GADTs by using first-class modules? 如果没有,可以通过使用一流的模块编码GADT来规避这一点吗? Or do I have to resort to the object system? 或者我是否必须求助于对象系统?

(In case this is relevant, I'm going to use these functions in the innermost loop in a code compiled by using js_of_ocaml .) (如果这是相关的,我将在使用js_of_ocaml编译的代码中的最内层循环中使用这些函数。)

EDIT: in response to PatJ's answer: 编辑:回应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)

First of all, that's quite good for a first GADT try. 首先,这对第一次GADT尝试来说非常好。 Your problem is indeed that your b type variable cannot be known statically. 你的问题确实是你的b类型变量无法静态知道。

Now you have several ways to circumvent that, depending on your needs. 现在,根据您的需要,您可以通过多种方式来规避这一点。

The easiest solution is to create an ADT that hides your type information: 最简单的解决方案是创建一个隐藏您的类型信息的ADT:

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

Note that you won't be able to access those types outside of a pattern matching on a game2 value. 请注意,您将无法访问与game2值匹配的模式之外的那些类型。 You basically have to consider play_phase game and bid_phase game to be two distinct and incompatible type. 你基本上必须考虑play_phase gamebid_phase game是两个截然不同bid_phase game兼容的类型。

Another possibility, that gives you more leeway (but may not be what you're looking for) is to separate your data from your proof of type: 另一种可能为您提供更多余地(但可能不是您正在寻找的)的可能性是将您的数据与您的类型证明分开:

(* 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
(* ... *)

Note that this second method allows you to access common without knowing the phase we're in. You may not want that. 请注意,第二种方法允许您在不知道我们所处的阶段的情况下访问公共。您可能不希望这样。 Also apply does not bind the next phase. 同样适用不会束缚下一阶段。 If you want to do that, you'll have to combine this method with the preceding one. 如果你想这样做,你必须将这个方法与前一个方法结合起来。

GADTs can be quite infuriating, but they're very fun to work with. GADT可能非常令人愤怒,但与他们合作非常有趣。 As you can see, you often need to have constructors dedicated to manipulate type information without any actual data associated with it. 如您所见,您经常需要具有专用于操作类型信息的构造函数,而不需要与之关联的任何实际数据。 Once you master that way of thinking, you can do some pretty awesome type error messages type-safe code. 一旦掌握了这种思维方式,就可以做一些非常棒的类型错误消息类型安全代码。

Edit: 编辑:

You now want to use first class module to hide the type information, that's not a good idea. 您现在想要使用第一类模块来隐藏类型信息,这不是一个好主意。 You gain exactly the same thing as you'd have with the game2 trick, but with a much more painful syntax. 你获得了与game2技巧完全相同的东西,但语法更加痛苦。

Also, @Drup's solution is better than mine. 另外,@ Drup的解决方案比我的好。

@PatJ's solution is to hide the types and try to move on like that. @ PatJ的解决方案是隐藏类型并尝试继续前进。 I think this is a bad solution because, in the end, it doesn't really give you anything and forces you to play hide-and-seek with the existentials. 我认为这是一个糟糕的解决方案,因为它最终并没有真​​正给你任何东西,并迫使你与存在者一起玩捉迷藏。

Instead, you should embrace the fact that you are encoding a state-machine in the type system where games are the states and moves are the transition. 相反,您应该接受这样一个事实,即您在类型系统中编码状态机,其中游戏是状态,移动是转换。 If you do that the path seems clearer: transitions are always from one state to another: 如果你这样做,路径似乎更清晰:转换总是从一个状态到另一个状态:

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 *)

Note here the explicit move to enter a bid. 请注意这里输入出价的明确举动。 You can only decide the types based on other type information. 只能根据其他类型信息确定类型。 In particular you can't say "the game is now bidding because the list of cards is empty" without lifting the fact that the card list is empty to the type level. 特别是你不能说“游戏现在正在竞标,因为卡片列表是空的”而没有解除卡片列表在类型级别为空的事实。

If you ask me, I think this is grossly overkill, but Eh. 如果你问我,我认为这是非常矫枉过正的,但是呃。 :p :p

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM