Introduction (0:00)
My name is Rui Peres, and I’m going to cover functional reactive programming and testing.
A small story (1:05)
The motivation for this talk came from when I was having issues doing my day-to-day job because of delegation and NSNotificationCenter
in iOS. The usual way to communicate between objects and layers is cumbersome. I was feeling that there must be a better way of doing this.
I decided to do a talk some months after learning ReactiveCocoa. Compared to how verbose Objective-C is, I was happy with how things are done in ReactiveCocoa, and I wanted to share it with others doing general reactive programming.
FRP 101 (5:17)
I’m going to give you a 101, in functional reactive programming. I’ll use ReactiveCocoa for the code, but what I show you, you can still use with RX Swift, Interstellar, or Swift Bun. The concepts are the same across the frameworks.
What is functional programming?
In one sentence, it’s KVO plus NSOperation
, plus some fairy dust. And the fairy dust is to take away the bad things from KVO and NSOperation
.
KVO is a way for you to observe changes and act upon them. You can think of KVO, as the reactive bit of functional reactive programming.
KVO is cumbersome, and typically, people use it when they’re forced to, and not because they want to.
MutableProperty<T>
In ReactiveCocoa, there’s a the concept of a MutableProperty
, on a T
. T
is a generic, so it can be an integer or a string. The way you use it is quite simple:
let foo = MutableProperty(1)
// 1. observe foo
foo.signalProducer.startWithNext { newFooValue in
print(newFooValue) // prints "1"
}
// 2. modifiy foo's value
foo.value = 2
// 3. prints "2"
You start with a MutableProperty
of an integer one. Then you start observing the foo
, and because the value starts at one, you print it.
After printing one, modify the value of foo
, and it prints two again. This mimics the KVO behavior.
SignalProducer<T, E: ErrorType>
ReactiveCocoa provides a SignalProducer
. You have a T
, and you have an E
which is the error type. It can fail and so you have that E
to represent that.
let networkCall: SignalProducer<NSData, NSError> = ...
let parser: NSData -> [Painting] = ...
let paintingsProducer = networkCall.map(parser)
.startOn(QueueScheduler())
In this case, there’s a network call with NSData
along with a potential NSError
.
This is a path that goes from NSData
to an array of paintings. The same way you use optionals is same way you use an array in map, filter, and reduce. Here, I have the network call, and I map the value from an NSData
to a painting.
The startOn
is to indicate done in a background queue.
paintingsProducer.startWithNext { paintings in
print(paintings) // [!, "]
}
paintingsProducer.on( failure: { error in
// handle error
},
next: { paintings in
print(paintings) // [!, "]
})
.start()
Once you want to start your work, you can use startWithNext
, but I’m only interested in the green path - I don’t care if there’s an error, I just want to print the paintings.
Examples (13:32)
Form validation
The standard form logic is as follows: you enter information, including a password, and the login button becomes enabled. ReactiveCocoa is pretty good at doing this.
Conceptually, this is simple. But from a technical standpoint, without any third party libraries, this is cumbersome: you have delegates and callbacks, so when the delegate method is called, you need to check which text field is being used. You also need to validate, then check the button.
let email: MutableProperty<String> = emailTextField.rex_text
let password: MutableProperty<String> = passwordTextField.rex_text
let isValid: (String, String) -> Bool = ...
let areCredentialsValid = combineLatest(email.producer, password.producer).map(isValid)
loginButton.rex_enabled <~ areCredentialsValid
You have a emailTextField
and a passwordTextField
, and you take the rex_text
. The idea of KVO is that you’re just observing the text. The isValid
takes two strings and returns a boolean.
Combine both the email and password and you map that value to it. Every time one of them changes, the function is called, and you are mapping that call to a boolean.
Only when you bind that call to the rex_enabled
from the button will something happen.
You can still use functional reactive programming in the very same way you use NSOperation
and KVO, but functional reactive programming is more than that.
Observe the following “game” code as another example:
You have the initial drinks, and a function that defines if a person is sober. It starts with drinks, and you concat with the drinks delayed by 7.5.
let drinks = SignalProducer<[String], NoError>(value: ["!","!"])
let areSober: [String] -> Bool = ...
let gamePlan = drinks
.concat(drinks.delay(7.5))
.concat(timer(5).flatMapLatest { _ in drinks }.takeWhile(areSober))
There’s also a timer that is going to fire every five minutes. You’re going to map it to new drinks, and you’re going to use that while they’re still sober. If you’re doing this without functional reactive programming, it would be a mess.
Testing FRP (20:38)
Testing in functional reactive programming is easy. There are two ways to do it. One is illustrated by a network call:
func testNumberPaintings() {
let expectation = self.expectationWithDescription("Expect to have 2 paintings")
defer { self.waitForExpectationsWithTimeout(1.0, handler: nil) }
paintingsViewModel.paintingsProducer.startWithNext { paintings in
XCTAssertEqual(paintings.count, 2)
expectation.fulfill()
}
}
There’s an expectation
, you defer
to waitForExpectationsWithTimeout
, then you start that producer. You assert something and an expectation is fulfilled.
ReactiveCocoa has a way of cheating on this. When this call starts, it blocks the current thread and you can just assert. It’s an easier way. The result here is something that I would advise using, as opposed to using try catch
.
final class StubbedNextValues<T: Equatable> {
private var nextValues: [T]
private let expectation: XCTestExpectation
init(nextValues: [T] , expectation: XCTestExpectation) {
self.nextValues = nextValues
self.expectation = expectation
}
func handleNextValue(nextValue: T) -> Void {
guard !nextValues.isEmpty else { fatalError("Can't handle \(nextValue)") }
let stubbedValue = nextValues.removeFirst()
XCTAssertEqual(stubbedValue, nextValue)
if nextValues.isEmpty {
expectation.fulfill()
}
}
}
You can think about the above as a state machine. This is what you would expect, and these are the values you’re expecting. handleNextValue
is what’s going to verify all of the values.
let producer: SignalProducer<Int, NoError> = ...
let stubbedNextValues = StubbedNextValues(nextValues: [1, 2, 3, 4], expectation: expectation)
producer.startWithNext(stubbed.handleNextValue)
There is a producer
which returns ints, and creates stubbedNextValues
, in an array of things. In the producer
, instead of creating an anonymous function, you pass this handleNextValue
from the stubbedNextValues
.
Conclusion (24:22)
Functional reactive programming takes some time to get used to, but testing functional reactive code is as difficult as you want it to be. If you take into consideration things like the solid principles, such as defining well the responsibilities of the code, functional reactive programming is orthogonal to that. If you respect those, functional reactive code is easy to test.
Receive news and updates from Realm straight to your inbox