Stuart
Stuart

Reputation: 4268

lang.LazySeq cannot be cast to IPersistantVector

In the process of learning Clojure.

I have a function to draw a random card from the deck

(defn draw-random-card
  [cards]
  (let [card (rand-nth cards)
        index (.indexOf cards card)]
    {:card card :remaining-cards (concat (subvec cards 0 index)
                                         (subvec cards (inc index)))}))

Running it:

(draw-random-card ["Ace" 2 3 4 5 6 7 8 9 10 "Jack" "Queen" "King"])
=> {:card 4, :remaining-cards ("Ace" 2 3 5 6 7 8 9 10 "Jack" "Queen" "King")}

I'd like to call it twice and get 2 cards out but the second time it calls it, it will pass the reduced deck from the first call.

IN the end I'd like to have the 2 cards and the reduced deck to use later.

I would have thought I could do something like:

(def full-deck ["Ace" 2 3 4 5 6 7 8 9 10 "Jack" "Queen" "King"])
(let [first-draw (draw-random-card full-deck)
       first-card (:drawn-card first-draw)
       second-draw (draw-random-card (:remaining-cards first-draw)) 
       second-card (:drawn-card second-draw)
       remaining-deck (:remaining-cards second-draw)]
   (println "First card: " first-card)
   (println "Second card: " second-card)
   (println "Remaining deck:" remaining-deck))

However, I'm obviously doing something dumb here as I get the error:

Execution error (ClassCastException) at aceyducey.core/draw-random-card (form-init3789790823166246683.clj:5).
clojure.lang.LazySeq cannot be cast to clojure.lang.IPersistentVector

I think the problem is in the line

second-draw (draw-random-card (:remaining-cards first-draw))]

Because remaining-cards isn't a vector?

Which means

concat (subvec cards 0 index)
           (subvec cards (inc index)))}))

Isn't returning a vector? Rather a lazy sequence ???

But at this point I'm lost.

Help!

Upvotes: 0

Views: 342

Answers (2)

Alan Thompson
Alan Thompson

Reputation: 29976

@amalloy makes a good point: the Clojure built-in function shuffle is probably what you want:

(ns tst.demo.core
  (:use demo.core tupelo.core tupelo.test) )

(def cards [:ace 2 3 4 5 6 7 8 9 10 :jack :queen :king] )

(dotest
  (dotimes [i 3]
    (spyx (shuffle cards))))

=>

Testing tst.demo.core
(shuffle cards) => [:king :jack 6 2 9 10 :ace 4 8 5 3 :queen 7]
(shuffle cards) => [2 :jack 7 9 :queen 8 5 3 4 :ace 10 :king 6]
(shuffle cards) => [7 :queen :jack 4 3 :king 6 :ace 2 10 5 8 9]

This and much more is available at the Clojure CheatSheet. Be sure to bookmark it and always keep a browser tab open with it.

Upvotes: 3

Alan Thompson
Alan Thompson

Reputation: 29976

concat returns a lazy sequence. You can coerce it into a vector by using:

(vec (concat ...))

Here is the full code with a test:

(ns tst.demo.core
  (:use demo.core tupelo.core tupelo.test))

(defn draw-random-card
  [cards]
  (let [card  (rand-nth cards)
        index (.indexOf cards card)]
    {:drawn-card card :remaining-cards (vec (concat (subvec cards 0 index)
                                              (subvec cards (inc index))))}))

(def full-deck ["Ace" 2 3 4 5 6 7 8 9 10 "Jack" "Queen" "King"])

(dotest
  (let [first-draw     (draw-random-card full-deck)
        first-card     (:drawn-card first-draw)
        second-draw    (draw-random-card (:remaining-cards first-draw))
        second-card    (:drawn-card second-draw)
        remaining-deck (:remaining-cards second-draw)]
    (println "First card: " first-card)
    (println "Second card: " second-card)
    (println "Remaining deck:" remaining-deck))

  )

and result:

-------------------------------
   Clojure 1.10.0    Java 12
-------------------------------

Testing tst.demo.core
First card:  Queen
Second card:  King
Remaining deck: [Ace 2 3 4 5 6 7 8 9 10 Jack]

Update:

To be specific, the problem was the call to subvec in the 2nd iteration of your code. Here is an example:

(dotest
  (let [vals   (vec (range 10))     ; a vector
        s1     (subvec vals 2 4)    ; so `subvec` works
        s2     (subvec vals 6)      ; and again
        lazies (concat s1 s2)]      ; creates a lazy sez
    (is= [2 3] (spyxx s1))
    (is= [6 7 8 9] (spyxx s2))
    (is= [2 3 6 7 8 9] (spyxx lazies))
    (throws? (subvec lazies 0 2)))) ; ***** can't call `subvec` on a non-vector (lazy sequence here) *****

with result:

s1     => <#clojure.lang.APersistentVector$SubVector [2 3]>
s2     => <#clojure.lang.APersistentVector$SubVector [6 7 8 9]>
lazies => <#clojure.lang.LazySeq (2 3 6 7 8 9)>

so by coercing the output of concat to a vector, the call to subvec succeeds on the next time through the function.

So, in hindsight, a better solution would have been to coerce the input to a vector like so:

(let [cards   (vec cards)
      card    (rand-nth cards)
      index   (.indexOf cards card)]
  {:drawn-card card 
   :remaining-cards (vec (concat (subvec cards 0 index)
                                 (subvec cards (inc index))))}))

Update #2

If you don't want to coerce your input to a vector, you can use the .subList() function via Java interop:

(dotest
  (spyxx (.subList (concat (range 5) (range 10 15)) 5 10))
  (spyxx (.subList (range 10) 2 5))
  (spyxx (.subList (vec (range 10)) 2 5))
  (spyxx (subvec (vec (range 10)) 2 5))
  (throws? (subvec (range 10) 2 5)))   ; *** not allowed ***

with result

(.subList (concat (range 5) (range 10 15)) 5 10)   
        => <#java.util.ArrayList$SubList [10 11 12 13 14]>

(.subList (range 10) 2 5) 
        => <#java.util.Collections$UnmodifiableRandomAccessList [2 3 4]>

(.subList (vec (range 10)) 2 5) 
        => <#clojure.lang.APersistentVector$SubVector [2 3 4]>

(subvec (vec (range 10)) 2 5) 
        => <#clojure.lang.APersistentVector$SubVector [2 3 4]>

Upvotes: 1

Related Questions