Hi, i've seen a few proposals dating back from 2015 & 2016 on the forum, regarding automatic protocol forwarding (aka declaring protocol conformance while forwarding calls for that protocol to a subcomponent that actually implements it). They seem to have all been rejected based on the fact that it wouldn't work in all cases, yet i think it would be quite a useful feature, even if working only for the simple cases.
Go has a very natural way to doing this, thanks to its embedding feature, and it's something i dearly miss when working with Swift.
Has any progress to the language in the past years made that feature possible now ?
For what it's worth I imagine what is being alluded to here is similar to Scala 3's export clauses:
class BitMap
class InkJet
class Printer:
type PrinterType
def print(bits: BitMap): Unit = ???
def status: List[String] = ???
class Scanner:
def scan(): BitMap = ???
def status: List[String] = ???
class Copier:
private val printUnit = new Printer { type PrinterType = InkJet }
private val scanUnit = new Scanner
export scanUnit.scan
export printUnit.{status as _, *}
def status: List[String] = printUnit.status ++ scanUnit.status
I've peripherally heard conversation about this, but this is my first time directly interacting with the idea.
Purely to provide at least some example, but not at all an endorsement of this particular approach over any other, I think this pseudo-code is along the lines of what we're talking about?:
struct Cyborg: HasHumanGenmoe {
#useToSatisfy(\.humanGenome)
var human: Human
}
struct Human: HasHumanGenome {
var humanGenome: HumanGenome
}
protocol HasHumanGenome {
var humanGenome: HumanGenome { get }
}
struct HumanGenome { }
In the example above, the new syntax is standing in place of declaring the property:
struct Cyborg: HasHumanGenmoe {
var human: Human
var humanGenome: HumanGenome { human.humanGenome }
}
When I started writing this I thought that there wasn't much value in the case of forwarding a single property, but the syntax that I ultimately came up with #useToSatisfy(_:) convinced me a little more that there could be some real value here.
It could also be used for full protocol conformance, which I suppose now that I think about it might be the biggest advantage:
I don't feel sure that this example with StringProtocol is logically sound. Are there issues with this concept relating to associated types of StringProtocol or other issues I'm not aware of?
At this point, it would be nice to be able to say something like "forward all functions from the Reader protocol to the self.reader property, and all functions from the Writer protocol to the self.writer property",
without explicitely redefine the functions at the FileSystem level
Could be something like
struct FileSystem: ReaderWriter {
@implements(Reader) let reader: FileReader
@implements(Writer) let writer: FileWriter
}
My re-read of the linked discussions is that a missing component was an exploration of concrete examples substantiating the claim that it would be āquite a useful feature.ā Not examples of demonstrating how it could work (the rules around whatās proposed), but rather examples of real-world use cases that would be improved by the presence of the feature.
Keep in mind that with macros on the horizon, a feature that forwards to another typeās implementation at runtime would (or at least, could) be a subtly different creature from a synthesized conformance, and one salient question would be under what circumstances that behavior would be desirable and independently valuable when macros are available.
Consider specifically that specifying that implementations of one protocolās requirements would be forwarded from one type to another would mean, if Alice extends Bobās type to provide a custom implementation of a requirement of Charlieās protocol which has a default implementation, Dorisās type which conforms to Charlieās protocol by forwarding to Bobās type would behave differently at runtime if Aliceās library is imported by the end userāwithout any party being able to reason about it. Further, if Charlie adds a new requirement to the protocol, which is an ABI-compatible change if a default implementation is provided, but both Bob and Doris have already implemented that functionality before it was a protocol requirement to do so, Dorisās type would (depending on the runtime version of Charlieās library) dispatch to Bobās implementation of the requirement rather than Dorisās own.
I was indeed speaking about a completely static behavior, so definitely something that an evolved macro system could do in theory. I'm not sure however a macro system would be able to deal with edge cases, such as manually defining parts of the protocol conformance, nor how the ergonomic would look like (it would, for example, need to have access to the whole type, and not just the generated properties).
Consider specifically that specifying that implementations of one protocolās requirements would be forwarded from one type to another would meanā¦
Swift type system has become way too complex for me to think about all the edge cases that one could encounter, so i'm fully trusting you on finding potential problems.
However, it would i think be a bit problematic if this feature could be implemented in such wide varieties of language such as go, scala or kotlin, but not swift.
but rather examples of real-world use cases that would be improved by the presence of the feature.
You won't obviously find real-world swift code with this design, but i think one could easily find examples in go code base (since go doesn't have inheritance and classes at all). It seems to me that it's a very critical part of a system based on structs instead of classes (which, i think is encouraged in Swift).
I've come here looking for solution to this specific problem.
Obviously I can implement protocol forwarding by hand, but that's a lot of boilerplate, that can be avoided "easily"?
As for a real-world example we can consider this: imagine you have an instance implementing a protocol. Now you need to add some behaviour to it. Usually you would do this implementing a wrapper:
protocol A {
func funcA()
func funcB()
}
class Original: A {
func funcA() {}
func funcB() {}
}
class LoggingOriginal<T: A>: A {
@implements(A)
private let base: T
init(base: T) { self.base = base }
func funcA() {
os.log()
base.funcA()
}
}
With this approach LoggingOriginal conformance to A is implemented with explicit funcA defined on it and any missing protocol requirements are simply forwarded to the base.
It's similar, but not the same.
My suggestion is along the lines of: "let me implement couple of protocol requirements myself and whatever I don't implement forward to this member"
This can be done explicitly, but I'd like a language feature to make all this automatic.
Currently, macros are indeed not able to do that. At the moment, they have no way of getting any type information, which means that they can't possibly know what requirements a protocol has and thus they can't forward implement a generic protocol.