Let's make a Sudoku game with ocamljs and the Dom
library for programming the browser DOM. Like on the cooking shows, I have prepared the dish we're about to make beforehand; why don't you taste it now? OK, it is not yet Sudoku, lacking the important ingredient of some starting numbers to guide the game--we'll come back to that next time.
moduleD = Dom letd =D.documentWe begin with some definitions. The
Dom
module includes class types for much of the standard browser DOM, using the ocamljs facility for interfacing with Javascript objects. Dom.document
is the browser document object. letmake_board()=letmake_input()=letinput =(d#createElement "input":D.input)in input#setAttribute "type""text"; input#_set_size 1; input#_set_maxLength 1;letstyle = input#_get_style in style#_set_border "none"; style#_set_padding "0px";letenforce_digit()=match input#_get_value with|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"->()| _ -> input#_set_value ""in input#_set_onchange (Ocamljs.jsfun enforce_digit); input inWe construct the Sudoku board in several steps. First, we make an input box for each square. Notice that you can call DOM methods (e.g.
createElement
) with OCaml object syntax. But what is the type of createElement
? The type of the object you get back depends on the tag name you pass in; OCaml has no type for that. So createElement
is declared to return #element
(that is, a subclass of element
). If you need only methods from element
then you usually don't need to ascribe a more-specific type, but in this case we need an input
node. (Static type checking with Javascript objects is therefore only advisory in some cases--if you ascribe the wrong type you can get a runtime error--but still better than nothing.)We next set some attributes, properties, and styles on the input box. Properties are manipulated with specially-named methods: foo#_get_bar
becomes foo.bar
in Javascript, and foo#_set_bar baz
becomes foo.bar = baz
. Finally we add a validation function to enforce that the input box contains at most a single digit. To set the onchange
handler, you need to wrap it in Ocamljs.jsfun
, because the calling convention of an ocamljs function is different from that of plain Javascript function (to accomodate partial application and tail recursion).
letmake_td i j input =lettd = d#createElement "td"inletstyle = td#_get_style in style#_set_borderStyle "solid"; style#_set_borderColor "#000000";letwidths=function| 0 -> 2, 0 | 2 -> 1, 1 | 3 -> 1, 0 | 5 -> 1, 1 | 6 -> 1, 0 | 8 -> 1, 2 | _ -> 1, 0 inlet(top, bottom)= widths i inlet(left, right)= widths j inletpx k = string_of_int k ^"px"in style#_set_borderTopWidth (px top); style#_set_borderBottomWidth (px bottom); style#_set_borderLeftWidth (px left); style#_set_borderRightWidth (px right); ignore (td#appendChild input); td inNext we make a table cell for each square, containing the input box, with borders according to its position in the grid. Here we don't ascribe a type to the result of
createElement
since we don't need any td
-specific methods.letrows =Array.init 9 (funi ->Array.init 9 (funj -> make_input ()))inlettable = d#createElement "table"in table#setAttribute "cellpadding""0px"; table#setAttribute "cellspacing""0px";lettbody = d#createElement "tbody"in ignore (table#appendChild tbody);ArrayLabels.iteri rows ~f:(funi row ->lettr = d#createElement "tr"inArrayLabels.iteri row ~f:(funj cell ->lettd = make_td i j cell in ignore (tr#appendChild td)); ignore (tbody#appendChild tr));(rows, table)Then we assemble the full board: make a 9 x 9 matrix of input boxes, make a table containing the input boxes, then return the matrix and table. Notice that we freely use the OCaml standard library. Here the
tbody
is necessary for IE; the cellpadding
and cellspacing
don't work in IE for some reason that I have not tracked down. This raises an important point: the Dom
module is the thinnest possible wrapper over the actual DOM objects, and as such gives you no help with cross-browser compatibility. letcheck_board rows _ =leterror i j =letcell = rows.(i).(j)in cell#_get_style#_set_backgroundColor "#ff0000"inletcheck_set set =letseen =Array.make 9 None inArrayLabels.iter set ~f:(fun(i,j)->letcell = rows.(i).(j)inmatch cell#_get_value with| "" ->()| v ->letn = int_of_string v inmatch seen.(n - 1)with| None -> seen.(n - 1)<- Some (i,j)| Some (i',j')-> error i j; error i' j')inletcheck_row i = check_set (Array.init 9 (funj ->(i,j)))inletcheck_column j = check_set (Array.init 9 (funi ->(i,j)))inletcheck_square i j =letset =Array.init 9 (funk -> i * 3 + k mod 3, j * 3 + k / 3)in check_set set inArrayLabels.iter rows ~f:(funrow ->ArrayLabels.iter row ~f:(funcell -> cell#_get_style#_set_backgroundColor "#ffffff"));for i = 0 to 8 do check_row i done;for j = 0 to 8 do check_column j done;for i = 0 to 2 dofor j = 0 to 2 do check_square i j donedone;falseNow we define a function to check that the Sudoku constraints are satisfied: that no row, column, or heavy-lined square has more than one occurrence of a digit. If more than one digit occurs then we color all occurrences red. The only ocamljs-specific parts here are getting the cell contents (with
_get_value
) and setting the background color style. However, it's worth noticing the algorithm: we imperatively clear the error states for all cells, then set error states as we check each constraint. I'll revisit this in a later post about functional reactive programming. letonload()=let(rows, table)= make_board ()inletcheck = d#getElementById "check"in check#_set_onclick (Ocamljs.jsfun (check_board rows));letboard = d#getElementById "board"in ignore (board#appendChild table);;D.window#_set_onload (Ocamljs.jsfun onload)Finally we put the pieces together: make the board, insert it into the DOM, call
check_board
when the Check button is clicked, and call this setup code once the document has been loaded. See the full source for build files.By writing this in OCaml rather than directly in Javascript, we've gained the assurance of static type checking; we get to use OCaml's syntax, pattern matching, and standard library; we have a for loop that's not broken. On the flip side we have to worry about type ascription and Ocamljs.jsfun
. If you don't already think that OCaml is a better language than Javascript, this won't convince you. But perhaps the followup posts, in which I'll show how to use RPC over HTTP with orpc and functional reactive programming with froc, will tip the scales for you.