Kombo Kombo - 2 months ago 7
Swift Question

Overload Functions For Different Structs Implementing The Same Protocol?

I'm new to statically typed languages and was wondering if it was possible to cast a Struct to its type in order to call the proper overloaded function? The issue I'm running into to is I have a list of Structs that conform to a

Mutation
protocol. I want to iterate through the list and call the correct
handle
function for each Struct. I could move this
handle
function to the Struct itself, but for the API I'm trying to implement I'd like to see if such a thing is possible as below:

//: Playground - noun: a place where people can play

import UIKit

protocol Mutation {
func mutate(state: Int) -> Int
}

struct CountIncrement: Mutation {
func mutate(state: Int) -> Int {
return state + 1
}
}

struct CountDecrement: Mutation {
func mutate(state: Int) -> Int {
return state - 1
}
}

func handle(mutation: CountIncrement, state: Int) -> Int {
print("WILL INCREMENT")
return mutation.mutate(state: state)
}

func handle(mutation: CountDecrement, state: Int) -> Int {
print("WILL DECREMENT")
return mutation.mutate(state: state)
}

var state = 0
var queue = [CountIncrement(), CountDecrement()] as [Mutation]

for mutation in queue {
handle(mutation: mutation, state: state) // error: cannot invoke 'handle' with an argument list of type '(mutation: Mutation, state: Int)'
}

Answer

This is backwards of how you should approach the problem. In Swift you should generally avoid free functions (like handle(mutation:state:)), and instead attach methods to types.

Your loop wants a specific method, so you want a new protocol (you could of course attach this function to Mutation, but making it separate means that it could have different access controls or the like, so is as flexible as the free functions).

protocol MutationHandling {
    func handleMutation(forState state: Int) -> Int
}

So we've just declared that this function you want is a thing. Now we can attach it to things that we care about. This is exactly the same as your writing free-functions above. It's the same pattern. The syntax is just different, and provides some extra documentation about why this function exists and allows the IDE to provide auto-completion and gather documentation together more effectively.

extension CountIncrement: MutationHandling {
    func handleMutation(forState state: Int) -> Int {
        print("WILL INCREMENT")
        return mutate(state: state)
    }
}

Now, you use this special protocol for your list:

var queue = [CountIncrement(), CountDecrement()] as [MutationHandling]

And call it:

for mutation in queue {
    mutation.handleMutation(forState: state)
}

This isn't just some random limitation in Swift. This is a deep part of how Swift decomposes problems. Free-functions are explicitly discouraged (see the API Design Guidelines). Methods and extensions are the Swift way.