3
\$\begingroup\$

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?

\$\endgroup\$
  • \$\begingroup\$ Once you have solved a 4Clojure problem, you can look at others' solutions. Top users' solutions can teach you a lot, though some tend to be a bit code golfy. \$\endgroup\$ – Thumbnail Nov 4 '14 at 13:40
2
\$\begingroup\$

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.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ I can't thank you enough for taking the time to do this. Great points! \$\endgroup\$ – dagda1 Sep 17 '14 at 9:58
1
\$\begingroup\$

Stimulated by your question, I've had another go at solving this.

Could you have created the diagonal vectors in one expression?

Yes. See below.

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

Yes. The problem is not properly recursive. You can use the sequence functions to

  • generate the lines,
  • scan through them, and
  • to scan each line if need be (avoided below).

And, by the way, there is now the reduced function which allows you to exit a reduce.


The idea is to do it in two stages:

  1. From the given rows, generate the columns and the diagonals, hence all the lines.
  2. Test each line in turn to see if it is purely :xs or purely :os. If so, that's the answer.

Hopefully, whatever we use to run through the lines will return nil if it doesn't find a solving one.

(defn ttt [rows]
  (let [cols (apply map vector rows)
        diags (map #(map % (range 3)) [#((rows %) %) #((rows %) (- 2 %))])
        lines (concat rows cols diags)]
    (first (some (comp #{#{:x} #{:o}} set) lines))))

I now see that @abuzittingillifirca's solution exploits all the techniques employed here, though preferring for to some and map.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.