Linking OCaml Record with its GUI representation

109 views Asked by At

I have this record type.

   type cell = { alive : bool ; column : int ; row : int }
   ;;

Now I create a grid of such cells.

    #require "containers";;
   let makegrid = CCList.init 2 ( fun i -> (CCList.init 2 (fun j -> { alive = true; column = j;row = i })) );;

I draw a square grid using lablgtk based on the number of cells in the grid.

    let drawgrid area (backing:GDraw.pixmap ref) grid =
    let rec loop1 limit m y =
     match m with
   | m when m < limit ->
  (let rec loop x n =
    match n with
    | n when n < limit ->
      let x = x + 20 in
      let width, height = 20,20 in
      displayrectangle area backing x y width height;
      (*Printf.printf "%3d %3d\n" x y;*)
      loop x   (n + 1)
    | n when n >= limit -> loop1  (List.length grid) (m + 1) (y + 20)
  in loop 0  0)
 (* when m >= limit *)  
 | m when m >= limit ->  ()
in loop1 (List.length grid) 0 0

;;

So the final code is like this.

 let makegameoflifegrid = CCList.init 7 ( fun i -> (CCList.init 7 (fun j -> { alive = false; column = j;row = i })) ) in
 let drawing_area = GMisc.drawing_area ~width:200 ~height:200 ~packing:aspect_frame#add () in
 drawing_area#event#connect#expose ~callback:(expose drawing_area backing);
 drawing_area#event#connect#configure ~callback:(configure window backing);
 drawing_area#event#add [`EXPOSURE];
 window#show ();
 drawgrid drawing_area backing makegameoflifegrid;
 GMain.Main.main ()
 ;;
 let _ = main ()
 ;;

I was wondering how to relate the cell type with its GUI representation which has x,y co-ordinates. This is basically a game of life and if I have to make a cell solid based on whether the cell is alive or not then I have to deal with two different represenations - alive attribute in the cell and x,y co-ordinates in the GUI.

Is there a functional solution for this ? The code actually works(except this problem) and has no inherent problem at this time and I know basic OCaml.

Update :

One could put the x and y co-ordinates in the record itself like this.

let drawgridrepresentation area (backing:GDraw.pixmap ref) grid =
let rec loop1 limit m y g1=
match m with
| m when m < limit ->
  (let rec loop x n g=
    match n with
    | n when n < limit ->
      let x = x + 20 in
      let width, height = 20,20 in
      displayrectangle area backing x y width height;
      (*Printf.printf "%3d %3d\n" x y;*)
      let gridmapi = 
      List.mapi (fun i el -> List.mapi ( fun i el1 ->
          if (n = el1.column && m = el1.row)
          then 
            ({ el1  with row = x; column = y}
             ) else el1) el ) g in

      loop x   (n + 1) gridmapi
    | n when n >= limit -> loop1  (List.length grid) (m + 1) (y + 20) g
  in loop 0  0 g1)
  (* when m >= limit *)  
  | m when m >= limit ->  g1
  in loop1 (List.length grid) 0 0 grid
  ;;

But I think I miss something.

1

There are 1 answers

3
ivg On BEST ANSWER

Functional programming favors the application of transformations on mathematical objects. I would say that this is the main constituent of a functional thinking - a functional programmer reasons in terms of transformations, where an OOP programmer reasons in terms of objects.

The strong part of functional reasoning lies in a tight connection between it and mathematics, in particular with Category Theory and Logic, that are the foundations of mathematics.

A transformation is a relation between mathematical objects. The mathematical objects by itself are abstract, pure, and immutable. So, whenever a functional programmer (or a mathematician - that is the same) thinks about a transformation, he actually thinks about two abstractions (one to the left of the arrow, and another to the right).

If we will apply mathematical thinking to your problem, then we can express our problem as a set of abstractions. First of all, we need to speak about a Coordinate abstraction. We care only about the neighborhood relation in our Game, so I would propose the following signature for the Coordinate structure:

module type Coord = sig
  type t
  val fold_neighbors : t -> ('a -> t -> 'b) -> 'a -> 'b
end

This is only one possible way to express this abstraction, for example this is another:

module type Coord' = sig
  type t
  val neighbors : t -> t list (* bad - we are encoding the list representation *)
end

But let's stick with the Coord signature. Btw, notice how OCaml parlance matches the mathematics. We have OCaml structures for mathematical structures and OCaml signatures for mathematical signatures.

The next abstraction is our world. Basically, it is just a collection of coordinates that we will also represent using the fold function (although we could choose 'a list or any other container, I would prefer not to hard-code any specific data structure).

module type World = sig
  type t
  type coord

  val fold : t -> ('a -> coord -> 'b) -> 'a -> 'b
end

Now we have everything we need to implement our Game. From a mathematical perspective a game is just a set of rules, described with the following signature:

module type Game = sig
  type world
  type coord
  val state : world -> coord -> [`Live | `Dead | `Empty]
  val step  : world -> world
end

The implementation of rules would be a functor of the following type:

module type Rules = functor
  (Coord : Coord)
  (World : World with type coord = Coord.t) ->
  Game with type world = World.t
        and type coord = Coord.t

With these abstractions, we can already start to play the game, for example, choose different starting worlds and see whether the World.step function reaches a fixpoint (i.e., cells worlds w and step w have the same states), how long does it take it to reach a fixpoint, etc.

If we want to visualize, then we need to throw in more abstractions. Since we're not going to handle 3d devices, like 3d printers and hologram monitors right now, we will stick to 2d visualization. For our visualization we need a canvas abstraction, e.g.,:

module type Canvas = sig
  type t

  val rectangle : t ->
    ?color:int ->
    ?style:[`solid | `raised] ->
    width:int -> height:int -> int -> int -> unit

  val width : t -> int
  val height : t -> int

  val redraw : t -> unit
end

We also need to handle coordinate transformations from our abstract coordinates to the Cartesian coordinates in which Canvas lives:

module type Cartesian = sig
  type t
  type coord
  type dom
  val x : t -> coord -> dom
  val y : t -> coord -> dom
end

Finally, using these abstractions we can implement an animated game:

module Animation2d
    (World : World)
    (Game : Game with type world = World.t and type coord = World.coord)
    (Canvas : Canvas)
    (Coord : Cartesian with type coord = Game.coord and type dom = int) =
struct

  let black = 0x000000
  let white = 0xFFFFFF
  let red   = 0xFF0000

  let color_of_state = function
    | `Live -> red
    | `Dead -> black
    | `Empty -> white

  let run ?(width=10) ?(height=10) world canvas proj =
    let draw game =
      World.fold game (fun () coord ->
          let color = color_of_state (Game.state world coord) in
          let x = Coord.x proj coord in
          let y = Coord.y proj coord in
          Canvas.rectangle canvas ~color ~width ~height x y) () in
    let rec play world =
      draw world;
      Canvas.redraw canvas;
      play world in
    play world
end

As you may see, with properly chosen abstractions you even don't have the problem that you were describing (i.e., the simultaneous presence of two representations of the same abstraction). So a functional way of solving your problem is not to create it :)

Bibliography

There are two essential textbooks, that teach functional programming and functional reasoning. They do not use OCaml, but Scheme, although these doesn't diminish their value, as Scheme is a pure abstraction without any syntactic sugar, that will help you to understand the essence, without blurring your mind with syntactic issues: