I'm iterating through a list, building up state as I go, and occasionally when I encounter a certain sentinel, I return a result. If I was doing this in Python, I would lazily yield
the results, tracking state in the function's local scope as I go:
# this is simplified for illustration
def yielder(input_list):
state = 0
for item in input_list:
if item = 'SENTINEL':
yield state * 2
state = 0
else:
state += item
yielder([1, 5, 2, 5, 'SENTINEL', 4, 6, 7]) # [26, 34]
My first implementation uses reduce
, but that's not as good as yield
because:
iterate
could be used to mitigate the latter, but i don't actually want to return something for every input item, so it would require more munging.
What's an idiomatic way to do this in Clojure?
Yield is a keyword in Python that is used to return from a function without destroying the states of its local variable and when the function is called, the execution starts from the last yield statement. Any function that contains a yield keyword is termed a generator. Hence, yield is what makes a generator.
At least in this very simple test, yield is faster than append.
A normal function has a 'return' statement. 'return' stops the execution but 'yield' pauses the execution and resumes at the same point. Generators are memory efficient.
Conclusion. Like other programming languages, Python can return a single value, but in this, we can use yield statements to return more than one value for the function. The function that uses the yield keyword is known as a generator function.
You can build this yourself using lazy-seq as you mention or you could use partition
and reduce
to split the problem into phases then thread them together. I'll use the thread-last macro to show each step on it's own:
user> (->> [1, 5, 2, 5, :SENTINEL, 4, 6, 7] ;; start with data
(partition-by #(= :SENTINEL %)) ;; ((1 5 2 5) (:SENTINEL) (4 6 7))
(take-nth 2) ;; ((1 5 2 5) (4 6 7))
(map #(* 2 (reduce + %)))) ;; the map here keeps it lazy
(26 34)
and here it is usin lazy-seq directly:
user> (defn x [items]
(when (seq items)
(lazy-seq (cons (* 2 (reduce + (take-while #(not= :SENTINEL %) items)))
(x (rest (drop-while #(not= :SENTINEL %) items)))))))
#'user/x
user> (x [1, 5, 2, 5, :SENTINEL, 4, 6, 7])
(26 34)
The Tupelo library has a way to do this using lazy-gen
/yield
which mimics a Python generator function:
(ns xyz
(:require [tupelo.core :as t] ))
(def data-1 [1 5 2 5 :SENTINEL 4 6 7] )
(def data-2 [1 5 2 5 :SENTINEL 4 6 7 :SENTINEL] )
(defn yielder [vals]
(t/lazy-gen
(let [state (atom 0)]
(doseq [item vals]
(if (= :SENTINEL item)
(do
(t/yield (* 2 @state))
(reset! state 0))
(swap! state + item))))))
(yielder data-1) => (26)
(yielder data-2) => (26 34)
Note that the original problem description had a bug, since the cumulative state is only output when the :SENTENEL
tag is encountered. The different outputs for data-1
and data-2
illustrate the problem.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With