Writing Friendlier Clojure
adambard.com
adambard.com
(defn sentence [mapping]
(->> :start
(iterate #(pick-next-word mapping %))
next
(take-until #(= (last %) \.))
(clojure.string/join " ")))
There is no loop, and it almost reads like English: "Using :start as a departure point, iteratively pick the next words until you find one that ends with a dot. Join all the words generated so far with spaces."Small catch: take-until is not yet in clojure.core, however it's easy to add to your utility toolbelt and there's already an issue open for it http://dev.clojure.org/jira/browse/CLJ-1451
I, er, may have spent a while clicking around the related docs for loop and recur searching for a better function. I remembered there was something like this out there, but I never did find it.
1) Use a #"[.]$" to detect the full stop 2) Just pass in the iteration function (partial pick-next-word mapping)
One of his rules is "If your function starts with a conditional, get rid of the conditional and look for that pattern in your function arguments." That is, move the conditional into the different versions of your function.
Also, his "Recursive Function Pattern Matching Pattern" makes recursion much easier to read. See the screenshots here and then click through to the video:
http://www.smashcompany.com/technology/the-recursive-functio...
Ultimately, we're revealing no new insight about the underlying problem and offering no new capabilities for the future.
(defn markov-chain [coll]
(let [adjacencies (partition 2 1 coll)
initial-chain {::start [(first coll)]
(last coll) [::end]}]
(reduce
(fn [chain [predecessor successor]]
(assoc chain predecessor (conj (get chain predecessor []) successor)))
initial-chain
adjacencies)))
(defn successor [chain item]
(rand-nth (get chain item)))
(defn follow-chain [chain]
(->> ::start
(iterate (partial successor chain))
(remove #{::start ::end})
(take-while some?)))
(defn sentences [text]
(clojure.string/split text #"\."))
(defn words [sentence]
(remove empty? (clojure.string/split sentence #"\s+")))
(defn markov-text [text]
(->> text
sentences
(map words)
(map markov-chain)
(apply merge-with into)))
(defn render-sentence [words]
(str (clojure.string/join " " words) \.))
(defn sentence [chain]
(render-sentence (follow-chain chain)))
I'm not saying this is in any way strictly better, but it leaves you with abstractions that can be reused, and text-specific stuff in one place that could be a separate namespace. This is what I'd look for in a code-review of a refactoring like this - not just neatness, but making abstractions explicit and separate, naming as many things as possible, etc etc. (defun wordlist-sentence (sentence)
[(opip
`@1.`
(split-str @1 #/\s+/)
(cons :start)
(remqual ""))
sentence])
(defun make-markov-mapping (sentence)
(let ((wordlist (wordlist-sentence sentence)))
(hash-from-pairs (mapcar (ret ^(,@1 (,@2))) wordlist (rest wordlist))
:equal-based)))
(defun markov-data (text)
[(opip
(split-str @1 #/\./)
(mapcar make-markov-mapping)
(reduce-left (op hash-uni @1 @2 append) @1 (hash :equal-based)))
text])
(defun pick-next-word (map word)
(let ((choices [map word]))
[choices (rand (length choices))]))
(defun sentence (map)
(for
((words ())
(this-word :start)
(next-word "X"))
((not (eql [next-word -1] #\.))
(cat-str words " "))
((set next-word (pick-next-word map this-word))
(set words ^(,*words ,next-word))
(set this-word next-word))))
Run: $ txr -i markov.tl
1> (defvar map (markov-data
"Influence in society, however, is a capital which has to be economized \
\ if it is to last. Prince Vasili knew this, and having once realized that \
\ if he asked on behalf of all who begged of him, he would soon be unable \
\ to ask for himself, he became chary of using his influence. But in \
\ Princess Drubetskaya's case he felt, after her second appeal, something \
\ like qualms of conscience. She had reminded him of what was quite true; \
\ he had been indebted to her father for the first steps in his career. \
\ Moreover, he could see by her manners that she was one of those women-- \
\ mostly mothers--who, having once made up their minds, will not rest \
\ until they have gained their end, and are prepared if necessary to go on \
\ insisting day after day and hour after hour, and even to make scenes. \
\ This last consideration moved him."))
map
2> (sentence map)
"Prince Vasili knew this, and having once realized that she was quite true; he
could see by her manners that if it is to ask for himself, he felt, after hour,
and even to last." (defun sentence (map)
(let* ((words (cdr [giterate true (op pick-next-word map) :start]))
(dot (pos-if (op eq [@1 -1] #\.) words)))
(cat-str [words 0..(succ dot)] " ")))
Requires small weak to pick-next-word (defun pick-next-word (map word)
(let ((choices [map word]))
(if choices [choices (rand (length choices))])))
giterate returns an infinite list, but we take from it up to the word which ends with dot, ignoring the endless stream of nil objects which follows.Alternative: partition the list on the ending word, then glue the first two partitions:
(defun sentence (map)
(let* ((words (cdr [giterate true (op pick-next-word map) :start]))
(part (partition-by (op eq [@1 -1] #\.) words)))
(cat-str (append (first part) (second part)) " ")))The problem here is that termination is indicated by a word which ends in a period, and that word must be included in the word list. This is a poor form of "in-band signaling".
Kludgy sequence operators which take or produce one extra item which doesn't satisfy the inclusion predicate can be avoided by proper design of the data representation which avoids this type of framing.
TXR Lisp version fixed to use the t symbol for start, nil for end, and straight giterate while true. (I.e. while the sequence element isn't nil):
(defun wordlist-sentence (sentence)
[(opip
`@1.`
(split-str @1 #/\s+/)
^(t ,*@1 nil) ;; list starts with t, ends with nil
(remqual "")) ;; blanks not permitted
sentence])
(defun make-markov-mapping (sentence)
(let ((wordlist (wordlist-sentence sentence)))
(hash-from-pairs (mapcar (ret ^(,@1 (,@2))) wordlist (rest wordlist))
:equal-based)))
(defun markov-data (text)
[(opip
(split-str @1 #/\./)
(mapcar make-markov-mapping)
(reduce-left (op hash-uni @1 @2 append) @1 (hash :equal-based)))
text])
(defun pick-next-word (map word)
(let ((choices [map word]))
[choices (rand (length choices))]))
(defun sentence (map)
(let ((words (cdr [giterate true (op pick-next-word map) t])))
(cat-str words " ")))
In this representation, the words which end with a period are still there, but they have a next-word entry in the map: they all point to nil.So we have a nice "spindle":
t
|
+---------+-------+----+
| | | |
various start ..... words
| | | |
.
:
.
:
| | |
sentence. end. words.
| | |
+---------+------+
|
nilPersonally, I favour Haskell's unfoldr http://hackage.haskell.org/package/base-4.8.1.0/docs/Data-Li... which both doesn't require the output to be the same as the intermediate values and cleanly supports termination.
(There's actually a very good reason Haskell cares about termination: it means you can be precise about side-effects. With a classic lazy list, any side effects caused by it are determined by how many items you actually evaluate.)
With explicit lambdas, decrement from 5 to zero, not including it in list:
1> (expand-right (lambda (item) (if (zerop item) nil (cons item (pred item)))) 5)
(5 4 3 2 1)
Using functional combinators: 2> [expand-right [iff zerop false [callf cons identity pred]] 5]
(5 4 3 2 1)
Include zero; bit of a mouthful: 1> [expand-right [iff null nilf [callf cons identity [iff zerop nilf pred]]] 5]
(5 4 3 2 1 0)
Though it can do it, it doesn't look like a great interface when inclusion of the last element is required.Comparison with the inclusive iterate (which I did implement and called "ginterate"--thanks for the nice name:
1> [ginterate [chain zerop not] pred 5]
(5 4 3 2 1 0)