Home > Enterprise >  Generic type not preserved when called within another generic function
Generic type not preserved when called within another generic function

Time:11-13

[Updated with a less contrived example]

I'm trying to extend a generic function to provide different behavior for a specific type. This works as expected when I call the function directly. But If I call it from within another generic function, the original generic type is not preserved and I get the default behavior. I'm a bit new to Swift, so I may be missing something obvious here.

My code looks something like this:

protocol Task {
    associatedtype Result
}

struct SpecificTask: Task {
    typealias Result = String
    
    // other taks related properties
}

enum TaskRunner<T: Task> {
    static func run(task: T) throws -> T.Result {
        // Task not supported
        throw SomeError.error
    }
}

extension TaskRunner where T == SpecificTask {
    static func run(task: T) throws -> T.Result {    
        // execute a SpecificTask
        return "Some Result"
    }
}

func run<T: Task>(task: T) throws -> T.Result {
    // Additional logic related to running the task
    return try TaskRunner.run(task: task)
}

print(try TaskRunner.run(task: SpecificTask())) // Prints "Some Result"
print(try run(task: SpecificTask()))            // Throws SomeError

I need the top-level run function to call the SpecificTask version of the lower-level run() function, but the generic version of the function is called instead

CodePudding user response:

You're trying to reinvent class inheritance with generics. That is not what generics are for, and they don't work that way. Generic methods are statically dispatched, which means that the code is chosen at compile-time, not runtime. An overload should never change the behavior of the function (which is what you're trying to do here). Overrides in where clauses can be used to improve performance, but they cannot be used to create dynamic (runtime) dispatch.

If you must use inheritance, then you must use classes. That said, the problem you've described is better solved with a generic Task rather than a protocol. For example:

struct Task<Result> {
    let execute: () throws -> Result
}

enum TaskRunner {
    static func run<Result>(task: Task<Result>) throws -> Result {
        try task.execute()
    }
}

let specificTask = Task(execute: { "Some Result" })

print(try TaskRunner.run(task: specificTask)) // Prints "Some Result"

Notice how this eliminates the "task not supported" case. Rather than being a runtime error, it is now a compile-time error. You can no longer call this incorrectly, so you don't have to check for that case.

If you really want dynamic dispatch, it is possible, but you must implement it as dynamic dispatch, not overloads.

enum TaskRunner<T: Task> {
    static func run(task: T) throws -> T.Result {
        
        switch task {
        case is SpecificTask:
            // execute a SpecificTask
            return "Some Result" as! T.Result   // <=== This is very fragile
        default:
            throw SomeError.error
        }
    }
}

This is fragile because of the as! T.Result. If you change the result type of SpecificTask to something other than String, it'll crash. But the important point is the case is SpecificTask, which is determined at runtime (dynamic dispatch). If you need task, and I assume you do, you'd swap that with if let task = task as? SpecificTask.

Before going down that road, I'd reconsider the design and see how this will really be called. Since the Result type is generic, you can't call arbitrary Tasks in a loop (since all the return values have to match). So it makes me wonder what kind of code can actually call run.

  • Related