For quote-word-count
, there's a few things to note.
I think this is a good use of ->>
. Personally, I would have started the threading with (range quote-count)
instead of splitting that though. From my experience, needlessly elongating the thread call just hurts readability. I also recommend maintaining the same type of object being threaded all the way down. I find it makes the thread much easier to understand. If you're threading collections like you are here, I'd prefer that every object being threaded is a collection (whereas quote-count
is a number).
It isn't a good idea to use map
to run side effects (like starting a future
task). map
doesn't run its effect immediately, so the futures aren't started when (map (fn [_] (fetch-quote)))
runs, they're started when the pmap
begins requesting elements. I'd switch to mapv
which is strict, so the future's are started right away.
I don't think that map
ping a range
is the right approach here though; which is hinted at by your use of a wrapper (fn [_])
function. repeatedly
would be a better candidate for this situation. It's lazy though, so I'd probably add a call to vec
after it to realize it right away.
Your zero
is actually unnecessary. fnil
can be used here instead:
(update word-counts word (fnil inc 0)))
I wouldn't have parameters called quote
ideally. quote
is a rather important built-in, and it's a good idea to avoid shadowing built-ins.
I'd probably use ->>
in count-words
:
(defn count-words
[quote]
(let [cleaned-quote (clean-input quote)]
(->> (clojure.string/split cleaned-quote #" ")
(reduce update-word {}))))
I personally find that to be much more readable than the nested version.
I'll note too that Clojure has the built-in frequencies
that can do the work here:
(defn count-words
[quote]
(let [cleaned-quote (clean-input quote)]
(->> (clojure.string/split cleaned-quote #" ")
(frequencies))))
In clean-input
, I wouldn't use comp
. I think it would be much neater using ->
:
(defn clean-input
[input]
(->> input
(clojure.string/lower-case)
(remove-author)
(remove-commas)))
I really only like comp
when it's being passed to another function. Writing out ((comp g f) x)
just seems like an overly complicated way of applying x
to f
then g
.
Your functions are in reverse order for some reason. I'm not sure where you're running this that's allowing for functions to be used before they're declared.
Personally, I like having the parameter vector on the first line unless I have a doc string. I find the functions look noisy otherwise.
You're writing clojure.string/
a fair amount. You may want to require
that module to avoid verbose, qualified function calls. While you're at it, you might as well add a ns
macro call, since each file should really have one anyways:
(ns your.file.path.here
(:require [clojure.string :as s]))
Now, clojure.string/split
can be written as s/split
.
And for 3.:
The function will take the number of quotes to download as an argument and return the atom’s final value.
It wants you to return the value of the atom, not the atom itself. They want you to update an atom inside the futures; although I don't think that's necessary, or even preferable.
Here's the final code I ended up with:
(ns your.file.path.here
(:require [clojure.string :as s]))
(defn remove-author [quote]
(s/replace quote #".\n-- .*\n" ""))
(defn remove-commas [quote]
(s/replace quote #"," ""))
(defn clean-input [input]
(->> input
(s/lower-case)
(remove-author)
(remove-commas)))
(defn count-words [quote]
(let [cleaned-quote (clean-input quote)]
(->> (s/split cleaned-quote #" ")
(frequencies))))
(defn fetch-quote []
(future (slurp "https://www.braveclojure.com/random-quote")))
(defn quote-word-count [quote-count]
(->> (repeatedly quote-count fetch-quote)
(vec) ; To ensure futures start before pmap begins processing chunks
(pmap (comp count-words deref))
(apply merge-with +)))