Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure Variables and Looping

From googling around, I found that using while loops or using variables is discouraged.

Now I implemented a very simple algorithm that will read characters from an inputstream and parse accordingly: if input is 10:abcdefghej it will parse out 10 then read next 10 bytes after the colon.

The thing I am kinda lost with is how I can refactor this so it does not rely on variables.


(defn decode-string [input-stream indicator]

  (with-local-vars [length (str (char indicator) )
            delimiter (.read input-stream ) 
            string (str "")
            counter 0 ]

    (while (not(= (var-get delimiter) 58 ))
       (var-set length (str (var-get length) (char (var-get delimiter)) ))
       (var-set delimiter (.read input-stream )))

    (var-set length (new BigInteger (var-get length)) )
    (var-set counter (var-get length))

    (while (not(zero? (var-get counter) ))
       (var-set string (str (var-get string) (char (.read input-stream ))  ))
       (var-set counter (dec (var-get counter))))
    (var-get string)))

Also, I understand that the only way to declare variables is using the with-local-vars keyword. Isn't it kind of unpractical to define all the variables in one block at the beginning, or am I missing some crucial point?

like image 457
Hamza Yerlikaya Avatar asked Jun 28 '09 03:06

Hamza Yerlikaya


2 Answers

What you are writing is C code with lisp-like syntax (no offense intended). Defining a style by what you don't do is very defining, but it is not very helpful if you don't know "well, then how else?"

By the way, I don't know what indicator is supposed to do.

This is how I would approach this problem:

  1. The problem has two parts: find the number of characters to read, then read that many characters. Therefore, I would write two functions: read-count and read-item, the latter using the former.

    (defn read-count [stream]
      ;; todo
      )
    
    (defn read-item [stream]
      ;; todo
      )
    
  2. read-item first needs to determine the number of characters to read. For that, it uses the convenient function read-count that we will also define.

    (defn read-item [stream]
      (let [count (read-count stream)]
        ;; todo
        ))
    
  3. Looping is in Clojure generally best handled by using loop and recur. loop also binds variables, like let. acc is meant to accumulate the read items, but note that it is not modified in place but re-bound each iteration.

    (defn read-item [stream]
      (loop [count (read-count stream)
             acc ""]
        ;; todo
        (recur (dec count)        ; new value for count
               (str acc c)))))    ; new value for acc
    
  4. Now we need to do something in that loop: bind c to the next character, but return acc when count is 0. (zero? count) is the same as (= count 0). I annotated the if form a bit for those unfamiliar with it.

    (defn read-item [stream]
      (loop [count (read-count stream)
             acc ""]
        (if (zero? count)                  ; condition
            acc                            ; then
            (let [c (.read stream)]        ; \
              (recur (dec count)           ;  > else
                     (str acc c)))))))     ; /
    
  5. Now all we need is the read-count function. It uses a similar loop.

    (defn read-count [stream]
      (loop [count 0]
        (let [c (.read stream)]
          (if (= c ":")
              count
              (recur (+ (* count 10)
                        (Integer/parseInt c)))))))
    
  6. Test it on the REPL, debug, refactor. Does .read really return characters? Is there a better way to parse an integer?

I have not tested this, and I am a bit hampered by not having any experience nor deep knowledge of Clojure (I use Common Lisp mostly), but I think that it shows how to approach this kind of problem in a "lispy" way. Note how I don't think about declaring or modifying variables.

like image 127
Svante Avatar answered Oct 30 '22 13:10

Svante


A bit late to this party, I suppose, but the problem is much simpler if you just treat the string as a sequence of characters and use Clojure's sequence-handling primitives:

(defn read-prefixed-string [stream]
  (let [s (repeatedly #(char (.read stream)))
        [before [colon & after]] (split-with (complement #{\:}) s)
        num-chars (read-string (apply str before))]
    (apply str (take num-chars after))))

user> (let [in (java.io.StringReader. "10:abcdefghij5:klmnopqrstuvwxyz")]
        (repeatedly 2 #(read-prefixed-string in)))
("abcdefghij" "klmno")

A summary:

  • Convert the ugly, side-effectful input stream into a lazy sequence of characters, so that we can pretend it's just a string for the rest of this operation. As you can see, no more characters are actually read from the stream than are necessary to compute the result.
  • Split the string into two parts: the first half characters before the first colon, and the second half whatever is left.
  • Use destructuring to bind those parts to locals named before and after, and strip out the : while we are at it, by binding it to an unused local, named colon for descriptiveness.
  • Read before to get its numerical value
  • Take that many characters from after, and mash them all together into a string with (apply str)

Svante's answer is an excellent example of how to write loop-ish code with Clojure; I hope that mine is a good example of assembling the built-in functions so that they do what you need. Certainly both of these make the C solution look anything but "very simple"!

like image 32
amalloy Avatar answered Oct 30 '22 13:10

amalloy