[could become a pitch] universal forwarding / implicit conversions

Motivation

Right now, there are at least three proposals (SE-0258: Property Delegates - #41 by shpakovski, SE-0253: Static callables, [Accepted] SE-0249: Key Paths Expressions as Functions - #4 by John_McCall) which possibly could be substituted with a more general concept.
I have no illusions that this post will actually lead to a replacement for any of those proposals, and haven't spend much time thinking of the downsides of the idea - but I'd still like to share it, as I'd like to see more discussions with a wider scope (and imho it's more productive than only posting vague complaints and requests for more holistic solutions ;-).

The concept

I hope it's as easy to explain as I think ;-)
The very core of the idea is to allow a type to act as another type.
I really like solid examples, so I'll take the one case where Swift already does the thing I'm talking about:
Wherever Swift expects a parameter of Optional<T>, it will also accept a value of type T (this behavior actually has its disadvantages, but overall, it seems to be better than the alternative).

The conversion from T to Optional<T> is baked into the language, but it could also be a feature open to be used for other applications.

Strawman syntax

There might be other and better paths, but the most obvious solution for me is an option to "bless" members so that they can directly be accessed in places where only their owning object is written out:

extension Result {
    @forward var optional: Success? {
        switch self {
        case .success(let value):
            return value
        default:
            return nil
        }
    }
}

// this would allow a Result to be used like an Optional
if let value = methodReturningResult()?.someMemberOfSuccess {
....

The choice of @forward wouldn't be my favorite, but as this isn't even a pitch, imho there's no need to argue about the color of a bikeshed that doesn't exist ;-)

Applications

Let's look how some examples from other proposals might be implemented with forwarding...

Unwrappable

The example above is basically the implementation of Unwrappable.

Static callables

struct Adder {
    var base: Int
    @forward func call(_ x: Int) -> Int {
        return base + x
    }
}

let add3 = Adder(base: 3)
add3(10) // => 13

Property Delegates

struct Lazy<T> {
    private var storage: T?
    private var creator: () -> T
    
    init(_ creator: @escaping () -> T) {
        self.creator = creator
    }
    
    @forward var value: T { // edited
        set {
            self.storage = newValue
        }
        mutating get {
            if let storage = storage {
                return storage
            } else {
                let value = creator()
                self.storage = value
                return value
            }
        }
    }
}

var example = Lazy {
    return 42
}

let sum = example + 1
print(sum) // 43

Reinventing broken C math ;-)

extension Double {
    @forward var float: Float { // edited
        get {
            return Float(self)
        }
        set {
            self = Double(newValue)
        }
    }
}

let x = 10 as Double
let y = 20 as Float
let z = y/x // 2 as Float
5 Likes

The reason I would prefer property delegates over this solution is specificity. In that world, if I want a certain variable on my structure to have some special handling, I would need to specify that while declaring the member variable. With this solution, a type forwarding extension would apply to all instances of that type.

I can imagine this causing issues: for instance, it might be the case that an updated dependency introduces some forwarding extensions which introduces unexpected behavior: suddenly some Double is interpreted as an Int somewhere and I have no idea why.

Also it seems like there would be issues with ambiguity. For instance in the Double example you give: what if Float is also forwarded to Double? Which result type should be inferred?

Being able to express whatever you like in a language might seem like an amazing idea but it would come with huge disadvantages. People already complain in nearly every pitch that it adds too much cognitive load to the language. Personally I don‘t understand that trend as it prevents the language to evolve. I think it‘s great if we can introduce complex features that we can extend and refine later by more fine grained features (a very good example are opaque types and reverse generics).

That said, even though your pitch spund like you would open up an existing feature for custom use-cases, I think it can potentially do more harm than good. I already can implement two distinct structs A and B and make let a: A = B() compile (it requires an ugly hack but it‘s not impossible). Again these structs are distinct and do not share any super-type relation. At this point you might realize how hard it becomes to reason about this line of code. If you add even more complex types into the game, this would become very chaotic. I appreaciate your time and respect your general ideas, but personally I would not support this idea unless it can demonstrate valuebale examples that would pay of the complexity of this generalization.

I didn't expect anything but pushback, but if replacing three other proposals isn't enough examples, what is?

Sorry but I personally don‘t understand then how this would replace the other proposals. Static callable for example is a language feature just like subscripts from my point of view, I don‘t understand how your pitch would also replace subscripts? If we had callables first and then introduce subscripts we would end up with the same discussion like in the static callable threads.

I think the name might be misleading here. If I'm understanding your examples correctly, this is a proposal to add user-defined implicit conversions to the language (which were previously removed from a very early Swift version, if I recall). When people have talked about “forwarding” they have generally meant a method of automatically forwarding certain methods and/or property accesses to an instance variable without having to manually write forwarding methods. I doubt user-defined implicit conversions will be added, for all the reasons that they were removed (e.g. significant type-checking performance issues, etc) but I think people would be pretty receptive to a forwarding feature (e.g. forwarding a type's protocol conformance to a property conforming to that protocol).

I also think you might be missing some @forward attributes in your examples, or I'm missing something fundamental about the Lazy<T> and Double examples.

2 Likes

What ugly hack are you referring to? I'm not aware of any way to do such a thing.

Yes. For reference: [Proposal Draft] automatic protocol forwarding. The draft in that thread is out of date. I worked on a second draft incorporating some discussion feedback but that was never published. I will return to it if / when somebody willing to collaborate as an implementer comes along.

Swift supports implicit supertype conversions, but is very careful about what subtype relationships exist. I agree that we won't see implicit conversions outside of this narrow scope.

1 Like

IIRC in previous Swift versions you could abuse some of the invisble but public _ExpressibleBy* protocols (notice the underscore prefix) for doing such things. I do not recommend that to anyone though. These implementation detail protocols should be closed in my opinion, but we don‘t have that feature.

The last sentence is true — but at the same time, it would be used as in the first sentence: Lazy is dedicated to wrap member variables (it would be rather odd to use it as, for example, function parameter). Therefor, it should always do the forwarding, as that is its main responsibility.

I can't predict all consequences of the idea, but I'm sure it would cause issues — simply because there is hardly anything that does not.
My hope was that the headline for the math-example makes sure this isn't considered to be a primary usecase, and I would recommend such applications outside a rather narrow scope.

Ambiguity could indeed happen, so this would definitely be a feature to be used carefully (just like existing concepts which share that problem). I'm confident those could be resolved with simple (though not always intuitive) rules; alas, the whole topic is to hypothetical for such detailed discussion.

An alternative syntax could be to have a @valueForwarding or @wrapping, etc. attribute on the type itself, and then always use a particular attribute, such as value. This means that a type cannot wrap multiple different types, and we could also enforce the attribute to be on the declaration of the the type, both of which eliminate some surprises.

My view is that the complaints are not about avoid such features completely, but that even powerful features can be made easier to understand for all, not just wizarding work for the ones in the know. And it takes a lot of design work to get it right.

1 Like

Yet another proposal - but it's imho quite similar to https://forums.swift.org/t/se-0253-static-callables/22243:
[Accepted] SE-0249: Key Paths Expressions as Functions - #4 by John_McCall