2
votes

Sorry for the bad title 'cause I don't know how to describe in 10 words. Here's the detail:

I'd like to loop a file in format like:

a:1 b:2...

I want to loop each line, collect all 'k:v' into a hash-map.

{ a 1, b 2...}

I initialize a hash-map in a 'let' form, then loop all lines with 'for' inside let form. In each loop step, I use 'assoc' to update the original hash-map.

(let [myhash {}]

(for [line #{"A:1 B:2" "C:3 D:4"}

   :let [pairs (clojure.string/split line #"\s")]]

(for [[k v] (map #(clojure.string/split %1 #":") pairs)]

 (assoc myhash k (Float. v)))))

But in the end I got a lazy-seq of hash-map, like this:

{ {a 1, b 2...} {x 98 y 99 z 100 ...} }

I know how to 'merge' the result now, but still don't understand why 'for' inside 'let' return a list of result.

What I'm confused is: does the 'myhash' in the inner 'for' refers to the 'myhash' declared in the 'let' form every time? If I do want a list of hash-map like the output, is this the idiomatic way in Clojure ?

3

3 Answers

2
votes

Clojure "for" is a list comprehension, so it creates list. It is NOT a for loop. Also, you seem to be trying to modify the myhash, but Clojure's datastructures are immutable. The way I would approach the problem is to try to create a list of pair like (["a" 1] ["b" 2] ..) and the use the (into {} the-list-of-pairs)

1
votes

If the file format is really as simple as you're describing, then something much more simple should suffice:

(apply hash-map (re-seq #"\w+" (slurp "your-file.txt")))

I think it's more readable if you use the ->> threading macro:

(->> "your-file.txt" slurp (re-seq #"\w+") (apply hash-map))

The slurp function reads an entire file into a string. The re-seq function will just return a sequence of all the words in your file (basically the same as splitting on spaces and colons in this case). Now you have a sequence of alternating key-value pairs, which is exactly what hash-map expects...

I know this doesn't really answer your question, but you did ask about more idiomatic solutions.

I think @dAni is right, and you're confused about some fundamental concepts of Clojure (e.g. the immutable collections). I'd recommend working through some of the exercises on 4Clojure as a fun way to get more familiar with the language. Each time you solve a problem, you can compare your own solution to others' solutions and see other (possibly more idomatic) ways to solve the problem.


Sorry, I didn't read your code very thorougly last night when I was posting my answer. I just realized you actually convert the values to Floats. Here are a few options.

1) partition the sequence of inputs into key/val pairs so that you can map over it. Since you now how a sequence of pairs, you can use into to add them all to a map.

(->> "kvs.txt" slurp (re-seq #"\w") (partition 2)
     (map (fn [[k v]] [k (Float. v)])) (into {}))

2) Declare an auxiliary map-values function for maps and use that on the result:

(defn map-values [m f]
  (into {} (for [[k v] m] [k (f v)])))

(->> "your-file.txt" slurp (re-seq #"\w+")
     (apply hash-map) (map-values #(Float. %)))

3) If you don't mind having symbol keys instead of strings, you can safely use the Clojure reader to convert all your keys and values.

(->> "your-file.txt" slurp (re-seq #"\w+")
     (map read-string) (apply hash-map))

Note that this is a safe use of read-string because our call to re-seq would filter out any hazardous input. However, this will give you longs instead of floats since numbers like 1 are long integers in Clojure

0
votes

Does the myhash in the inner for refer to the myhash declared in the let form every time?

Yes.

  • The let binds myhash to {}, and it is never rebound. myhash is always {}.
  • assoc returns a modified map, but does not alter myhash.

So the code can be reduced to

(for [line ["A:1 B:2" "C:3 D:4"]
      :let [pairs (clojure.string/split line #"\s")]]
  (for [[k v] (map #(clojure.string/split %1 #":") pairs)]
    (assoc {} k (Float. v))))

... which produces the same result:

(({"A" 1.0} {"B" 2.0}) ({"C" 3.0} {"D" 4.0}))

If I do want a list of hash-map like the output, is this the idiomatic way in Clojure?

No.

See @DaoWen's answer.