How easy would it be to use a multimethod that could handle multiple arguments? And what is the point of having such methods? Wouldn’t they add complexity to the problem? I deferred answering those questions as much as I could while developing the Tic Tac Toe.

The first step of the project was to build a simple game: ‘X’ would play first, ‘O’ would play second, and both players would be humans. At each player’s turn, select-spot would ask for the user input. That was it.

The second step was to add a computer player. At that point, I started thinking about implementing polymorphism, so that I could have just one function able to handle different players: if player had the role human, select-spot would ask for the user input; if the player had the role computer, it would generate a random number.

After learning about protocols, records and multimethods, I realized that it was too much trouble for something that could have a simpler solution. My decision was to identify the player as a map that would hold the values of its marker and its role. For instance, { :role :human :marker :x }.

I solved the problem with the following function:

(defn select-spot
  [player board-length]
  (if (= :human (:role player))
    (human/get-human-spot)
    (computer/get-computer-spot board-length)))

That worked alright until the next step of the project: add an unbeatable computer player. Then, I had not only one, but several functions that should behave differently according to the role of a player.

Protocols

The first solution I tried was to use a protocol, a set of methods that must be implemented by all data structures that extend it. This is a simple example of a protocol and how it can be used:

; all data structures that use the protocol 'Stringification'
; must implement 'stringify' and 'shout'

(defprotocol Stringification
  (stringify [this])
  (shout [this]))

; extend vectors
(extend-type clojure.lang.PersistentVector
  Stringification
  (stringify [this] (clojure.string/join (map str this)))
  (shout [this] (str (clojure.string/upper-case (stringify this)) "!!!"))))

; extend integers
(extend-type java.lang.Long
  Stringification
  (stringify [this] (str this))
  (shout [this] (str this "!!!")))

; extend strings
(extend-type java.lang.String
  Stringification
  (stringify [this] this)
  (shout [this] (str (clojure.string/upper-case this) "!!!")))

; handling vectors
(stringify ["a" 1 2]) ;-> "a12"
(shout [1 2 a]) ;-> "12A!!!"

; handing integers
(stringify 1) ;-> "1"
(shout 1) ;-> "1!!!"

; handling strings
(stringify "foo") ;-> "foo"
(shout "foo") ;-> "FOO!!!"

For the Tic Tac Toe, the idea was to create a Player protocol and extend the records Human, EasyComputer and UnbeatableComputer. It was not very practical, though. Protocols do not support functions with optional arguments, which would make the implementation very complicated: select-spot needs no argument if player is a human; it needs one argument (board-length) if player is an easy-computer; and it needs multiple arguments if player is an unbeatable-computer.

Multimethods

The second option was, obviously, to have a multimethod that could handle optional arguments. The difference between them is that a protocol is a set of different methods, while multimethod is a single method.

It was not a simple task, and new questions kept coming up: Would the complexity of the arguments structure make it more difficult to implement and use a method? Should the namespaces and their functions be reorganized? How recursion handle optional arguments?

The first thing I tried was define the multimethod in one namespace and having the methods implementation in different ones. However, because I have been using alias to require functions from another namespaces, having methods in different files would defeat the purpose of a multimethod. I would still have to use conditionals: if player is a human, use human/select-spot; if player is easy-computer, use easy/select-spot and so on. Not very smart.

After reordering files and namespaces, I came up with this solution:

(defrecord Player [marker role ai value])

(defmulti select-spot (fn [player & params] (:role player)))

(defmethod select-spot :human
  [player & params]
  (let [input (helpers/clean-string (read-line))]
    (if (helpers/is-int? input)
      (helpers/input-to-number input)
      (recur player params))))

(defmethod select-spot :easy-computer
  [player params]
  (rand-int (:board-length params)))

(defmethod select-spot :hard-computer
  [player params]
  (negamax/best-move (:board params)
                     (:current-player params)
                     (:opponent params)
                     (:depth params)))

This post has an update


apprenticeship

clojure