1
votes

I want to write a function in clojure which would return a map give a string of the form "key1$value1,key2$value2". I came up with this.

(defn get-map
  "Returns a map of key value pairs from a string formatted in the form 'key1$value1,key2$value2'"
  [line]
  (let [result {}]
    (for [item (split line #",")]
      (let [pair (split item #"\$")]
        (assoc result (nth pair 0)
          (if (= (.size pair) 2) (nth pair 1) ""))))))

Although it works the only problem with this code is that it returns the map inside a list.

=>(get-map "key1$value1,key2,value2")
({"key1" "value1"} {"key2" "value2"})

I tried to return the result as the last expression of the first let form but then the result is an empty map.

(defn get-map
  "Returns a map of key value pairs from a string formatted in the form 'key1$value1,key2$value2'"
  [line]
  (let [result {}]
    (for [item (split line #",")]
      (let [pair (split item #"\$")]
        (assoc result (nth pair 0)
          (if (= (.size pair) 2) (nth pair 1) ""))))
    result))

=>(get-map "key1$value1,key2,value2")
{}

Two questions -

  1. How should I modify the function to return just the map, without the list wrapper?
  2. Why did returning the result as the last expression of the first let form return an empty map?

Also if you have suggestions to write the same function in a better, more idiomatic clojure way that would be appreciated.

3

3 Answers

2
votes
(defn get-map
  "Returns a map of key value pairs from a string formatted in the form 'key1$value1,key2$value2'"
  [line]
  (->> (clojure.string/split line #",")
       (mapcat #(clojure.string/split % #"\$"))
       (apply hash-map)))

user> (get-map "key1$value1,key2$value2")
{"key1" "value1", "key2" "value2"}

Answer/hint on question 1: For always returns a sequence. It is list comprehension in Clojure.

Answer/hint on question 2: Clojure's datastructures are immutable. This means that assoc-ing on an existing map doesn't alter the existing map, but returns a new one.

2
votes

The for form does lazy list comprehension, so is not appropriate for sequentially updating a structure. You can use for to produce your list of pairs, and reduce to package them into a map. Or, use into, which does the reduction:

(defn get-map [line]
  (into {}
    (for [item (split line #",")]
      (split item #"\$"))))

Another possible implementation using re-seq:

(defn get-map [s]
  (into {} (map #(subvec % 1) (re-seq #"([^\$]+)\$([^,]+),*" s))))
2
votes

In Clojure, for isn't a loop it's a list comprehension. Basically it takes each item in your collection and binds it to whatever name you specified (item in this case) and then evaluates the expression (let [pair... in this case). The results of each of those evaluations are returned in a sequence.

For example:

(for [item (split "key1$value1,key2$value2" #",")] 
  item)
;returns: ("key1$value1" "key2$value2")

In your function it's for that actually creates the list. Names bound by let are immutable so each time for evaluates its expression result is still an empty map. That's why you're getting a list of maps with a single entry in each one.

Here's a way to produce a list of key-value pairs that uses the not-found capability of nth to provide a default if the $ is missing:

(for [item (split "key1$value1,key2$value2,key3" #",")]
  (let [pair (split item #"\$")] 
    [(nth pair 0) (nth pair 1 "")]))
;returns: (["key1" "value1"] ["key2" "value2"] ["key3" ""])

Then you can use into to turn that into a map. Here's the complete function:

(defn get-map   
  "Returns a map of key value pairs from a string formatted in the form 'key1$value1,key2$value2'"
  [line]
  (into {}
    (for [item (split line #",")]
      (let [pair (split item #"\$")] 
        [(nth pair 0) (nth pair 1 "")]))))