General swift and Future concepts

·

0 min read

I'm currently using a home grown Future library. The setup combines three concepts that I ~rarely~ never write on my own:

  • Generics
  • Functions as parameters
  • Error throwing
  • Closures

Here's an example of a function declaration that combines all three:

func then<V>(coolFunction: @escaping (T) throws -> V) -> Future<V>

Generics

There are two generics in here: V and T. Now we understand them, let's re-write it without the generics so they aren't so confusing.

We'll pretend V is always of type Bool, and T is always of type Int.

Note that because these are generics, either of these types could be Void in reality

func then(coolFunction: @escaping Int throws -> Bool) -> Future<Bool>

@escaping

This is used for closures typically. It means that whatever you return here can be used elsewhere in your code base. i.e. it can "escape".

Here's an example function.

func isItTimeForChocolate(time: String) -> Bool {
   let chocolateTime: String = "now"
   return time == chocolateTime
}

This function has an internal variable called chocolateTime. No one outside of this function knows that chocolateTime exists. E.g.

func isItTimeForChocolate(time: String) -> Bool {
   let chocolateTime: String = "now"
   return time == chocolateTime
}

print(chocolateTime) // This will fail to compile: chocolateTime doesn't exist outside the function

This is a very simplified concept of something that is not escaping. In this simple example, if we did want to use chocolateTime outside of the function, we would declare it outside the function, as it's just a String.

However in closure life and our then function, things are a little more tricky. So to make sure we can use elements outside of the function, we use @escaping.

The @escaping keyword tells us that we can still access and see Int outside of the coolFunction.

func then(coolFunction: @escaping Int throws -> Bool) -> Future<Bool>

So now we know what the purpose of @escaping is, let's remove it from our function to make it a bit simpler to understand.

func then(coolFunction: Int throws -> Bool) -> Future<Bool>

throws

throws denotes error handling. The Swift documentation does a nice job of explaining it.

Let's look at a simple function:

func isItCakeTime() -> Bool {
    return true
}

This function always returns a Bool. However what happens if there is no cake, and you want to return an error? Usually I would make it return an optional, i.e. Bool?.

func isItCakeTime() -> Bool? {
    guard isThereCakeAtHome else { return nil }
    return true
}

However this assumes that the user of the function knows that nil is actually an error state. Often nil is not an error, it is an acceptable output.

If I wanted to explicitly call out that this is an error, not an acceptable output, then I would use throws.

enum CakeError {
    case noMoreCakeLeft
    case notARelaxingTimeToEatCake
}

func isItCakeTime() throws -> Bool {
    guard isThereCakeAtHome else { throw CakeError.noMoreCakeLeft }
    guard isBabyAsleep else { throw CakeError.notARelaxingTimeToEatCake }
    return true
}

So now we can look at our original function and understand that throws denotes the ability of that function to throw an error.

Nothing more complex than that.

func then(coolFunction: Int throws -> Bool) -> Future<Bool>

If we didn't want it to throw an error, we could simplify it to:

func then(coolFunction: Int -> Bool) -> Future<Bool>

Functions as parameters

The then function takes only one parameter, coolFunction.

Usually I'm used to parameters having simple types, like String. However in this case, we want the parameter to be a function instead of a String. Note that these two statements are the same:

func then(coolFunction: Int -> Bool) -> Future<Bool>
func then(coolFunction: (Int -> Bool)) -> Future<Bool> // this is the same as above, but with () around (Int -> Bool)

When I add the parentheses around (Int -> Bool), I find it a lot easier to understand that coolFunction is actually expecting another function.

It's not just expecting any function though. It's expecting a function which takes an Int and returns a Bool. (Actually, we want a function that takes a generic parameter, T, and returns another generic parameter, V).

So now we understand this bit, we could simplify the function to:

func then(coolFunction: String) -> Future<Bool>

Ta-dah! Now we have a simple function declaration that we understand!

Looking back at the original function declaration, hopefully it's a bit less tricky now:

func then<V>(coolFunction: @escaping (T) throws -> V) -> Future<V>

Using the function

Here's a simple example of how to use then.

private var secretCakeRecipe: String = ""

then { [weak self] recipe in
   self?.secretCakeRecipe = recipe
   return true
}

In this example, everything inside the { } is the coolFunction. So the coolFunction takes an input, which is recipe and it returns a Bool.

then doesn't know what type recipe is. In this instance it is a String. But it could be an Int or anything else.

[weak self] exists because we don't want a retain cycle, as we are referencing a variable, secretCakeRecipe inside this closure.

It is also possible to remove recipe and just use $0:

private var secretCakeRecipe: String = ""

then { [weak self] in
   self?.secretCakeRecipe = $0
   return true
}

However if for some reason, we weren't using the input value T at all inside the function, then we would need to use _ to tell the closure that yes, there is an input value but no, we won't be using it.

then {  _ in
   return true
}

Final note

You may have realised I've ignored the final -> Future<V>

func then<V>(coolFunction: @escaping (T) throws -> V) -> Future<V>

It's a bit tricky to deal with this in isolation without the other future functions that exist around it.

So for now, I'll ignore it, sorry!