Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Idiomatic Clojure way of mimicking Python's yield

Tags:

clojure

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:

  • The value I pass between iterations has both the loop state, and the items's i want to yield, which seems clunky
  • It's not lazy

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?

like image 661
aaronstacy Avatar asked Jan 29 '14 16:01

aaronstacy


People also ask

What is yield from Python?

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.

Is yield faster Python?

At least in this very simple test, yield is faster than append.

Does yield stop execution Python?

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.

Can I yield two values Python?

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.


2 Answers

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)
like image 175
Arthur Ulfeldt Avatar answered Oct 19 '22 08:10

Arthur Ulfeldt


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.

like image 40
Alan Thompson Avatar answered Oct 19 '22 07:10

Alan Thompson