Oleg Kosenko
Oleg Kosenko

Reputation: 145

Using generic methods in protocols Swift

I believe I have some misunderstanding of how generics work. I have the protocol:

protocol CommandProtocol {
    func execute<T>() -> T
    func unExecute<T>() -> T
}

And a class that conforms to it:

class CalculatorCommand: CommandProtocol {
    ...

    func execute<String>() -> String {
        return calculator.performOperation(operator: `operator`, with: operand) as! String
    }

    func unExecute<Double>() -> Double {
        return calculator.performOperation(operator: undo(operator: `operator`), with: operand) as! Double
    }

    ...
}

The calculator.performOperation() method actually returns Double, but here I just try to play with generics so I replace return type from Double to String.

After that, I have a class which invokes these methods:

class Sender {

    ...
    // MARK: - Public methods

    func undo() -> Double {
        if current > 0 {
            current -= 1
            let command = commands[current]
            return command.unExecute()
        }
        return 0
    }

    func redo() -> Double? {
        if current < commands.count {
            let command = commands[current]
            current += 1
            let value: Double = command.execute()
            print(type(of: value))
            return command.execute()
        }
        return nil
    }
    ...
}

In the undo() method everything works as expected (one thing that I did not understand fully is how Swift really knows whether the unExecute value will return Double or not, or compiler infers it based on the undo() return type?)

But in the redo() method, I am calling the execute() method which returns String, but the method expects Double, so I thought that my program would crash, but not, it works totally fine as if execute() method returns Double. Please, could someone explain to me what exactly happens under the cover of this code? Thank you in advance.

Upvotes: 1

Views: 97

Answers (2)

Rob Napier
Rob Napier

Reputation: 299663

You are correct that you misunderstand generics. First, let's look at this protocol:

protocol CommandProtocol {
    func execute<T>() -> T
    func unExecute<T>() -> T
}

This says "no matter what type the caller requests, this function will return that type." That's impossible to successfully implement (by "successfully" I mean "correctly returns a value in all cases without crashing"). According this protocol, I'm allowed to write the following code:

func run(command: CommandProtocol) -> MyCustomType {
    let result: MyCustomType = command.execute()
    return result
}

There's no way to write an execute that will actually do that, no matter what MyCustomType is.

Your confusion is compounded by a subtle syntax mistake:

func execute<String>() -> String {

This does not mean "T = String," which is what I think you expect it to mean. It creates a type variable called String (that has nothing to do with Swift's String type), and it promises to return it. when you later write as! String, that means "if this values isn't compatible with the type requested (not "a string" but whatever was requested by the caller), then crash.

The tool that behaves closer to what you want here is an associated type. You meant to write this:

protocol CommandProtocol {
    associatedType T
    func execute() -> T
    func unExecute() -> T
}

But this almost certainly won't do what you want. For example, with that, it's impossible to have an array of commands.

Instead what you probably want is a struct:

struct Command {
    let execute: () -> Void
    let undo: () -> Void
}

You then make Commands by passing closures that do what you want:

let command = Command(execute: { self.value += 1 }, 
                      undo: { self.value -= 1 })

Alternately, since this is a calculator, you could do it this way:

struct Command {
    let execute: (Double) -> Double
    let undo: (Double) -> Double
}

let command = Command(execute: { $0 + 1 }, undo: { $0 - 1 })

Then your caller would look like:

value = command.execute(value)
value = command.undo(value)

Upvotes: 0

user652038
user652038

Reputation:

You think this returns a Swift.Double, but no. This code is no different than using T instead of Double. Swift does not require the names of generic placeholders to match what you put in a protocol.

func unExecute<Double>() -> Double {
        return calculator.performOperation(operator: undo(operator: `operator`), with: operand) as! Double
    }

You're not actually looking for generic methods. You want this, instead.

protocol CommandProtocol {
  associatedtype ExecuteValue
  associatedtype UnExecuteValue

  func execute() -> ExecuteValue
  func unExecute() -> UnExecuteValue
}

Upvotes: 0

Related Questions