Returning the associatedtype of an opaque return type

854 views Asked by At

I have a simple protocol with an associated type, and a protocol extension that returns an array of this type.

protocol Foo {
    associatedtype Unit
}

extension Foo {
    var allTheFoos: [Unit] {
        return []
    }
}

I then have a struct which returns some Foo in a computed property, and another computed property that returns the allTheFoos array.

struct FakeFoo: Foo {
    typealias Unit = Int
}

struct FooFactory {
    var myFoo: some Foo {
        return FakeFoo()
    }

    /* WHICH RETURN TYPE WILL
       PLEASE THE SWIFT GODS?!
     */
    var allTheFoos: [Foo.Unit] {
        return myFoo.allTheFoos
    }
}

The return type of allTheFoos matches Xcode's autocomplete type suggestion for the myFoo.allTheFoos call, but understandably, this yields a:

// var allTheFoos: [Foo.Unit] {}
ERROR: Associated type 'Unit' can only be used with a concrete type or generic parameter base

My question is: What return type will make Xcode happy?

Below are my attempts, and their corresponding errors

// var allTheFoos: [some Foo.Unit] {}
ERROR: 'some' types are only implemented for the declared type of properties and subscripts and the return type of functions
// func allTheFoos() -> some [Foo.Unit]
ERROR: Associated type 'Unit' can only be used with a concrete type or generic parameter base
// func allTheFoos<U: Foo.Unit>() -> [U]
ERROR: Associated type 'Unit' can only be used with a concrete type or generic parameter base
ERROR: Cannot convert return expression of type '[(some Foo).Unit]' to return type '[U]'
// func allTheFoos<U>() -> [U] where U: (some Foo).Unit
ERROR: 'some' types are only implemented for the declared type of properties and subscripts and the return type of functions

FYI: The reason I'm doing this in a computed property in the first place is to keep things clean in some SwiftUI code.

Thanks for any help you can give!

=========== UPDATE ===========

I missed some important stuff in my sample code, so to give some context: the code is used in a unit conversion app, so something that can turn Celsius into Kelvin, Kg into lbs, and anything else into anything else.

protocol Unit: Equatable {
    var suffix: String { get }
}

struct Value<UnitType: Unit> {
    let amount: Double
    let unit: UnitType

    var description: String {
        let formatted = String(format: "%.2f", amount)
        return "\(formatted)\(unit.suffix)"
    }
}

Value is constrained to a unit type, so that it's not possible to convert Celsius into Litres.

Therefore, we have a Conversion protocol that stores all similar units together:

protocol Conversion {
    associatedtype UnitType: Unit

    var allUnits: [UnitType] { get }
    func convert(value: Value<UnitType>, to unit: UnitType) -> Value<UnitType>
}

extension Conversion {
    func allConversions(for value: Value<UnitType>) -> [Value<UnitType>] {
        let units = self.allUnits.filter { $0 != value.unit }
        return units.map { convert(value: value, to: $0) }
    }
}

So an example of a conversion for Temperature would be:

struct Temperature: Conversion {
    enum Units: String, Unit, CaseIterable {
        case celsius, farenheit, kelvin

        var suffix: String {
            switch self {
            case .celsius:   return "˚C"
            case .farenheit: return "˚F"
            case .kelvin:    return "K"
            }
        }
    }

    var allUnits: [Units] { return Units.allCases }

    func convert(value: Value<Units>, to unit: Units) -> Value<Units> {
        /* ... */
    }
}

Finally, the actual app code where the problem occurs is here:

struct MyApp {
    var current: some Conversion {
        return Temperature()
    }

    // ERROR: Associated type 'UnitType' can only be used with a concrete type or generic parameter base
    var allConversions: [Value<Conversion.UnitType>] {
        // This value is grabbed from the UI
        let amount = 100.0
        let unit = current.allUnits.first!
        let value = Value(amount: amount, unit: unit)

        return current.allConversions(for: value)
    }
}
2

There are 2 answers

3
XmasRights On

Managed to solve this issue using some Type Erasure:

struct AnyValue {
    let description: String

    init<U: Unit>(_ value: Value<U>) {
        self.description = value.description
    }
}

allowing for:

var allConversions: [AnyValue] {
    // This value is grabbed from the UI
    let amount = 100.0
    let unit = current.allUnits.first!
    let value = Value(amount: amount, unit: unit)

    return current.allConversions(for: value).map(AnyValue.init)
}

However, this feels like a clunky solution (and one that opaque return types was introduced to avoid). Is there a better way?

0
Rob Napier On

Looking at how you've implemented AnyValue, I think what you want here is just:

var allConversions: [String] {
    let units = self.allUnits.filter { $0 != value.unit }
    return units.map { convert(value: value, to: $0) }.description
}

Or something like that. All the algorithms that match what you're describing are just "conversion -> string." If that's the case, all you really want is CustomStringConvertible.