2

This question is related to another question I asked before.

I am reading data from a JSON file and try to parse them into a datatype i made.

{
  "rooms":
  [
    {
      "id": "room1",
      "description": "This is Room 1.  There is an exit to the north.\nYou should drop the white hat here.",
      "items": ["black hat"],
      "points": 10,
      "exits": [
        {
          "direction": "north",
          "room": "room2"
        }
      ],
      "treasure": ["white hat"]
    },
    {
      "id": "room2",
      "description": "This is Room 2.  There is an exit to the south.\nYou should drop the black hat here.",
      "items": [],
      "points": 10,
      "exits": [
        {
          "direction": "south",
          "room": "room1"
        }
      ],
      "treasure": ["black hat"]
    }
  ]  
}

My user-defined type for room is:

type room = {
  room_id          :  int ;
  room_description :  string ;
  room_items       :  item list ;
  room_points      :  int ;
  room_exits       :  exit list ;
  room_treasure    :  item list ;
}
and exit = direction * room

However, room has a "exit" field, which itself is a "room" type. Then when I try to create record for room1, I first need to define room2, but in order to define room2, I need to know room1. This seems like a cyclic type.

Can anyone help me with this?

Community
  • 1
  • 1
齐天大圣
  • 1,169
  • 5
  • 15
  • 36

2 Answers2

2

If you stick to the immutable and eager subset of OCaml, there's no real way to build up arbitrary cyclic structures. The problem is exactly as you state it.

It's possible to build specific examples of cyclic structures using let rec, but I don't believe this can be extended to building arbitrary structures while (for example) parsing JSON.

You can solve the problem by dropping the requirement for immutable data. If you make the links to other rooms into OCaml references (mutable fields), you can build cyclic structures pretty much as you'd do it in the imperative part of JavaScript.

One way to make this work might be to use an array for room_exits rather than a list. OCaml arrays are mutable.

Here's some code that creates the complete graph over 3 nodes (for a trivial node type that contains only the neighbor nodes):

# type node = { nabes: node array };;
type node = { nabes : node array; }
# type graph = node list;;
type graph = node list
# let z = { nabes = [||] };;
val z : node = {nabes = [||]}
# let temp = Array.init 3 (fun _ -> { nabes = Array.make 2 z});;
val temp : node array =
  [|{nabes = [|{nabes = [||]}; {nabes = [||]}|]};
    {nabes = [|{nabes = [||]}; {nabes = [||]}|]};
    {nabes = [|{nabes = [||]}; {nabes = [||]}|]}|]
# temp.(0).nabes.(0) <- temp.(1);;
- : unit = ()
# temp.(0).nabes.(1) <- temp.(2);;
- : unit = ()
# temp.(1).nabes.(0) <- temp.(0);;
- : unit = ()
# temp.(1).nabes.(1) <- temp.(2);;
- : unit = ()
# temp.(2).nabes.(0) <- temp.(0);;         
- : unit = ()
# temp.(2).nabes.(1) <- temp.(1);;
- : unit = ()
# let k3 : graph = Array.to_list temp;;
val k3 : graph =
  [{nabes =
     [|{nabes = [|<cycle>; {nabes = [|<cycle>; <cycle>|]}|]};
       {nabes = [|<cycle>; {nabes = [|<cycle>; <cycle>|]}|]}|]};
   {nabes =
     [|{nabes = [|<cycle>; {nabes = [|<cycle>; <cycle>|]}|]};
       {nabes = [|{nabes = [|<cycle>; <cycle>|]}; <cycle>|]}|]};
   {nabes =
     [|{nabes = [|{nabes = [|<cycle>; <cycle>|]}; <cycle>|]};
       {nabes = [|{nabes = [|<cycle>; <cycle>|]}; <cycle>|]}|]}]

You can also solve the problem by linking through an intermediate structure. For example, you can have a dictionary that maps room names to rooms. Then your links to other rooms can just use the name (rather than a direct link to the OCaml value). I've used this method in the past, and it works pretty well. (This is, in fact, how your JSON is working implicitly.)

Jeffrey Scofield
  • 65,646
  • 2
  • 72
  • 108
1

That's why in the previous answer I put function room_exits into the Game interface, not into the Room. The intuition behind this is that room exits, i.e., other rooms, are not part of the room. If you define some structure, as "the room is walls, treasure, and other rooms", then you defining something more than a room, that basically means, that you're defining the whole maze. So the room, is just a room, i.e., it's contents. The way how the rooms are connected is a Maze. (I've used Game for this in previous answer, but maybe Maze is a better name).

To summarize, in your particular case, you need just to remove references to other rooms from the room data representation, and store the maze information as an associative container inside the maze (or game) data structure:

 type exits = (dir * room) list

 type maze = {
   ...
   entry : room;
   rooms : exits Room.Map.t
 }

or maybe even more precise, you can use Dir.Map as an associative container, instead of associative list:

type exits = room Dir.Map.t

The latter representation guarantees that there is no more than one room per direction.

Note: the above definitions assumes that Room implements Comparable interface, and that you're using Core library. (I think you're since I remember that there was a link to RWO from the course page). To implement a comparable interface you need to implement the compare function and Sexpable interface. It is easy using type generators, basically it looks like this:

module Room = struct 
  type t = {
    name : string;
    treasures : treasure list;
    ...
  } with compare, sexp

  include Comparable.Make(struct 
    type nonrec t = t with compare, sexp
  end)
end

the with compare, sexp will automatically generate the compare function, and the pair of sexp_of_t, t_of_sexp functions, that are needed for a Comparable.Make functor to realize the Comparable interface.

Note: if it too much at this point of course, then you can just use String.Map.t data structure, and perform the lookup by a room name. This wouldn't be a bad idea.

ivg
  • 34,431
  • 2
  • 35
  • 63