How to create distinct intermediate key-path

I'm having trouble to come up with an intermediate key-path that the library user cannot extend while making it distinct since each branch can have different set of containers. The current idea is by using an empty protocol. Furthermore the compiler does not allow me to create a default argument (here I could workaround by providing an overload without the root argument and defaulting it to the generic function but that's also a little tedious).

public struct Root {
  public let first = BranchA()
  public let second = BranchB()
}

public struct BranchA : Branch {
  var containerA: Int { return 1 }
  var containerB: Int { return 2 }
}

public struct BranchB : Branch {
  var containerC: Int { return 3 }
  var containerD: Int { return 4 }
}

public protocol Branch {}

// If I try to set a default root key path then I'll get the following error:
// Key path value type 'BranchA' cannot be converted to contextual type 'B'
public func work<B>(
  for rootKeyPath: KeyPath<Root, B> /* = \.first */,
  on containerKeyPath: KeyPath<B, Int>
) where B : Branch {
  // Just a small test
  let root = Root()
  print(root[keyPath: rootKeyPath.appending(path: containerKeyPath)])
}

Any ideas?

Currently default arguments aren't allowed to be conditional on generic parameters, which is what you'd need for this to work—you want rootKeyPath to default to \.first when the type argument B can also default to BranchA. You could provide a second overload that provides the default argument.

Yeah I just thought that it's probably a missing feature. Do you have any alternative suggestions regarding the intermediate type, maybe there a better solution without an empty protocol?! That is an implementation detail and I'd want it to be a closed protocol instead if there is no better solution here.

What are you trying to enforce with the empty protocol? Is there any reason not to allow an arbitrary type there, and allow the set of properties on Root to naturally limit the set of possible arguments?

Well I want to enforce a parameter to branches which would default to a specific branch to shorten the API for convenience. The real world function is far more complicated and the declaration would look like this:

  func write<Container, Branch, TriggerObservable>(
    value: Container.WriteValue,
    on startPath: KeyPath<BluetoothCharacteristic, Branch>,
    to endPath: KeyPath<Branch, Container>,
    handleError: ErrorHandler<TriggerObservable>?,
    recover: RecoveryHandler?,
    complete: CompletionHandler?
  ) where
    Container : WritableCharacteristicContainer,
    Branch : CharacteristicBranch,
    TriggerObservable : ObservableType

// Usage (optionals defaulted to `nil`)
peripheral.write(value: 0x01, on: \.bootloader, to: \.dfu.controlPoint)
// defaults to `\.application` branch
peripheral.write(value: 0x00, to: \.deviceSetting.bond)
// allowed
peripheral.write(value: 0x00, on: \.application, to: \.deviceSetting.bond)

Edit: Actually in my case I can workaround it by making Branch an empty base class and create closed sub-classes as specific branches.

It seems to me that at some point you'll need to put formal requirements on Branch to provide the functionality write needs to perform on that key path, at which point it'll no longer be an empty protocol.

Not really no, branches could be completely static metatypes, but we don't access to static members through key-pathes yet. The most interesting information for that function is accumulated in the Container type. The function itself will just walk the from root to the branch (one step) then walk the path until a writable container can be accessed (always 2-3 steps) and then it extracts a lot of generic type data just from the Container type.

Long story short. I need to distinguish between branches in two ways. Each container must know on which branch it's on (that can be done by supplying a hidden flag in the container type itself) and for the library user during the function call as shown above. That way I can duplicate some peripheral characteristics since a peripheral can operate in two modes (application / bootloader) and can share similar or completely different characteristics.

// Hand code the generic?
public func workA(
    for rootKeyPath: KeyPath<Root, BranchA> = \.first,
    on containerKeyPath: KeyPath<BranchA, Int>
    ) {
    work(for: rootKeyPath, on: containerKeyPath)
}

The overload you presented does not make sense, at least in my case, because there will be only at most one instance of BranchA. The workaround was to provide an overload without the first key-path and specify it in the implementation of the function.