Search code examples
ocamllablgtk

Linking OCaml Record with its GUI representation


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.


Solution

  • 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: