How do you pick between closures and single-member protocols?

These two are pretty much equivalent in terms of purpose:

protocol StringConsumer {
	func consume(_: String)
}

struct PrinterStringConsumer: StringConsumer {
	func consume(_ str: String) {
		print("Got a string! " + str)
	}
}

And:

let printerStringConsumer: (String) -> Void = { print("Got a string! " + $0) }

Perhaps you could even typealias StringConsumer = (String) -> Void

How do you pick between them?

Here are the differences I observe between them:

  • The closure-appraoch is less future-proof, because you could never add another behavior to StringConsumer.
    • You would need to:
      • change the type from being a typealias to a function type to being a protocol
      • change all the call sites, to pass objects instead of closure
    • Yet adding requirements to an established protocol is also a breaking change, so I'm not sure there's too much of a difference in this regard.
      • The call sites could stay the same, but you would need to update all of the conforming types, or add an extension that satisfies the new protocol requirements with default implmentations
  • Protocols are nominally typed (if you made another protocol with a func consume(_: String) requirement, it'll be a distinct and unrelated type, despite being structurally identical) whereas closure types are structurally typed (all (String) -> Void are StringConsumer, regardless of how you spelled their type annotation)

I also have some experience with Java to compare against, which implements its lambdas as autonomous classes which conform to "functional interfaces" (interfaces with only a single requirement). I find this to usually be pretty annoying as they currently have it implemented, because there are no generalized "function types" outside of those explicitly laboriously spelled out through various functional interface declarations. But it has some benefits:

  1. Forward compatibility: if you need to add new requirements to your interface, you could still preserve the same function signature, but just change all the lambdas into their "autonomous inner class" syntax, which lets you define an anonymous type, implement multiple constructors/methods on it, and instantiate it, all in one shot.
  2. It lets you use lambdas where functional-interface-implementing-objects are required, because lamdas are functional-interface-implementing-objects