HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Clojure Tic Tac Toe solver

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
clojuretoetictacsolver

Problem

I am going through the 4clojure problems and I currently just solved the Tic Tac Toe analyzer:

(fn [board]
    (let [vr (for [x (range 3)]
              (get-in board[(+ x 0) (+ x 0)]))
          vl (for [x (range 3)]
               (get-in board [(+ x) (- 2 x)]))
          columns (apply map vector board)
          checks (concat board [vl] [vr] columns)]
      (letfn [(check-board [[row & res]]
                (if row
                  (let [dist (distinct row)]
                      (if (or (= dist [:o]) (= dist [:x]))
                        (first dist)
                        (check-board res)))
                  nil))]
        (check-board checks))))


I'm looking for any feedback about how I could make the code more terse.

Could I have created the diagonal vectors in one expression?

I don't like that I had to use recursion but I could not think of a way of using something like reduce because I don't think there is a way to short circuit reduce if you find the result midway through processing all the values.

Is there a way that I could avoid recursion with this problem?

Solution

Some Minor Points

(+ x 0) and (+x) can be replaced by just x, in (get-in board[(+ x 0) (+ x 0)])) and (get-in board [(+ x) (- 2 x)])) respectively.

You can omit the nil in else position in if : (if row ... nil) \$\rightarrow\$ (if row ...)

Idiomatic way of checking if something is one of a number of things is using a set of those values as a predicate: (or (= dist [:o]) (= dist [:x])) \$\rightarrow\$ (#{[:o] [:x]} dist)

Rename row to something not misleading. Since you named the collection checks you could name the iteration variable check.

Your Questions


Could I have created the diagonal vectors in one expression?

Yes. If you factor out the differing portions, namely [x x] and [x (- 2 x)], you get:

(let [diagonals (for [f [#(list % %) #(list % (- 2 %))]]
                  (for [x (range 3)]
                    (get-in board (f x))))


Since diagonals is already a sequence, you replace
checks (concat board [vl] [vr] columns)] with checks (concat board diagonals columns)


Is there a way that I could avoid recursion with this problem?

Recursive processing of collections/sequences can be straightforwardly transformed to for expressions. Your :

(letfn [(check-board [[row & res]]
                (if row
                  (let [dist (distinct row)]
                      (if (or (= dist [:o]) (= dist [:x]))
                        (first dist)
                        (check-board res)))
                  nil))]
        (check-board checks))))


becomes this:

(first 
  (for [row checks 
        :let [dist (distinct row)] 
        :when (or (= dist [:o]) (= dist [:x]))]
    (first dist)))


Note for is lazy, therefore (first (for ... is equivalent to your short-circuit return.

These Changes Applied Altogether

(fn [board]
  (let [diagonals (for [f [#(list % %) #(list % (- 2 %))]]
                    (for [x (range 3)]
                      (get-in board (f x))))
        columns (apply map vector board)
        checks (concat board diagonals columns)]
    (first 
      (for [check checks 
            :let [dist (distinct check)] 
            :when (#{[:o] [:x]} dist)]
        (first dist)))))


Final Point

Also check out some, every?, keep, etc. which process collections with a predicate; and which this exercise probably wants you to learn. For example some is probably what you mean by short-cut evaluation reduce.

Code Snippets

(let [diagonals (for [f [#(list % %) #(list % (- 2 %))]]
                  (for [x (range 3)]
                    (get-in board (f x))))
(letfn [(check-board [[row & res]]
                (if row
                  (let [dist (distinct row)]
                      (if (or (= dist [:o]) (= dist [:x]))
                        (first dist)
                        (check-board res)))
                  nil))]
        (check-board checks))))
(first 
  (for [row checks 
        :let [dist (distinct row)] 
        :when (or (= dist [:o]) (= dist [:x]))]
    (first dist)))
(fn [board]
  (let [diagonals (for [f [#(list % %) #(list % (- 2 %))]]
                    (for [x (range 3)]
                      (get-in board (f x))))
        columns (apply map vector board)
        checks (concat board diagonals columns)]
    (first 
      (for [check checks 
            :let [dist (distinct check)] 
            :when (#{[:o] [:x]} dist)]
        (first dist)))))

Context

StackExchange Code Review Q#62979, answer score: 2

Revisions (0)

No revisions yet.