swift,  reactive-swift

Flatten and some of its strategies

The power of ReactiveSwift lies within its operators.!

Flatten and some of its strategies
import Result
import ReactiveSwift
import Foundation

flatten

The power of ReactiveSwift lies within its operators.

Let’s start with a simple example.

You are working hard and almost ready to advance to the next level of Pokemon when your product manager appears out of nowhere.

“Hey geek, I have this great idea for our new product and our super smart CTO created a super secret function that does some super secret work”

func superSecretFunc(_ text:String, completion: @escaping ((Void) -> Void)) {
    DispatchQueue.global(qos: .userInitiated).async {
        let diceRoll = Int(arc4random_uniform(99) + 1)
        usleep(UInt32(diceRoll))
        print("Ran \(text) on thread \(Thread.current) for \(diceRoll) milliseconds")
        completion()
    }
}

“Your task is to run this superSecretFunc 3 times but here is the tricky part; the calls must be executed serially, i.e. one after another.”

“Sure” - you reply - “Come back in a week and I will have it done”

2 weeks later …

class CrappySolution1 {

    public func runTasks() {
        print("Solution 1 ...")
        superSecretFunc("1") {
            superSecretFunc("2") {
                superSecretFunc("3") { }
            }
        }

        Thread.sleep(forTimeInterval: 0.3)
    }
}

“Hey, I saw you completed my task in Jira!”

“Hmm, I marked the task as completed a week ago!”

“I know, I know but I was busy watching Game of Thrones; I mean Enterprise Gamification but good job, can you demonstrate it to me?”

let so1 = CrappySolution1()
so1.runTasks()


Solution 1 ...
Ran 1 on thread <NSThread: 0x7f833cd0b1e0>{number = 2, name = (null)} for 64 milliseconds
Ran 2 on thread <NSThread: 0x7f833f2000d0>{number = 3, name = (null)} for 97 milliseconds
Ran 3 on thread <NSThread: 0x7f833cd0b1e0>{number = 2, name = (null)} for 73 milliseconds

“Oh man, this is so awesome. Can we run it 5 times though?”

“Sure, come back in 2 weeks.”

Awesome, you think, I will have enough time to advance to Pokemon’s level 22.

class CrappySolution2 {

    public func runTasks() {
        print("Solution 2 ...")
        superSecretFunc("1") {
            superSecretFunc("2") {
                superSecretFunc("3") {
                    superSecretFunc("4") {
                        superSecretFunc("5") {
                        }
                    }
                }
            }
        }

    }
}

3 weeks later …

let so2 = CrappySolution2()
so2.runTasks()


Solution 2 ...
Ran 1 on thread <NSThread: 0x7f833cd0b1e0>{number = 2, name = (null)} for 52 milliseconds
Ran 2 on thread <NSThread: 0x7f833f2000d0>{number = 3, name = (null)} for 15 milliseconds
Ran 3 on thread <NSThread: 0x7f833cd0b1e0>{number = 2, name = (null)} for 3 milliseconds
Ran 4 on thread <NSThread: 0x7f833f2000d0>{number = 3, name = (null)} for 23 milliseconds
Ran 5 on thread <NSThread: 0x7f833f0004c0>{number = 5, name = (null)} for 67 milliseconds

Of course your PM will now ask you to run it 7 or maybe even 9 times.

Number of nested calls will keep on growing and maintainability of your code will keep on declining.

That is where ReactiveSwift comes to our rescue!

class ReactiveSolution1 {

    public func runTasks(times times:UInt) {
        print("Reactive Solution 1 ...")
        var producers = [SignalProducer<Void, NoError>]()

        for i in 1...times {
            let sp = SignalProducer<Void, NoError> { observer, disposable in
                superSecretFunc(String(i)) {
                    observer.send(value: ())
                    observer.sendCompleted()
                }
            }
            producers.append(sp)
        }

        let fsp = SignalProducer<SignalProducer<Void, NoError>, NoError>(producers)

        fsp.flatten(.concat)
            .on(completed: {
                print("Done!")
            }, value: {}).start()

    }
}

We can even run it a 100 times without a signle nested loop.

And that is the beauty of ReactiveSwift.
let rso1 = ReactiveSolution1()
rso1.runTasks(times: 7)

So how does it work?

A SignalProducer represents an operation or a task.

SignalProducers might seem complicated but SPs are actually simple creatures.

Anyway SPs are just an excuse to use operators.

flatten

joins SignalProducers together. In other words it has the potential of executing tasks serially, one after another.

Flatten accepts only one parameter: FlattenStrategy

Let’s take a look at some of the strategies:

  1. latest - will complete if the last SP completes.
  2. concat - will complete if all SPs complete.
  • It has an internal counter with a default value of 1.
  • Every time a SP completes it increments the counter.
  • It will disregard values as long as the counter is 0.
  • If the counter is 1 only values from the first SP are forwarded, if 2 then values from the first and the second SP are forwarded and so on.
  • Visualization
  1. merge - will complete if all SPs complete.

Let’s see it in action.

let s1 = SignalProducer<String, NoError> { observer, disposable in
    observer.send(value: "1")
//    observer.sendCompleted()
}
let s2 = SignalProducer<String, NoError> { observer, disposable in
    observer.send(value: "2")
//    observer.sendCompleted()
}
let s3 = SignalProducer<String, NoError> { observer, disposable in
    observer.send(value: "3")
    observer.sendCompleted()
}

let s5 = SignalProducer<SignalProducer<String, NoError>, NoError>([s1, s2, s3])

print("\n ---> latest")
s5.flatten(.latest)
    .on(completed: {
        print("latest completed")
    },
        value: { values in
            print(values)
    }).start()
print("\n ---> concat")
print("concat never completes because it is waiting for all inputs (producers) to complete")
s5.flatten(.concat)
    .on(completed: {
        // will never complete
        print("concat completed")
    },
        value: { values in
            print(values)
    }).start()
print("\n ---> merge")
print("like concat, merge never completes because it is waiting for all inputs (producers) to complete")
print("unlike concat, merge will send all the values from each input")
s5.flatten(.merge)
    .on(completed: {
        // will never complete
        print("merge completed")
    },
        value: { values in
            print(values)
    }).start()


 ---> latest
1
2
3
latest completed

 ---> concat
concat never completes because it is waiting for all inputs (producers) to complete
1

 ---> merge
like concat, merge never completes because it is waiting for all inputs (producers) to complete
unlike concat, merge will send all the values from each input
1
2
3

source code

Subscribe to The infinite monkey theorem

Get the latest posts delivered right to your inbox