Robert Robert - 4 months ago 20
Swift Question

How to have default and specialised function in Swift depending on generic type?

I have a generic

struct
which accepts any type, and a function:

struct Test<T> {
func makeSomething() -> T? {
print("Default implementation")
return nil
}
}


I also have a protocol which contains a static method which returns an instance of itself:

protocol TestProtocol {
static func defaultValue() -> Self
}


I want the
makeSomething
function to be specialised if
T
conforms to
TestProtocol
. Example usage:

struct TestItem: TestProtocol {
static func defaultValue() -> TestItem {
return TestItem()
}
}

let normalTest: Test<String> = Test()
let normalReturn = normalTest.makeSomething() // Should use default, returns nil.

let specialTest: Test<TestItem> = Test()
let specialReturn = specialTest.makeSomething() // Should use specialised, returns `TestItem`.


I can think of a few ways of doing this, but none of them work (or I don't know how to implement them properly).

Option 1



Create a type-constrained extension for
Test
:

extension Test where T : TestProtocol {
func makeSomething() -> T? {
print("Special implementation")
return T.defaultValue()
}
}


Problem: Attempting to use the
makeSomething
function results in an error:
ambiguous use of 'makeSomething()'
. I understand why the error occurs; just because I create a specialised function, doesn't mean the default function is any less valid, and therefore it doesn't know which function to use.

It might also be possible to move the default implementation into an extension as well, but it would have to have type constraints which say something along the lines of 'any type which doesn't conform to
TestProtocol
', and as far as I know, that's not possible.

Option 2



Add specialised parts to the
makeSomething
function so that it changes its behaviour depending on the type of
T
:

func makeSomething() -> T? {
if let specialType = T.self as? TestProtocol.Type {
print("Special implementation")
return specialType.defaultValue()
} else {
print("Default implementation")
return nil
}
}


Problem: As expected, this doesn't work, since
specialType
is now of type
TestProtocol.Type
and not
T
, so I get the error:
cannot convert return expression of type 'TestProtocol' to return type 'T?'
. I don't know how to make the compiler know that
T
is a
TestProtocol
in this case.




The actual problem I have is more complex, but I think this simplifies it down and correctly illustrates the problem I have. Is there a way of having default and specialised functionality depending on the conformance of
T
within the constraints above?

Answer

The easiest solution would be to simply use option #2 and just cast the result back to T? in order to bridge the gap with the fact that we can't cast T.self to a type of T.self that also conforms to TestProtocol.Type:

func makeSomething() -> T? {
    if let specialType = T.self as? TestProtocol.Type {
        print("Special implementation")
        return specialType.defaultValue() as? T
    } else {
        print("Default implementation")
        return nil
    }
}

As defaultValue returns Self, this cast should never fail (we're using a conditional cast as the method returns an optional).

Although that being said, relying on runtime type-casting doesn't feel like a particularly nice solution to this problem. A possible way of achieving the overloading that you were trying to achieve in your option #1 is to use a protocol – allowing both static dispatching & overloading when the given associated type Something conforms to TestProtocol.

// feel free to give this a better name
protocol MakesSomething {
    associatedtype Something
    func makeSomething() -> Something?
}

// default implementation
extension MakesSomething {
    func makeSomething() -> Something? {
        return nil
    }
}

protocol TestProtocol {
    static func defaultValue() -> Self
}

// specialised implementation
extension MakesSomething where Something : TestProtocol {
    func makeSomething() -> Something? { 
        return Something.defaultValue()
    }
}

struct Test<T> : MakesSomething {
    // define T == Something
    typealias Something = T
}

let normalTest : Test<String> = Test()
let normalReturn = normalTest.makeSomething() // nil

let specialTest : Test<TestItem> = Test()
let specialReturn = specialTest.makeSomething() // TestItem()

Perhaps not the most convenient solution, as it involves creating a new type – but as far as I'm aware the only way to achieve the conditional overloading behaviour you're after is through protocols. Although depending on the actual practical problem you're trying to solve, you may be able to integrate this into an already existing protocol.