Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds the interval operator. #810

Merged
merged 5 commits into from
Jan 9, 2021
Merged

Adds the interval operator. #810

merged 5 commits into from
Jan 9, 2021

Conversation

mluisbrown
Copy link
Contributor

Adds the interval operator with functionality similar to that of the operator with the same name in ReactiveX.

Allows specifying any Sequence, including infinite sequences such as 0... (which is the sequence used if none is provided, to mimic the ReactiveX behaviour).

This can be useful, for example, to make a "typewriter" effect of characters appearing on the screen at regularly spaced intervals.

Checklist

  • Updated CHANGELOG.md.

@mluisbrown mluisbrown changed the title Added the interval operator. Adds the interval operator. Dec 21, 2020
@andersio
Copy link
Member

andersio commented Jan 1, 2021

In case you missed it, we have a more generalized collect(every:on:) operator with two customizable options. Could you share your use case if you still deemed it achievable only with interval()?

/// Forward the latest values on `scheduler` every `interval`.
///
/// - note: If `self` terminates while values are being accumulated,
/// the behaviour will be determined by `discardWhenCompleted`.
/// If `true`, the values will be discarded and the returned producer
/// will terminate immediately.
/// If `false`, that values will be delivered at the next interval.
///
/// - parameters:
/// - interval: A repetition interval.
/// - scheduler: A scheduler to send values on.
/// - skipEmpty: Whether empty arrays should be sent if no values were
/// accumulated during the interval.
/// - discardWhenCompleted: A boolean to indicate if the latest unsent
/// values should be discarded on completion.
///
/// - returns: A producer that sends all values that are sent from `self`
/// at `interval` seconds apart.
public func collect(every interval: DispatchTimeInterval, on scheduler: DateScheduler, skipEmpty: Bool = false, discardWhenCompleted: Bool = true) -> SignalProducer<[Value], Error> {

@mluisbrown
Copy link
Contributor Author

mluisbrown commented Jan 4, 2021

The collect(every:on:) operator is not quite the same thing, and I'm not sure it can be used to replicate what can be achieved using interval. For example:

SignalProducer("abcde")
    .collect(every: .milliseconds(50), on: QueueScheduler.main, discardWhenCompleted: false)
    .startWithValues {
        print("\(Date()): \($0)")
}

will print 2021-01-04 12:13:03 +0000: ["a", "b", "c", "d", "e"], ie all the values a collected and emitted at the same time.

Whereas:

SignalProducer.interval("abcde", interval: .milliseconds(50), on: QueueScheduler.main)
    .startWithValues {
        print("\(Date()): \($0)")
}

will print each value in the sequence at 50ms intervals. One use case for this is for animating the appearance of text in the UI, as in this example:

AnimatedTitle.mp4

@mluisbrown
Copy link
Contributor Author

The alternative to using interval for the use case above is something like this:

SignalProducer(title)
    .flatMap(.concat) { char in
        SignalProducer(value: char)
            .delay(0.05, on: QueueScheduler.main)
    }

@mluisbrown
Copy link
Contributor Author

Other than that, interval exists as an operator in other FRP libraries so it would be nice to have it here also.

@andersio
Copy link
Member

andersio commented Jan 4, 2021

@mluisbrown

Seems like I might have misunderstood what interval() does in Rx.

Judging by your description, it seems SignalProducer.Type.timer should satisfy your need, except that it emits the target date as the value, instead of repeating a predefined constant.

SignalProducer
    .timer(.milliseconds(5), on: QueueScheduler.main)
    .map { date in (date, "abcde") }

    // or
    .map(value: "abcde")

@mluisbrown
Copy link
Contributor Author

@andersio

it seems SignalProducer.Type.timer should satisfy your need

Wouldn't the example you provided just emit the value "abcde" every 50 milliseconds? What I'm trying to do is emit each element of the sequence at the defined interval, until it ends (or for ever, in the case of an infinite sequence).

time value
0.050 "a"
0.100 "b"
0.150 "c"
0.200 "d"
0.250 "e"

FWIW: I tried creating this using timer and was unable to, ending up with the solution using flatMap and delay until I created a more generic interval solution.

@andersio
Copy link
Member

andersio commented Jan 4, 2021

Argh, missed the part of emitting elements in the collection one by one. Somehow thought it was repeating the string literal regularly.

I think zip would help in this use case?

SignalProducer.zip(
    SignalProducer.timer(.milliseconds(5), on: QueueScheduler.main),
    SignalProducer("Hello world!") // `SignalProducer<Character, Never>
)
.scan(into: "") { $0.append($1.1) }

Should work alright for small arrays/sequences.

interval() would indeed be more efficient though for sequences that are large or must be lazily consumed.

@mluisbrown
Copy link
Contributor Author

mluisbrown commented Jan 4, 2021

The zip with timer and scan approach would work. So would the flatMap and delay approach I mentioned above. There are probably a few other approaches that would work.

The idea of adding the interval operator though is to have an efficient solution that can work for any sequence of any length, including lazy sequences.

Given there is prior art for this in Rx, what is the objection to adding it?

When I needed this functionality I was sure there would be an operator for it, and a bit surprised that there wasn't.


it("shouldn't overflow on a real scheduler") {
let scheduler = QueueScheduler.makeForTesting()
let producer = SignalProducer.interval("abc", interval: .seconds(3), on: scheduler)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe provide an astronomically large sequence with repeatElement?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

@andersio
Copy link
Member

andersio commented Jan 4, 2021

@mluisbrown There is no objection, more about trying to establish the need/value other than RxSwift parity (which has never been a USP of RAS). :)

@mluisbrown
Copy link
Contributor Author

mluisbrown commented Jan 5, 2021

other than RxSwift parity (which has never been a USP of RAS). :)

😄 I just meant that the fact that it already exists in ReactiveX is one more factor in evaluating the usefulness.

FWIW interval in RxSwift is not really the same thing, and is basically just a version of timer.

The prior art I was referring to was from ReactiveX.io

@andersio andersio merged commit d1c7b8d into ReactiveCocoa:master Jan 9, 2021
@RuiAAPeres
Copy link
Member

Nice. 🤠

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants