18/05/2021
FunctionalDataOperation
implemented previously has hit a bottleneck due to a lack of built-in error handling.
FunctionalDataOperation<Result<Element, AnyError>>
. The question is, can we do better?public struct FunctionalDataOperation<Element> {
private let operation : (NSManagedObjectContext) -> Element
public init (_ operation: @escaping (NSManagedObjectContext) -> Element) {
self.operation = operation
}
public func operate(in context: NSManagedObjectContext) -> Element {
var result: Element!
context.performAndWait {
result = operation(context)
}
return result
}
public func operateAsync(in context: NSManagedObjectContext,
_ callback: @escaping (Element) -> Void){
let operation = self.operation
context.perform {
let element = operation(context)
callback(element)
}
}
public func map<NewElement>(_ f: @escaping (Element) -> NewElement)
-> FunctionalDataOperation<NewElement> {
return FunctionalDataOperation<NewElement> { ctx in
f(self.operate(in: ctx))
}
}
public func flatMap<NewElement>
(_ f: @escaping (Element) -> FunctionalDataOperation<NewElement>)
-> FunctionalDataOperation<NewElement> {
return FunctionalDataOperation<NewElement> { ctx in
f(self.operate(in: ctx)).operate(in:ctx)
}
}
}
public func zip<A, B>
(_ firstOperation: FunctionalDataOperation<A>, _ secondOperation: FunctionalDataOperation<B>)
-> FunctionalDataOperation<(A,B)> {
return FunctionalDataOperation { ctx in
(firstOperation.operate(in:ctx), secondOperation.operate(in:ctx))
}
}
What’s wrong with FunctionalDataOperation<Result<Element, AnyError>>?
Ok, I’ll briefly show you how things break apart when we perform a flatMap operation on FunctionalDataOperation<Result<Element,Error>>
.
This is what we have if it were to always return Result
.
struct FunctionalDataOperation<Element, Error> {
private let operation: (NSManagedObjectContext) -> Result<Element, Error>
}
And we shall start with implementing a little helper function fetchOperation
that takes in a fetch request and returns an operation.
func fetchOperation<ResultType>
( request: NSFetchRequest<ResultType>) -> FunctionalDataOperation<Result<[ResultType],
Error>> {
return FunctionalDataOperation { context in
context.fetching(request)
}
}
}
Here we assume – there is a FunctionalDataOperation
that returns the name of a Game
.
let nameOperation: FunctionalDataOperation<Result<String, AnyError>>
After that, a function that creates a fetch operation that filters our games by name.
func fetchGamesOperation(named: String) -> FunctionalDataOperation<Result<[Game], AnyError>> {
let gameRequest = NSFetchRequest<Game>(entityName: “Game”)
gameRequest.predicate = NSPredicate(format: “name == %@“, named)
return fetchOperation(gameRequest)
}
Finally, applying flatMap
to hook these two operations together:
let filteredGamesOperation = nameOperation.flatMap { nameResult in
return FunctionalDataOperation { context in
return nameResult.flatMap { name in
return fetchGamesOperation(named: name).operate(in: context)
}
}
}
}
See how that’s total a disaster? It was supposed to be just a simple flatMap.
I was inspired by Haskell’s Data.Either and IO type. IO type is essentially a lazily evaluated closure that we usually put side effects in, like reading from file or printing to console. Quite often, IO has a failure state, which means it will return an Either saying it’s either successful or failed. But having IO<Either<Success, Error>> everywhere would cause the problems we are currently facing, so they created EitherIO
.
ResultFDO
follows the name convention of putting the inner container first and then the outer container second. But ResultFunctionalDataOperation is a bit too long, so it’s been shortened down to ResultFDO
. After all, IO is also an abbreviation, right?
It is straightforward to implement, starting with its only property.
struct ResultFDO<Element, Error: Swift.Error> {
let operation: FunctionalDataOperation<Result<Element, Error>>
init(_ operation: @escaping (NSManagedObjectContext) -> Result<Element, Error>) {
self.operation = FunctionalDataOperation(operation)
}
}
There’s also a convenience initialiser so we can have a similar syntax to FunctionalDataOperation
which we could get access to the context directly.
Similarly, it should also have operate
and operateAsync
.
extension ResultFDO{
func operate(in context: NSManagedObjectContext) -> Result<Element,Error> {
return operation.operate(in: context)
}
func operateAsync
(in context: NSManagedObjectContext, _ callback: @escaping (Result<Element, Error>) -> Void)
{
operation.operateAsync(in: context, callback)
}
}
Along with map
and flatMap
operations:
extension ResultFDO {
func map<NewElement>
(_ f: @escaping (Element) -> NewElement) -> ResultFDO<NewElement, Error> {
return ResultFDO<NewElement, Error>(operation: operation.map { $0.map(f) }
}
func flatMap<NewElement>(_ f: @escaping (Element) -> ResultFDO<NewElement, Error>) {
return ResultFDO<NewElement, Error> { ctx in
self.operation.operate(in:ctx).flatMap { f($0).operate(in:ctx) }
}
}
}
I encourage you to try to create it yourself and only reference this when stuck, even though FlatMap
might seem intimidating at first.
Actually, why are we doing this instead of just using a typealias
? This wrapper type seems a little redundant, no? Too bad that we can’t write an extension to type alias easily, plus I didn’t think it’s justifiable to extend FunctionalDataOperation
to constrain its Element
to a Result. This seemed cleaner.
Comparing ResultFDO
to FunctionalDataOperation<Result<Element,Error>>
, now it’s much easier to navigate our minds between various functional operations such as map, flatMap,sequence .
Let’s rewrite fetchOperation function to return ResultFDO instead of a FunctionalDataOperation
.
func fetchOperation<ResultType>(_ request: NSFetchRequest<ResultType>) -> ResultFDO<[ResultType]> {
return ResultFDO { context in
context.fetching(request)
}
}
And here’s how the calling code would look like:
let gameRequest = NSFetchRequest<Game>(entityName: “Game”)
let gameOperation: ResultFDO<[Game], AnyError> = fetchOperation(gameRequest)
let gameResult: Result<[Game], AnyError> = gameOperation.operate(in: context)
}
Not much has changed, but that was kinda expected. ResultFDO
was meant to serve as a wrapper type, so it would make sense they have similar APIs.
What about map
?
let namesOperation = gameOperation.map { $0.map(getNameSafely) }
// Type: DataOperation<Result<[String],AnyError>
That’s exactly what we would expect from a container! There are not extraneous map calls. Then what about flatMap
?
Similarly, we assume we have a ResultFDO
that gives us a game’s name.
let namesOperation: ResultFDO<String, AnyError>
And then we rewrite a fetchGamesOp
in terms of ResultFDO, changing only the return signature.
func fetchGamesOp(named: String) -> ResultFDO<[Game], AnyError> {
let gameRequest = NSFetchRequest<Game>(entityName: “Game”)
gameRequest.predicate = NSPredicate(format: “name == %@“, named)
return fetchOperation(gameRequest)
}
Then finally, we hook them up together.
let filteredGamesOp: ResultFDO<[Game], AnyError> = nameOperation.flatMap(fetchDogsOp)
Sweet! It’s now simplified to just one call.
You might need to spend a bit of time to implement a brand new container and its methods, but it will be worthy of your time if this nested combination is used often enough.
In our previous article of (Re)implementing Result type, we talked about using lift
on nested wrapper types. Its primary purpose is to make it easy for the underlying types to be “lifted” into its world. In our use-case, the underlying types are FunctionalDataOperation
and Result
.
So we can implement two lift
functions that do exactly that.
//// Lifts a Result into ResultFDO
public func lift<Element, Error>
(_ result: Result<Element, Error>) -> ResultFDO<Element, Error> {
return ResultFDO { _ in result }
}
//// Lifts a FunctionalDataOperation into a ResultFDO
public func lift<Element, Error>
(_ operation: FunctionalDataOperation<Element>) -> ResultFDO<Element> {
return ResultFDO(operation: operation.map(Result.success))
}
P/S: Result.success is a function shorthand for { Result.success($0) } .
These are useful when you have a FunctionalDataOperation
and you want to use flatMap into a ResultFDO. Instead of calling map on the FunctionalDataOperation
and getting it a mess, it’s better to just lift it to ResultFDO
and then call flatMap.
It is not the purpose of this article to show you the implementation details of ResultFDO
, rather it is to show you the importance of separating concerns for different containers. For instance, the purpose of FunctionalDataOperation
is primarily thread management, so it should just focus on that, and we should not introduce the Result type to it. It may sound ironic but the key to making nested containers easy to deal with is by introducing new containers as demonstrated in this article. The upfront cost of doing so is little but can save us a lot of time in the long run.
I hope this article has inspired you to consider abstracting those nested containers you have been using (and hating all along!). Next up, I will be integrating Point-free’s randomness container Gen
into ResultFDO
which would then allow us to have a more controllable seeding of our CoreData context (for testing). Then, I will apply the same concept again to justify another container for wrapping our ResultFDO
as otherwise, we would need to deal with up to three levels deep of containers.
For any feedback (or just to say hi), buzz me😊