Skip to content

Commit f347ff8

Browse files
authored
Add scanMap (#695)
* Add `transduce` * Add documentation on `transduce` * Update CHANGELOG.md * Rename `transduce` to `scanMap` * Use `scanMap` for `scan` implementation * Sort method declaration order * Fix documentation
1 parent 6f308d6 commit f347ff8

File tree

6 files changed

+187
-10
lines changed

6 files changed

+187
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
1. New operator `materializeResults` and `dematerializeResults` (#679, kudos to @ra1028)
4646
1. New convenience initializer for `Action` that takes a `ValidatingProperty` as its state (#637, kudos to @Marcocanc)
4747
1. Fix legacy date implementation. (#683, kudos to @shoheiyokoyama)
48+
1. New operator `scanMap`. (#695, kudos to @inamiy)
4849

4950
# 4.0.0
5051

Sources/Event.swift

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -690,33 +690,51 @@ extension Signal.Event {
690690
}
691691
}
692692

693-
internal static func scan<U>(into initialResult: U, _ nextPartialResult: @escaping (inout U, Value) -> Void) -> Transformation<U, Error> {
693+
internal static func reduce<U>(into initialResult: U, _ nextPartialResult: @escaping (inout U, Value) -> Void) -> Transformation<U, Error> {
694694
return { action, _ in
695695
var accumulator = initialResult
696696

697697
return { event in
698-
action(event.map { value in
698+
switch event {
699+
case let .value(value):
699700
nextPartialResult(&accumulator, value)
700-
return accumulator
701-
})
701+
case .completed:
702+
action(.value(accumulator))
703+
action(.completed)
704+
case .interrupted:
705+
action(.interrupted)
706+
case let .failed(error):
707+
action(.failed(error))
708+
}
702709
}
703710
}
704711
}
705712

713+
internal static func reduce<U>(_ initialResult: U, _ nextPartialResult: @escaping (U, Value) -> U) -> Transformation<U, Error> {
714+
return reduce(into: initialResult) { $0 = nextPartialResult($0, $1) }
715+
}
716+
717+
internal static func scan<U>(into initialResult: U, _ nextPartialResult: @escaping (inout U, Value) -> Void) -> Transformation<U, Error> {
718+
return self.scanMap(into: initialResult, { result, value -> U in
719+
nextPartialResult(&result, value)
720+
return result
721+
})
722+
}
723+
706724
internal static func scan<U>(_ initialResult: U, _ nextPartialResult: @escaping (U, Value) -> U) -> Transformation<U, Error> {
707725
return scan(into: initialResult) { $0 = nextPartialResult($0, $1) }
708726
}
709727

710-
internal static func reduce<U>(into initialResult: U, _ nextPartialResult: @escaping (inout U, Value) -> Void) -> Transformation<U, Error> {
728+
internal static func scanMap<State, U>(into initialState: State, _ next: @escaping (inout State, Value) -> U) -> Transformation<U, Error> {
711729
return { action, _ in
712-
var accumulator = initialResult
730+
var accumulator = initialState
713731

714732
return { event in
715733
switch event {
716734
case let .value(value):
717-
nextPartialResult(&accumulator, value)
735+
let output = next(&accumulator, value)
736+
action(.value(output))
718737
case .completed:
719-
action(.value(accumulator))
720738
action(.completed)
721739
case .interrupted:
722740
action(.interrupted)
@@ -727,8 +745,12 @@ extension Signal.Event {
727745
}
728746
}
729747

730-
internal static func reduce<U>(_ initialResult: U, _ nextPartialResult: @escaping (U, Value) -> U) -> Transformation<U, Error> {
731-
return reduce(into: initialResult) { $0 = nextPartialResult($0, $1) }
748+
internal static func scanMap<State, U>(_ initialState: State, _ next: @escaping (State, Value) -> (State, U)) -> Transformation<U, Error> {
749+
return scanMap(into: initialState) { state, value in
750+
let new = next(state, value)
751+
state = new.0
752+
return new.1
753+
}
732754
}
733755

734756
internal static func observe(on scheduler: Scheduler) -> Transformation<Value, Error> {

Sources/Signal.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,6 +1324,34 @@ extension Signal {
13241324
public func scan<U>(into initialResult: U, _ nextPartialResult: @escaping (inout U, Value) -> Void) -> Signal<U, Error> {
13251325
return flatMapEvent(Signal.Event.scan(into: initialResult, nextPartialResult))
13261326
}
1327+
1328+
/// Accumulate all values from `self` as `State`, and send the value as `U`.
1329+
///
1330+
/// - parameters:
1331+
/// - initialState: The state to use as the initial accumulating state.
1332+
/// - next: A closure that combines the accumulating state and the latest value
1333+
/// from `self`. The result would be "next state" and "output" where
1334+
/// "output" would be forwarded and "next state" would be used in the
1335+
/// next call of `next`.
1336+
///
1337+
/// - returns: A producer that sends the output that is computed from the accumuation.
1338+
public func scanMap<State, U>(_ initialState: State, _ next: @escaping (State, Value) -> (State, U)) -> Signal<U, Error> {
1339+
return flatMapEvent(Signal.Event.scanMap(initialState, next))
1340+
}
1341+
1342+
/// Accumulate all values from `self` as `State`, and send the value as `U`.
1343+
///
1344+
/// - parameters:
1345+
/// - initialState: The state to use as the initial accumulating state.
1346+
/// - next: A closure that combines the accumulating state and the latest value
1347+
/// from `self`. The result would be "next state" and "output" where
1348+
/// "output" would be forwarded and "next state" would be used in the
1349+
/// next call of `next`.
1350+
///
1351+
/// - returns: A producer that sends the output that is computed from the accumuation.
1352+
public func scanMap<State, U>(into initialState: State, _ next: @escaping (inout State, Value) -> U) -> Signal<U, Error> {
1353+
return flatMapEvent(Signal.Event.scanMap(into: initialState, next))
1354+
}
13271355
}
13281356

13291357
extension Signal where Value: Equatable {

Sources/SignalProducer.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,6 +1493,34 @@ extension SignalProducer {
14931493
return core.flatMapEvent(Signal.Event.scan(into: initialResult, nextPartialResult))
14941494
}
14951495

1496+
/// Accumulate all values from `self` as `State`, and send the value as `U`.
1497+
///
1498+
/// - parameters:
1499+
/// - initialState: The state to use as the initial accumulating state.
1500+
/// - next: A closure that combines the accumulating state and the latest value
1501+
/// from `self`. The result would be "next state" and "output" where
1502+
/// "output" would be forwarded and "next state" would be used in the
1503+
/// next call of `next`.
1504+
///
1505+
/// - returns: A producer that sends the output that is computed from the accumuation.
1506+
public func scanMap<State, U>(_ initialState: State, _ next: @escaping (State, Value) -> (State, U)) -> SignalProducer<U, Error> {
1507+
return core.flatMapEvent(Signal.Event.scanMap(initialState, next))
1508+
}
1509+
1510+
/// Accumulate all values from `self` as `State`, and send the value as `U`.
1511+
///
1512+
/// - parameters:
1513+
/// - initialState: The state to use as the initial accumulating state.
1514+
/// - next: A closure that combines the accumulating state and the latest value
1515+
/// from `self`. The result would be "next state" and "output" where
1516+
/// "output" would be forwarded and "next state" would be used in the
1517+
/// next call of `next`.
1518+
///
1519+
/// - returns: A producer that sends the output that is computed from the accumuation.
1520+
public func scanMap<State, U>(into initialState: State, _ next: @escaping (inout State, Value) -> U) -> SignalProducer<U, Error> {
1521+
return core.flatMapEvent(Signal.Event.scanMap(into: initialState, next))
1522+
}
1523+
14961524
/// Forward only values from `self` that are not considered equivalent to its
14971525
/// immediately preceding value.
14981526
///

Tests/ReactiveSwiftTests/SignalProducerLiftingSpec.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,55 @@ class SignalProducerLiftingSpec: QuickSpec {
423423
}
424424
}
425425

426+
describe("scanMap(_:_:)") {
427+
it("should update state and output separately") {
428+
let (baseProducer, observer) = SignalProducer<Int, NoError>.pipe()
429+
let producer = baseProducer.scanMap(false) { state, value -> (Bool, String) in
430+
return (true, state ? "\(value)" : "initial")
431+
}
432+
433+
var lastValue: String?
434+
435+
producer.startWithValues { lastValue = $0 }
436+
437+
expect(lastValue).to(beNil())
438+
439+
observer.send(value: 1)
440+
expect(lastValue) == "initial"
441+
442+
observer.send(value: 2)
443+
expect(lastValue) == "2"
444+
445+
observer.send(value: 3)
446+
expect(lastValue) == "3"
447+
}
448+
}
449+
450+
describe("scanMap(into:_:)") {
451+
it("should update state and output separately") {
452+
let (baseProducer, observer) = SignalProducer<Int, NoError>.pipe()
453+
let producer = baseProducer.scanMap(into: false) { (state: inout Bool, value: Int) -> String in
454+
defer { state = true }
455+
return state ? "\(value)" : "initial"
456+
}
457+
458+
var lastValue: String?
459+
460+
producer.startWithValues { lastValue = $0 }
461+
462+
expect(lastValue).to(beNil())
463+
464+
observer.send(value: 1)
465+
expect(lastValue) == "initial"
466+
467+
observer.send(value: 2)
468+
expect(lastValue) == "2"
469+
470+
observer.send(value: 3)
471+
expect(lastValue) == "3"
472+
}
473+
}
474+
426475
describe("reduce(_:_:)") {
427476
it("should accumulate one value") {
428477
let (baseProducer, observer) = SignalProducer<Int, Never>.pipe()

Tests/ReactiveSwiftTests/SignalSpec.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,55 @@ class SignalSpec: QuickSpec {
834834
}
835835
}
836836

837+
describe("scanMap(_:_:)") {
838+
it("should update state and output separately") {
839+
let (baseSignal, observer) = Signal<Int, NoError>.pipe()
840+
let signal = baseSignal.scanMap(false) { state, value -> (Bool, String) in
841+
return (true, state ? "\(value)" : "initial")
842+
}
843+
844+
var lastValue: String?
845+
846+
signal.observeValues { lastValue = $0 }
847+
848+
expect(lastValue).to(beNil())
849+
850+
observer.send(value: 1)
851+
expect(lastValue) == "initial"
852+
853+
observer.send(value: 2)
854+
expect(lastValue) == "2"
855+
856+
observer.send(value: 3)
857+
expect(lastValue) == "3"
858+
}
859+
}
860+
861+
describe("scanMap(into:_:)") {
862+
it("should update state and output separately") {
863+
let (baseSignal, observer) = Signal<Int, NoError>.pipe()
864+
let signal = baseSignal.scanMap(into: false) { (state: inout Bool, value: Int) -> String in
865+
defer { state = true }
866+
return state ? "\(value)" : "initial"
867+
}
868+
869+
var lastValue: String?
870+
871+
signal.observeValues { lastValue = $0 }
872+
873+
expect(lastValue).to(beNil())
874+
875+
observer.send(value: 1)
876+
expect(lastValue) == "initial"
877+
878+
observer.send(value: 2)
879+
expect(lastValue) == "2"
880+
881+
observer.send(value: 3)
882+
expect(lastValue) == "3"
883+
}
884+
}
885+
837886
describe("reduce(_:_:)") {
838887
it("should accumulate one value") {
839888
let (baseSignal, observer) = Signal<Int, Never>.pipe()

0 commit comments

Comments
 (0)