Related
I've been working with RxSwift for a while now, just switched to Combine and I am trying to wrap my head around this specific .filter behaviour. Here's a short playground example:
import Combine
let publisher = [1, 2, 3, 4, 5]
.publisher
.share()
let filter1 = publisher
.filter { $0 == 1 }
.print("filter1")
let filter2 = publisher
.filter { $0 == 2 }
.print("filter2")
Publishers
.Merge(filter1, filter2)
.sink {
print("Result is: \($0)")
}
the output is
filter1: receive subscription: (Multicast)
filter1: request unlimited
filter1: receive value: (1)
Result is: 1
filter1: receive finished
filter2: receive subscription: (Multicast)
filter2: request unlimited
filter2: receive finished
What surprises me is that Result is: 2 is never called because the stream finishes. I could remove .share() operator which would result in receiving both values as I'd expect
filter1: receive subscription: ([1])
filter1: request unlimited
filter1: receive value: (1)
Result is: 1
filter1: receive finished
filter2: receive subscription: ([2])
filter2: request unlimited
filter2: receive value: (2)
Result is: 2
filter2: receive finished
But what if my publisher is an API call and I don't want to create a duplicate network request? Which is exactly the case I am trying to handle now and it's also why I need to use .share() operator.
Any better explanation why is this happening and how to handle a case where you want to filter a stream, do a separate logic in each stream and then merge the results back together?
So there are a couple of different things going on here.
First, the [1, 2, 3].publisher works different than Observable.from([1, 2, 3]). The latter emits the values once per cycle, while the former emits all the values back to back. The Publisher example works more like this in Rx:
Observable<Int>.create { observer in
[1, 2, 3, 4, 5].forEach {
observer.onNext($0)
}
observer.onCompleted()
return Disposables.create()
}
Because of this, in the Observable.from case, the emissions are not complete by the time the filter2 observable is subscribed to. So even if you omit the share() both "Result is: 1" and "Result is: 2" will be emitted.
Second, the share() operator also works differently. By default, the RxSwift share operator will reset the Observable once all subscriptions are disposed (it's a reference counting share). In the Combine case, the share operator makes the publisher connectable and then connects to it. Essentially, it's the same as the .share(replay: 0, scope: .forever) operator in RxSwift (something I have never needed in Rx BTW).
So the Rx code that is equivalent to the Combine code you posted is actually this:
let observable = emitSequence([1, 2, 3, 4, 5])
.share(replay: 0, scope: .forever)
let filter1ʹ = observable
.filter { $0 == 1 }
.debug("filterʹ1")
let filter2ʹ = observable
.filter { $0 == 2 }
.debug("filterʹ2")
Observable.merge(filter1ʹ, filter2ʹ)
.subscribe(onNext: {
print("Resultʹ is: \($0)")
})
func emitSequence<S>(_ sequence: S) -> Observable<S.Element> where S: Sequence {
Observable.create { observer in
sequence.forEach {
observer.onNext($0)
}
observer.onCompleted()
return Disposables.create()
}
}
All this said the practical aspect of dealing with an API call is fine. In that case, the assumption is that the call won't immediately return (it will take a cycle at least) and since it's one-shot, as long as you make sure you aren't resubscribing to the Observable, the fact that share() doesn't reset isn't a problem.
I have two Publishers A and B. They are imbalanced as in A will emit 3 values, then complete, B will only emit 1 value, then complete (A actually can emit a variable number, B will remain 1 if that helps):
A => 1, 2, 3
B => X
B also runs asynchronously and will likely only emit a value after A already emitted its second value (see diagram above). (B might also only emit any time, including after A already completed.)
I'd like to publish tuples of A's values combined with B's values:
(1, X) (2, X) (3, X)
combineLatest is not up for the job as it will skip the first value of A and only emit (2, X) and (3, X). zip on the other hand will not work for me, because B only emits a single value.
I am looking for an elegant way to accomplish this. Thanks!
Edit and approach to a solution
A bit philosophical, but I think there is fundamental question if you want to go the zip or combineLatest route. You definitely need some kind of storage for the faster publisher to buffer events while you wait for the slower to start emitting values.
One solution might be to create a publisher that collects events from A until B emits and then emits all of the collected events and continues emitting what A gives. This is actually possible through
let bufferedSubject1 = Publishers.Concatenate(
prefix: Publishers.PrefixUntilOutput(upstream: subject1, other: subject2).collect().flatMap(\.publisher),
suffix: subject1)
PrefixUntilOutput will collect everything until B emits (subject2) and then switch to just regularly passing the output of it.
However if you run
let cancel = bufferedSubject1.combineLatest(subject2)
.sink(receiveCompletion: { c in
print(c)
}, receiveValue: { v in
print(v)
})
you are still missing the first value from A (1,X) -- this seems to be a bit like a race condition: Will bufferedSubject1 have all values emitted first or does subject2 provide a value to combineLatest first?
What I think is interesting is that without any async calls, the behavior seems to be undefined. If you run the sample below, sometimes™️ you get all values emitted. Sometimes you are missing out on (1,X). Since there is no async calls and no dispatchQueue switching here, I would even assume this is a bug.
You can "dirty fix" the race condition by providing a delay or even just a receive(on: DispatchQueue.main) between bufferedSubject1 and combineLatest, so that before we continue the pipeline, we hand back control to the DispatchQueue and let subject2 emit to combineLatest.
However, I would not deem that elegant and still looking for a solution that uses zip semantics but without having to create an infinite collection of the same value (which does not play well with sequential processing and unlimited demand, the way I see it).
Sample:
var subject1 = PassthroughSubject<Int, Never>()
var subject2 = PassthroughSubject<String, Never>()
let bufferedSubject1 = Publishers.Concatenate(prefix: Publishers.PrefixUntilOutput(upstream: subject1, other: subject2).collect().flatMap(\.publisher),
suffix: subject1)
let bufferedSubject2 = Publishers.Concatenate(prefix: Publishers.PrefixUntilOutput(upstream: subject2, other: subject1).collect().flatMap(\.publisher),
suffix: subject2)
let cancel = bufferedSubject1.combineLatest(subject2)
.sink(receiveCompletion: { c in
print(c)
}, receiveValue: { v in
print(v)
})
subject1.send(1)
subject1.send(2)
subject2.send("X")
subject2.send(completion: .finished)
subject1.send(3)
subject1.send(completion: .finished)
Ok, this was an interesting challenge and though it seemed deceptively simple, I couldn't find a simple elegant way.
Here's a working approach (though hardly elegant) that seems to not suffer from the race condition of using PrefixUntilOutput/Concatenate combo.
The idea is to use combineLatest, but one that emits as soon as the first publisher emits, with the other value being nil so that we don't lose the initial values. Here's a convenience operator that does that that I called combineLatestOptional:
extension Publisher {
func combineLatestOptional<Other: Publisher>(_ other: Other)
-> AnyPublisher<(Output?, Other.Output?), Failure>
where Other.Failure == Failure {
self.map { Optional.some($0) }.prepend(nil)
.combineLatest(
other.map { Optional.some($0) }.prepend(nil)
)
.dropFirst() // drop the first (nil, nil)
.eraseToAnyPublisher()
}
}
Armed with the above, the second step in the pipeline uses Scan to collect values into an accumulator until the other publisher emits the first value. There are 4 states of the accumulator that I'm representing this state with a State<L, R> type:
fileprivate enum State<L, R> {
case initial // before any one publisher emitted
case left([L]) // left emitted; right hasn't emitted
case right([R]) // right emitted; left hasn't emitted
case final([L], [R]) // final steady-state
}
And the final operator combineLatestLossless is implemented like so:
extension Publisher {
func combineLatestLossless<Other: Publisher>(_ other: Other)
-> AnyPublisher<(Output, Other.Output), Failure>
where Failure == Other.Failure {
self.combineLatestOptional(other)
.scan(State<Output, Other.Output>.initial, { state, tuple in
switch (state, tuple.0, tuple.1) {
case (.initial, let l?, nil): // left emits first value
return .left([l]) // -> collect left values
case (.initial, nil, let r?): // right emits first value
return .right([r]) // -> collect right values
case (.left(let ls), let l?, nil): // left emits another
return .left(ls + [l]) // -> append to left values
case (.right(let rs), nil, let r?): // right emits another
return .right(rs + [r]) // -> append to right values
case (.left(let ls), _, let r?): // right emits after left
return .final(ls, [r]) // -> go to steady-state
case (.right(let rs), let l?, _): // left emits after right
return .final([l], rs) // -> go to steady-state
case (.final, let l?, let r?): // final steady-state
return .final([l], [r]) // -> pass the values as-is
default:
fatalError("shouldn't happen")
}
})
.flatMap { status -> AnyPublisher<(Output, Other.Output), Failure> in
if case .final(let ls, let rs) = status {
return ls.flatMap { l in rs.map { r in (l, r) }}
.publisher
.setFailureType(to: Failure.self)
.eraseToAnyPublisher()
} else {
return Empty().eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
}
The final flatMap creates a Publishers.Sequence publisher from all the accumulated values. In the final steady-state, each array would just have a single value.
The usage is simple:
let c = pub1.combineLatestLossless(pub2)
.sink { print($0) }
zip on the other hand will not work for me, because B only emits a single value.
Correct, so fix it so that that’s not true. Start a pipeline at B. Using flatmap turn its signal into a publisher for a sequence of that signal, repeated. Zip that with A.
Example:
import UIKit
import Combine
func delay(_ delay:Double, closure:#escaping ()->()) {
let when = DispatchTime.now() + delay
DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
class ViewController: UIViewController {
var storage = Set<AnyCancellable>()
let s1 = PassthroughSubject<Int,Never>()
let s2 = PassthroughSubject<String,Never>()
override func viewDidLoad() {
super.viewDidLoad()
let p1 = s1
let p2 = s2.flatMap { (val:String) -> AnyPublisher<String,Never> in
let seq = Array(repeating: val, count: 100)
return seq.publisher.eraseToAnyPublisher()
}
p1.zip(p2)
.sink{print($0)}
.store(in: &storage)
delay(1) {
self.s1.send(1)
}
delay(2) {
self.s1.send(2)
}
delay(3) {
self.s1.send(3)
}
delay(2.5) {
self.s2.send("X")
}
}
}
Result:
(1, "X")
(2, "X")
(3, "X")
Edit
After stumbling on this post I wonder if the problem in your example is not related to the PassthroughSubject:
PassthroughSubject will drop values if the downstream has not made any demand for them.
and in fact using :
var subject1 = Timer.publish(every: 1, on: .main, in: .default, options: nil)
.autoconnect()
.measureInterval(using: RunLoop.main, options: nil)
.scan(DateInterval()) { res, interval in
.init(start: res.start, duration: res.duration + interval.magnitude)
}
.map(\.duration)
.map { Int($0) }
.eraseToAnyPublisher()
var subject2 = PassthroughSubject<String, Never>()
let bufferedSubject1 = Publishers.Concatenate(prefix: Publishers.PrefixUntilOutput(upstream: subject1, other: subject2).collect().flatMap(\.publisher),
suffix: subject1)
let cancel = bufferedSubject1.combineLatest(subject2)
.sink(receiveCompletion: { c in
print(c)
}, receiveValue: { v in
print(v)
})
subject2.send("X")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
subject2.send("Y")
}
I get this output :
(1, "X")
(2, "X")
(3, "X")
(3, "Y")
(4, "Y")
(5, "Y")
(6, "Y")
And that seems to be the desired behavior.
I don't know if it is an elegant solution but you can try to use Publishers.CollectByTime :
import PlaygroundSupport
import Combine
PlaygroundPage.current.needsIndefiniteExecution = true
let queue = DispatchQueue(label: "com.foo.bar")
let cancellable = letters
.combineLatest(indices
.collect(.byTimeOrCount(queue, .seconds(1), .max))
.flatMap { indices in indices.publisher })
.sink { letter, index in print("(\(index), \(letter))") }
indices.send(1)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
indices.send(2)
indices.send(3)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
letters.send("X")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3.3) {
indices.send(4)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) {
letters.send("Y")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3.7) {
indices.send(5)
indices.send(6)
}
Output :
(X, 1)
(X, 2)
(X, 3)
(Y, 3)
(Y, 4)
(Y, 5)
(Y, 6)
Algorithmically speaking, you need to:
wait until B emits an event, collect all elements that A emits
store the element you just received from B
emit the pair of elements emitted so far by A
emit the rest of elements that A emits after B emitted it's element
An implementation of the above algoritm can be done like this:
// `share` makes sure that we don't cause unwanted side effects,
// like restarting the work `A` does, as we subscribe multiple
// times to this publisher
let sharedA = a.share()
// state, state, state :)
var latestB: String!
var cancel = sharedA
// take all elements until `B` emits
.prefix(untilOutputFrom: b.handleEvents(receiveOutput: { latestB = $0}))
// wait on those elements
.collect()
// uncollect them
.flatMap { $0.publisher }
// make sure we deliver the rest of elements from `A`
.append(sharedA)
// now, pair the outputs together
.map { ($0, latestB) }
.sink(receiveValue: { print("\($0)") })
Maybe there's a way to avoid the state (latestB), and use a pure pipeline, couldn't yet find it, though.
P.S. As an added bonus, if B is expected to emit more than one element, than with a simple change we can support this scenario too:
let sharedA = a.share()
let sharedB = b.handleEvents(receiveOutput: { latestB = $0}).share()
var latestB: String!
var cancel = sharedA.prefix(untilOutputFrom: sharedB)
.collect()
.flatMap { $0.publisher }
.append(sharedA)
.map { ($0, latestB)}
.sink(receiveValue: { print("\($0)") })
In Combine, using only the built-in operators, is there a way to skip an operator on the first value but then apply that operator for all subsequent values?
Consider the following:
publisher
.debounce(...)
.sink(...)
In this arrangement, debounce will wait for the specified timeout to elapse before passing on the value to sink. However, there are many times when you only want debounce to kick-in after the first element. For example, if the user is trying to filter a list of contacts, it's very possible that they only enter one letter into a text field. If that's the case, the application should probably start filtering immediately, without having to wait for the debounce to timeout.
I'm aware of the Drop publishers, but I can't seem to find a combination of them that will perform more of a "skip" operation such that the sink receives every value, but the debounce is ignored on the first value.
Something like the following:
publisher
.if_first_element_passthrough_to_sink(...), else_debounce(...)
.sink(...)
Is something like this possible with the built-in operators?
Clarification
Some clarification since my original posting wasn't as clear as it should have been... The answer provided by Asperi below is very close, but ideally the first element in a sequence is always delivered, then debounce would kick in.
Imagine the user is typing the following:
A B C ... (pauses typing for a few seconds) ... D ... (pauses) ... E F G
What I would like is:
A, D and E are delivered immediately.
B C is coalesced into just C using debounce
F G is coalesced into just G using debounce
If I correctly understood your needs it can be achieved based on Concatenate as like the following (in pseudo-code):
let originalPublisher = ...
let publisher = Publishers.Concatenate(
prefix: originalPublisher.first(),
suffix: originalPublisher.debounce(for: 0.5, scheduler: RunLoop.main))
.eraseToAnyPublisher()
so, prefix just sends first element downstream from original publisher and finished, afterwards suffix just pass all following elements using debounce.
In your particular case of debounce, you might prefer the behavior of throttle. It sends the first element immediately, and then sends no more than one element per interval.
Anyway, can you do it with Combine built-ins? Yes, with some difficulty. Should you? Maybe…
Here's a marble diagram of your goal:
Each time a value goes into the kennyc-debouncer, it starts a timer (represented by a shaded region). If a value arrives while the timer is running, the kennyc-debouncer saves the value and restarts the timer. When the timer expires, if any values arrived while the timer was running, the kennyc-debouncer emits the latest value immediately.
The scan operator allows us to keep state that we mutate each time an input arrives. We need to send two kinds of inputs into scan: the outputs from the upstream publisher, and timer firings. So let's define a type for those inputs:
fileprivate enum DebounceEvent<Value> {
case value(Value)
case timerFired
}
What kind of state do we need inside our scan transform? We definitely need the scheduler, the interval, and the scheduler options, so that we can set timers.
We also need a PassthroughSubject we can use to turn timer firings into inputs to the scan operator.
We can't actually cancel and restart a timer, so instead, when the timer fires, we'll see whether it should have been restarted. If so, we'll start another timer. So we need to know whether the timer is running, and what output to send when the timer fires, and the restart time for the timer if restarting is necessary.
Since scan's output is the entire state value, we also need the state to include the output value to send downstream, if any.
Here's the state type:
fileprivate struct DebounceState<Value, S: Scheduler> {
let scheduler: S
let interval: S.SchedulerTimeType.Stride
let options: S.SchedulerOptions?
let subject = PassthroughSubject<Void, Never>()
enum TimerState {
case notRunning
case running(PendingOutput?)
struct PendingOutput {
var value: Value
var earliestDeliveryTime: S.SchedulerTimeType
}
}
var output: Value? = nil
var timerState: TimerState = .notRunning
}
Now let's look at how to actually use scan with some other operators to implement the kennyc version of debounce:
extension Publisher {
func kennycDebounce<S: Scheduler>(
for dueTime: S.SchedulerTimeType.Stride,
scheduler: S,
options: S.SchedulerOptions? = nil
) -> AnyPublisher<Output, Failure>
{
let initialState = DebounceState<Output, S>(
scheduler: scheduler,
interval: dueTime,
options: options)
let timerEvents = initialState.subject
.map { _ in DebounceEvent<Output>.timerFired }
.setFailureType(to: Failure.self)
return self
.map { DebounceEvent.value($0) }
.merge(with: timerEvents)
.scan(initialState) { $0.updated(with: $1) }
.compactMap { $0.output }
.eraseToAnyPublisher()
}
}
We start by constructing the initial state for the scan operator.
Then, we create a publisher that turns the Void outputs of the state's PassthroughSubject into .timerFired events.
Finally, we construct our full pipeline, which has four stages:
Turn the upstream outputs (from self) into .value events.
Merge the value events with the timer events.
Use scan to update the debouncing state with the value and timer events. The actual work is done in an updated(with:) method we'll add to DebounceState below.
Map the full state down to just the value we want to pass downstream, and discard nulls (which happen when upstream events get suppressed by debouncing).
All that's left is to write the updated(with:) method. It looks at each incoming event's type (value or timerFired) and the state of the timer to decide what the new state should be and, if necessary, set a new timer.
extension DebounceState {
func updated(with event: DebounceEvent<Value>) -> DebounceState<Value, S> {
var answer = self
switch (event, timerState) {
case (.value(let value), .notRunning):
answer.output = value
answer.timerState = .running(nil)
scheduler.schedule(after: scheduler.now.advanced(by: interval), tolerance: .zero, options: options) { [subject] in subject.send() }
case (.value(let value), .running(_)):
answer.output = nil
answer.timerState = .running(.init(value: value, earliestDeliveryTime: scheduler.now.advanced(by: interval)))
case (.timerFired, .running(nil)):
answer.output = nil
answer.timerState = .notRunning
case (.timerFired, .running(.some(let pendingOutput))):
let now = scheduler.now
if pendingOutput.earliestDeliveryTime <= now {
answer.output = pendingOutput.value
answer.timerState = .notRunning
} else {
answer.output = nil
scheduler.schedule(after: pendingOutput.earliestDeliveryTime, tolerance: .zero, options: options) { [subject] in subject.send() }
}
case (.timerFired, .notRunning):
// Impossible!
answer.output = nil
}
return answer
}
}
Does it work? Let's test it:
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
let subject = PassthroughSubject<String, Never>()
let q = DispatchQueue.main
let start = DispatchTime.now()
let cfStart = CFAbsoluteTimeGetCurrent()
q.asyncAfter(deadline: start + .milliseconds(100)) { subject.send("A") }
// A should be delivered at start + 100ms.
q.asyncAfter(deadline: start + .milliseconds(200)) { subject.send("B") }
q.asyncAfter(deadline: start + .milliseconds(300)) { subject.send("C") }
// C should be delivered at start + 800ms.
q.asyncAfter(deadline: start + .milliseconds(1100)) { subject.send("D") }
// D should be delivered at start + 1100ms.
q.asyncAfter(deadline: start + .milliseconds(1800)) { subject.send("E") }
// E should be delivered at start + 1800ms.
q.asyncAfter(deadline: start + .milliseconds(1900)) { subject.send("F") }
q.asyncAfter(deadline: start + .milliseconds(2000)) { subject.send("G") }
// G should be delivered at start + 2500ms.
let ticket = subject
.kennycDebounce(for: .milliseconds(500), scheduler: q)
.sink {
print("\($0) \(((CFAbsoluteTimeGetCurrent() - cfStart) * 1000).rounded())") }
Output:
A 107.0
C 847.0
D 1167.0
E 1915.0
G 2714.0
I'm not sure why the later events are so delayed. It could just be playground side effects.
I've started to learn reactive-cocoa from couple of days, today I was playing with the flatten method of the reactivecocoa (reactiveSwift), I tried executing the snippet given for the concat flattening in the documentation Basic operators. Here's the snippet:
let (lettersSignal, lettersObserver) = Signal<String, NoError>.pipe()
let (numbersSignal, numbersObserver) = Signal<String, NoError>.pipe()
let (signal, observer) = Signal<Signal<String, NoError>, NoError>.pipe()
signal.flatten(.concat).observeValues { print($0) }
observer.send(value: lettersSignal)
observer.send(value: numbersSignal)
observer.sendCompleted()
numbersObserver.send(value: "1") // nothing printed
lettersObserver.send(value: "a") // prints "a"
lettersObserver.send(value: "b") // prints "b"
numbersObserver.send(value: "2") // nothing printed
lettersObserver.send(value: "c") // prints "c"
lettersObserver.sendCompleted() // prints "1, 2"
numbersObserver.send(value: "3") // prints "3"
numbersObserver.sendCompleted()
As per the documentation and the interactive visualization diagram (RAC marbles - flatten(.concat) visual diagram, the output should have been something like this,
First it should have printed letter stream i.e,
a, b, c
& once the letterStream has completed it should've printed the number stream i.e.
1, 2, 3
So the final output of this observation should've been
[a, b, c, 1, 2, 3]
However, the concatenated output I'm seeing is,
[a, b, c, 3]
why is this so? Why only the latest value of the numberStream is being printed? Instead of printing the entire number stream values once the letter stream was completed.
Please let me know if I've misunderstood something. Cheers.
As mentioned in the ReactiveSwift's slack channel, that is the expected outcome.
Quoting the documentation:
The outer event stream is started observed. Each subsequent event stream is not observed until the preceeding one has completed.
So numbersSignal will only send values, once lettersObserver has completed.
Is it possible to break from a Groovy .each{Closure}, or should I be using a classic loop instead?
Nope, you can't abort an "each" without throwing an exception. You likely want a classic loop if you want the break to abort under a particular condition.
Alternatively, you could use a "find" closure instead of an each and return true when you would have done a break.
This example will abort before processing the whole list:
def a = [1, 2, 3, 4, 5, 6, 7]
a.find {
if (it > 5) return true // break
println it // do the stuff that you wanted to before break
return false // keep looping
}
Prints
1
2
3
4
5
but doesn't print 6 or 7.
It's also really easy to write your own iterator methods with custom break behavior that accept closures:
List.metaClass.eachUntilGreaterThanFive = { closure ->
for ( value in delegate ) {
if ( value > 5 ) break
closure(value)
}
}
def a = [1, 2, 3, 4, 5, 6, 7]
a.eachUntilGreaterThanFive {
println it
}
Also prints:
1
2
3
4
5
Replace each loop with any closure.
def list = [1, 2, 3, 4, 5]
list.any { element ->
if (element == 2)
return // continue
println element
if (element == 3)
return true // break
}
Output
1
3
No, you can't break from a closure in Groovy without throwing an exception. Also, you shouldn't use exceptions for control flow.
If you find yourself wanting to break out of a closure you should probably first think about why you want to do this and not how to do it. The first thing to consider could be the substitution of the closure in question with one of Groovy's (conceptual) higher order functions. The following example:
for ( i in 1..10) { if (i < 5) println i; else return}
becomes
(1..10).each{if (it < 5) println it}
becomes
(1..10).findAll{it < 5}.each{println it}
which also helps clarity. It states the intent of your code much better.
The potential drawback in the shown examples is that iteration only stops early in the first example. If you have performance considerations you might want to stop it right then and there.
However, for most use cases that involve iterations you can usually resort to one of Groovy's find, grep, collect, inject, etc. methods. They usually take some "configuration" and then "know" how to do the iteration for you, so that you can actually avoid imperative looping wherever possible.
Just using special Closure
// declare and implement:
def eachWithBreak = { list, Closure c ->
boolean bBreak = false
list.each() { it ->
if (bBreak) return
bBreak = c(it)
}
}
def list = [1,2,3,4,5,6]
eachWithBreak list, { it ->
if (it > 3) return true // break 'eachWithBreak'
println it
return false // next it
}
You can't break from a Groovy each loop, but you can break from a java "enhanced" for loop.
def a = [1, 2, 3, 4, 5, 6, 7]
for (def i : a) {
if (i < 2)
continue
if (i > 5)
break
println i
}
Output:
2
3
4
5
This might not fit for absolutely every situation but it's helped for me :)
I agree with other answers not to use an exception to break an each. I also do not prefer to create an extra closure eachWithBreak, instead of this I prefer a modern approach: let's use the each to iterate over the collection, as requested, but refine the collection to contain only those elements to be iterated, for example with findAll:
collection.findAll { !endCondition }.each { doSomething() }
For example, if we what to break when the counter == 3 we can write this code (already suggested):
(0..5)
.findAll { it < 3 }
.each { println it }
This will output
0
1
2
So far so good, but you will notice a small discrepancy though. Our end condition, negation of counter == 3 is not quite correct because !(counter==3) is not equivalent with it < 3. This is necessary to make the code work since findAll does not actually break the loop but continues until the end.
To emulate a real situation, let's say we have this code:
for (n in 0..5) {
if (n == 3)
break
println n
}
but we want to use each, so let's rewrite it using a function to simulate a break condition:
def breakWhen(nr) { nr == 3 }
(0..5)
.findAll { !breakWhen(it) }
.each { println it }
with the output:
0
1
2
4
5
now you see the problem with findAll. This does not stop, but ignores that element where the condition is not met.
To solve this issues, we need an extra variable to remember when the breaking condition become true. After this moment, findAll must ignore all remaining elements.
This is how it should look like:
def breakWhen(nr) { nr == 3 }
def loop = true
(0..5)
.findAll {
if (breakWhen(it))
loop = false
!breakWhen(it) && loop
} .each {
println it
}
with the output:
0
1
2
That's what we want!
(1..10).each{
if (it < 5)
println it
else
return false
You could break by RETURN. For example
def a = [1, 2, 3, 4, 5, 6, 7]
def ret = 0
a.each {def n ->
if (n > 5) {
ret = n
return ret
}
}
It works for me!