2
\$\begingroup\$

I am writing a function to filter a "dictionary" by the "subject" of its definitions.

The dictionary data structure is a hash-map of this kind:

{ "word1" {:foo :bar
           :definitions [{:definition "def1" :subject ["Subject1" "Subject2"]}
                         {:definition "def2" :subject ["Subject3" "Subject1"]}]}
  "word2" {:foo :baz
           :definitions [{:definition "def3" :subject "Subject2"}
                         {:definition "def4" :subject ["Subject2" "Subject3"]}]}

The function needs to return a new dictionary with only definitions of the specified subject (no word entry if no definition available).

I came up with two (working) different functions that returns the same exact result.
The first version uses map, the second version uses loop/recur.

map version:

(defn filter-by-subject [dict subject]
  (apply hash-map
         (flatten (map (fn [[word entry]]
                           (let [definitions (definitions-for-subject entry subject)]
                              (if (> (count definitions) 0)
                                  [word (assoc entry :definitions definitions)]
                                  [])))
                       dict))))

loop/recur version:

(defn filter-by-subject2 [dict subject]
  (loop [old-dict dict
         new-dict {}]
     (if-let [[word entry] (first old-dict)]
             ;; then                                                                                                                        
             (recur (dissoc old-dict word)
                    (let [definitions (definitions-for-subject entry subject)]
                       (if (> (count definitions) 0)
                           (assoc new-dict word
                                  (assoc entry :definitions definitions))
                           new-dict)))
             ;; else                                                                                                                        
             new-dict)))

Supporting functions:

(defn subjects-of-definition [definition]
  (let [sbjs (:subject definition)]
     (if (coll? sbjs)
         sbjs
         [sbjs])))

(defn definitions-for-subject [entry subject]
  (into [] (filter (fn [definition]
                       (some #{subject} (subjects-of-definition definition)))
                   (:definitions entry))))

I would like to hear your opinion on these functions.

  1. Do they follow the common "functional style"?
  2. Which version of the two do you find more readable, clear, understandable or even faster?

UPDATE

A little clarification.
If a word has two definitions, one of subjects A and B, and one of subjects B and C, and we filter by subject A, the resulting dictionary will have only the first definition of word (the one of subjects A and B).

Some examples:

=> (def dictionary { "Bit" { :definitions [
                                { :definition "A unit of information storage on a computer"
                                  :subject "IT" }
                                { :definition "Beijing Institute of Technology"
                                  :subject ["Acronyms" "Education"] }] }
                     "Matrix" { :definitions [
                                { :definition "A rectangular array of numbers, symbols or expressions"
                                  :subject "Mathematics"}
                                { :definition "A 1999 science fiction film"
                                  :subject "Movies"}]}})
#'user/dictionary

=> (filter-by-subject dictionary "Education")
{"Bit" {:definitions [{:subject ["Acronyms" "Education"], :definition "Beijing Institute of Technology"}]}}

=> (filter-by-subject dictionary "IT")
{"Bit" {:definitions [{:subject "IT", :definition "A unit of information storage on a computer"}]}}

=> (filter-by-subject dictionary "Movies")
{"Matrix" {:definitions [{:subject "Movies", :definition "A 1999 science fiction film"}]}}

(note that, for example, filtering by "Education" deletes the "IT" definition of "Bit".)

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Updated answer:

Thanks for clarifying the question, the examples helped. Here is my new attempt:

(defn update-subjects [dict subj]
  (map (fn [[k {val :definitions}]]
         [k {:definitions
             (filter (fn [{s :subject}] 
                       ((into #{} (flatten [s])) subj)) val)}]) dict))

(defn my-filter [dict subj]
  (into {} (->> (update-subjects dict subj)
                (remove (comp empty? :definitions second)))))

(= (my-filter dictionary "IT") (filter-by-subject dictionary "IT"))
=> true

in this solution, I first remove all the definitions which subjects do not match, and then I remove all the entries with empty definitions. I used the threading macro (->>) for readability.


original answer:

Here is another way of implementing your subject filter:

(defn subject-filter [p m]
   (filter (fn[[k {v :definitions}]]
               (some (fn [{x :subject}] 
                        ((into #{} (flatten [x])) p)) v)) m))

This implementation makes use of two important features:

  1. Destructuring - an idiomatic method of extracting items from a data structure in clojure. I highly recommend keeping the following link handy at all times http://blog.jayfields.com/2010/07/clojure-destructuring.html
  2. Sets as a predicate - in clojure, if a set is in a function position, it acts as a function that returns true iff the first argument is in the set. So it is very idiomatic to write (if (#{1 2 3} i) .. ..) to know if i is either 1, 2 or 3. Perhaps you could even use sets instead of arrays in your data definition for :subject, which will make this code even shorter and faster, and your DS more "correct" - you could never keep two identical subjects and :subject will be a predicate of a query subject.

Just a note, you could also do without so much destructuring, and get a more concise function:

(defn my-filter [p m] 
   (filter (fn[[k v]] (some (fn [x] ((into #{} (flatten [(:subject x)])) p)) (:definitions v))) m))
\$\endgroup\$
2
  • \$\begingroup\$ Thanks for the good information! Unfortunately I already came up with a similar solution but filtering on the dictionary is not the solution. In fact if a word has two definitions, e.g. { "word" { :definitions [{:definition "One" :subject ["foo" "bar"]} {:definition "Two" :subject ["bar" "baz"]}]}}, filtering by subject "baz" this code would return the same exact dictionary while the correct solution would be { "word" { :definitions [{:definition "Two" :subject ["bar" "baz"]}]}}, i.e. only the definition with subject "baz" must be there. I'm updating the question to better clarify this. \$\endgroup\$ Jan 23, 2014 at 9:14
  • \$\begingroup\$ Hey, you are right. I gave it another shot, check the updated answer \$\endgroup\$
    – Shlomi
    Jan 23, 2014 at 14:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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