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 ?

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

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

1 Like