Automatic Protocol forwarding

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 ?

6 Likes

Could you provide an example on protocol forwarding?

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

Quite a nice feature that helps with "composition over inheritance". If someone wanted to read up more here's some docs: https://docs.scala-lang.org/scala3/reference/other-new-features/export.html

I don't think it has been actively discussed in Swift though.

3 Likes

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:

struct AirportCode: StringProtocol {

    #useToSatisfy(StringProtocol)
    fileprivate(set) var code: String

    init? (_ rawCode: String) { /*validation logic*/ }
}

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?

Sure, let me give you an example

protocol Reader {
func read(url: URL) -> Data?
}

protocol Writer {
func write(data: Data, to url: URL)
}

protocol ReaderWriter: Reader, Writer {}

struct FileReader: Reader { 
func read(url: URL) -> Data? { return FileManager.default.read(at: url) }
}

struct FileWriter: Writer {
func write(data: Data, to url: URL) { FileManager.default.write(data, to: url) }
}

struct FileSystem: ReaderWriter {
let reader: FileReader
let writer: FileWriter
}

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
}
3 Likes

I searched in the forum and found this :

as well as a few others from the same period.

Kotlin use the Ā“by’ keyword for similar things IIRC

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.

3 Likes

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).

3 Likes

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.

Are there any issues with this approach?

1 Like

This seems like something the upcoming macros feature could enable but I don't know if it has all the needed insight. @Douglas_Gregor?

I hadn’t seen any of this before—seems like the same idea:

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.

As mentioned up-thread by @Jon_Shier , I believe it should be possible to implement Scala's export clause using a freestanding macro in Swift.

Worth giving it a shot implementing something of that shape or similar:

protocol P {  func p()    }
struct Impl { func p() {} }

struct X: P {
  let impl: Impl
  #export(impl, \.p)
}

And if protocol has 10 methods, I need 10 macros?
This is pretty much what we already have - explicit implementation, with a bit less lines of code

With Macros we can now imagine something as follow:

// I use the previous example with human genome and cyborg
@ForwardImplementation(ofProtocol: HasHumanGenome.self, toPropertyNamed: "human")

I'm working on it but oh boy that's complex/new to me so not sure if there are some issues that could prevent a macro to be able to do that

The idea is that it would extend the Cyborg struct and forward every variable and func to the referenced property

// With macro errors thrown if the property doesn't exist / doesn't implement the protocol
extension Cyborg: HasHumanGenmoe {
    var humanGenome: HumanGenome { human.humanGenome }
}

In this case, it would be possible to remove so many boilerplate code with only 1 macro

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.

3 Likes