Opaque Result Types not Assignable to property, bug or design as intended?

(Davide Mendolia) #1

I'm testing the Opaque Result Types with the toolchain: Swift 5.1 Snapshot 2019-05-03 (a)

And I get the following error when I try to assign to a property an Opaque type.

Cannot assign value of type 'some SaveAddress' to type 'some SaveAddress'

Do the opaque types can only be assigned to a local variable or it's a feature that's under development?

protocol UseCase {
    associatedtype Input
    associatedtype Output

    func execute(_ input: Input) -> Output
}

struct SignUpAddressData {
}

protocol SaveAddress : UseCase {
    func execute(_ input: SignUpAddressData) -> Void
}

func bindSaveAddress() -> some SaveAddress {
    return SaveAddressDefault()
}

class SaveAddressDefault: SaveAddress {
    func execute(_ input: SignUpAddressData) {
    }
}

class InputPersonalDataViewModel {
    private let saveAddress: some SaveAddress
    init() {
        self.saveAddress = bindSaveAddress()
    }
}
(Paul Cantrell) #2

As I understand the feature, this is by design.

The problem is that some SaveAddress secretly means a different type in every declaration where it appears. The some SaveAddress that bindSaveAddress() returns thus is not necessarily the same as the some SaveAddress that is saveAddress’s type.

An unfortunate consequence of this proposal is that we end up with distinct types that have the same spelling in code. That’s confusing, and I hope the release version of the compiler will at least generate a better error message for it, as Joe Groff proposed here.


You raise a good question about assigning to uninitialized properties. I’m not even sure what this means under the current implementation of the feature:

private let saveAddress: some SaveAddress

What is the single concrete runtime type of saveAddress that we are making opaque here? There’s no initializer, and thus nothing to infer the type from. @Joe_Groff, should this code even compile?


The solution in your case (again, in my limited understanding) would be to either use an existential:

private let saveAddress: SaveAddress  // not just some, but any SaveAddress

…or to use a hypothetical not-yet-supported syntax to bind that property’s type to the specific function that populates it:

private let saveAddress: #returnType(of: bindSaveAddress)

It would be great if the compiler could suggest these two as fixits. Unfortunately, the latter is (IIRC) not yet implemented or even formally proposed, and the former will currently not work if the protocol uses Self or associated types.

I do wonder it’s premature to release this feature without one or both of those solutions available.

2 Likes
(Davide Mendolia) #3

I intend to achieve is choose the implementation of SaveAddress at compile time, but even moving to property assignation don't get the desired effect. If I use as a computed property I have no way to reuse the same instance and will always ask for a new that's what I try to avoid.

class InputPersonalDataViewModel {
    private let saveAddress = bindSaveAddress()
}

Or

    class InputPersonalDataViewModel {
        private let saveAddress: some SaveAddress = bindSaveAddress()
    }


On the other side:
I understand that difference in type would be solved by an existential like to following(or possible corresponding syntax):

class InputPersonalDataViewModel {
    private let saveAddress: any SaveAddress
    init() {
        self.saveAddress = bindSaveAddress()
    }
}

As you mention, the "reverse Generic" would potentially have two different concrete type at compilation time. And an improvement would be to throw an error message for properties similar to function when not having information to infer an underlying type.

(Davide Mendolia) #4

Also, in the proposal, there is an explicit mention of stored property.

Opaque result types can also be used in stored properties that have an initializer, in which case the concrete type is the type of the initializer

I'm trying to instantiate PAT dependencies at compile time, but it seems that we will have to wait for existential. Not sure of the added value of opaque type by itself yet.

(Adrian Zubarev) #5

The very simple mental model is that you have to think about who knows the concrete type. If sou would examine a generic function with one parameter and one result type.

func foo<T: P>(_ t: T) -> some P // same as `func foo(_ t: some P) -> some P`
  • the concrete return type is known only be the implementor of the function (reverse generics)
  • the concrete parameter type is known only by the caller (regular generics)

Since the only generic part you can have on properties is through the generic type parameter list you can imagine that some Type on properties is just a different UI for regular generics just like in the above example in case of the function parameter type (the concrete generic type is known only by the type user).

If you don‘t want the property be a generic you‘d use your protocol directly which is currently implicitly any P written as P and also known as an existential type.

(Davide Mendolia) #6

Option 1, use the protocol directly, does not compile as the SaveAddress as PAT in his hierarchy:

class InputPersonalDataViewModel {
    private let saveAddress: SaveAddress = bindSaveAddress()
}

Option 2, use it as Generic constraint so that it can be any concrete subtype of SaveAddress

class InputPersonalDataViewModel<SA: SaveAddress> {
    private let saveAddress: SA

    init(saveAddress: SA) {
        self.saveAddress = saveAddress
    }
}

func main() {
    InputPersonalDataViewModel(saveAddress: bindSaveAddress())
}

So to store Reverse Generic, I need to use regular generics :sweat_smile:

(Davide Mendolia) #7

And Add more protocols to resolve at compile time the Opaque types

protocol InputPersonalDataViewModelProtocol {
}

class InputPersonalDataViewModel<SA: SaveAddress>: InputPersonalDataViewModelProtocol {
    private let saveAddress: SA

    init(saveAddress: SA) {
        self.saveAddress = saveAddress
    }
}

func bindInputPersonalDataViewModel() -> some InputPersonalDataViewModelProtocol {
    return InputPersonalDataViewModel(saveAddress: bindSaveAddress())
}
(Adrian Zubarev) #8

You can create a custom 'type erasure' type if you don‘t really need generics.

I always like to point to the implementation of AnyHashable. The tricks used there are really neat to discover the first time.

(Adrian Zubarev) #9

I moved this thread into a different category because I think the thread fits better #swift-users.

1 Like
(Davide Mendolia) #10

Yes, Any boxing is well know, that's not my goal. I don't want to implement type erasure by hand, _AnyHashableBox it's a trick the compiler implementation can afford, I can't add _Any***Box and Any*** for every type that I want to delegate the implementation to an Opaque type.

(Adrian Zubarev) #11

Sure, I just wanted to mention an alternative solution as there are quite a few that you can use. It really depends on the situation. ;)

1 Like