An inheritance problem

Yep, these are related things. Unfortunately, it seems to me that to allow this, the dispatching problems have to be solved first. :pensive:

Given that Objective-C style dispatching is unacceptable, I hope JIT in Swift's runtime is the next big thing.

This is a valid question even if asked in a provactive way. I'd answer it with a few layers:

  • Protocol-based dispatch may not mean dynamic dispatch in the final binary, even with dynamic libraries involved. However, this is considered an optimization in Swift: if the compiler can "devirtualize" a call through a protocol, it is permitted to. (That can happen even for libraries compiled with library evolution enabled, though I won't get into details of when that can and can't happen.) The catch is that as an optimization, this has to not change which implementation gets called.

  • Binary compatibility does limit what changes can be made, but it doesn't mean no changes can be made—it just means the old stuff has to keep working. That means new features can be added as new entry point symbols in a library, or by rewriting them in terms of the old features if possible, or by storing extra data alongside the existing data. It might mean that the new features are only available when running on newer Apple OSs. (Non-Apple OSs don't have ABI stability at this time.)

  • Source compatibility may actually be trickier here: when recompiling a program changes its behavior, that's concerning. There are discussions elsewhere of when we can introduce such breaking changes, and usually we'll have to keep the old behavior around as a language mode. So yes, it may indeed be hard to make improvements here.


Separately, I wanted to clarify my example, and in doing so I realized that I hadn't quite gotten it right. Here it is again with proper imports:

// Module BaseProtocols
public protocol A {}
public protocol B {}
// Module ConcreteStruct
import BaseProtocols
public struct Concrete: A, B {
  public init() {}
}
// Module MixinProtocol
import BaseProtocols

public protocol Mixin {
  func fin() -> Int
}

extension A: Mixin {
  public func fin() -> Int { return 1 }
}

extension B: Mixin {
  public func fin() -> Int { return 2 }
}
// Module MixinUser
import MixinProtocol
public func doSomething<Value: A>(_ value: Value) {
  value.fin() // from Mixin
}
// App
import MixinUser
import ConcreteStruct

doSomething(Concrete())

In this example, Concrete and Mixin aren't brought together until you get to the app, but the app doesn't know that doSomething(_:) is using Mixin. You could resolve this by saying that it'll use the default conformance A: Mixin, but…then it won't be compatible with situations where Concrete ends up using B: Mixin.

5 Likes

Protocols can refine other protocols, but they don't inherit from other protocols.

This is false. Protocol inheritance is pervasive throughout the standard library.
You cannot extend a protocol to inherit from another protocol.

Just take the protocol hierarchy for number types as an example.

The same thing applies to sequences.

1 Like

Whether or not protocol hierarchies count as "inheritance" is something different people have different opinions on. They do introduce a subtyping relationship and they do allow you to use operations defined on the base, so I personally would say "yes, it counts", but you can state the same relationship as a constraint rather than using inheritance syntax, and that's closer to how they behave at both compile and run time. (Requirements on the parent protocol aren't copied into the child protocol, for example.)

protocol Collection where Self: Sequence { … }
3 Likes

Sorry, didn't mean any harm, just used my charm :sunglasses:.

Why it wouldn't know? The imports are there. Is it a current implementation limitation? If so, than it is to be fixed.

That defaults might be set per unique module per unique member of the type, I wish. (and per unique scope/namespace for this matter)

What situations? In other modules, programmers may set different defaults (not necessarily for the whole protocol, but for its members).
Probably, to not spawn a crazy amount of modules if such a case has arisen, it is better to use namespacing, so a programmer can choose required conformance within a certain scope within a single module.


In your example, though, I don't see any problem. My intuition says that in App doSomething(_:) would use the implementation of A: Mixin from MixinProtocol since it sees the argument as type A and thus it returns 1. The problem would be if it would see it as a Mixin type (because there are multiple defaults for Concrete, the compiler wouldn't know which to pick. And in such a case, the manual resolution (for the current module, but not restricted to it) would be required).

Oh, where did you get these pretty hierarchies? :hushed:

Compiler can probably know alright, but why would the programmer know?

Using a function doSomething<Value: A>(_: Value), requires me to resolve clashing Mixin conformance?! I'd just call bogus and file a bug report without thinking too much about it.

1 Like

It doesn't (shouldn't). App doSomething(_:) would use the implementation of A: Mixin from MixinProtocol since it sees the argument as type A and thus it returns 1, means that you don't have to type a lot in this case. But in general, you might be, if some library has been structured badly (has a lot of unnecessary public exposure or such), although these cases are unlikely, I believe.

This resolution should be transparent obviously. Probably some sort of report (that can be enabled with flag) about compilation should be provided by the compiler about what it has chosen and what not.

I got them off of google images. It's a shame that I can't find a full list of all of the hierarchies.

1 Like

Since it sees the argument as type A, shouldn't it ignore the extensions altogether and fail at compile time? Otherwise it'd be fair to ask the compiler to never fail on "unknown" methods and properties but always perform a dynamic dispatch (as is mostly done in Objective-C).

You are right. In this case - manual resolution for App context. I missed it.

The compiler's not allowed to see whether the body of doSomething(_:) uses Mixin, because maybe it does in this version of the MixinUser library, but doesn't in next year's release. Or vice versa. (When library evolution is enabled, changing the body of a function should not be an ABI-breaking change—maintaining ABI compatibility is already trickier in Swift than in C!)

I also agree with @Lantua that if you can't see a requirement in the declaration of a function, it's weird for the compiler to complain about it. Then again, there are all sorts of function preconditions in Swift that get deferred to runtime ("the upper bound of a Range cannot be less than the lower bound"), and it would actually be very nice if the compiler checked them, so I can't say that this is out of consideration.


Using different conformances in different contexts is a self-consistent answer to this problem, but a somewhat tricky one. Let's make this more concrete using Hashable. (I'm not explicitly separating things into separate modules this time, but imagine I did to make the problem harder.)

protocol A: Equatable {
  var a: Int
}
protocol B: Equatable {
  var b: Int
}

extension A: Hashable {
  func hash(into hasher: inout Hasher) {
    hasher.combine(a)
  }
}
extension B: Hashable { // typo fixed, thanks Lantua
  func hash(into hasher: inout Hasher) {
    hasher.combine(b)
  }
}

struct Concrete: Equatable, A, B {
  var a, b: Int
}

func makeSet<Element: A>(_ element: Element) -> Set<Element> {
  var result = Set<Element>()
  result.insert(element)
  return result
}

func checkIf<Element: B>(_ element: Element, in set: Set<Element>) -> Bool {
  return set.contains(element)
}

let value = Concrete(a: 1, b: 2)
let set = makeSet(value) // if this uses A: Hashable
let isPresent = checkIf(value, in: set) // and this uses B: Hashable
print(isPresent) // then this might print 'false'

I'm oversimplifying here; the Swift runtime, at least, would not consider these Sets to have the same type because they use different conformances. But you get the idea.

One answer to this problem is actually what you said:

Probably, to not spawn a crazy amount of modules if such a case has arisen, it is better to use namespacing, so a programmer can choose required conformance within a certain scope within a single module.

...but today the way to spell "choose a particular (or alternate) conformance" in Swift is "use a wrapper type". I do think it should be easier to define and use such types, but Swift isn't unique here; I don't know of any languages with generics that don't use wrapper types for this besides ML. (ML's approach says you always have to specify which conformance you want—there's no default—and so it's as if all your generic types used a Strategy or delegation pattern.)


We've been ignoring dynamic lookup, and that's because it's really worse than everything else. In the same example as before, what happens for (Concrete() as Any) as? Mixin? (Keep in mind that the conversion to Any might happen in a binary library we can't see.) Without any other context, there are two, equally valid options, and we'd have to decide carefully what happens, or whether such "mixin conformances" can only be used statically or something.

(Objective-C's answer, at the method level, is to pick one based on library load order, which is pretty inscrutable.)

1 Like

Typo, should be B: Hashable.

1 Like

Does this mean that casting to Any disassociate type from its witness record? I thought that it doesn't :thinking:.

It should be, otherwise, what is the point. (Well the answer is pretty obvious :frowning_face:)

should be something like this

At compile-time, it absolutely does (ignoring optimizations when the compiler happens to be able to see all the code). At run-time, it gets a little tricky: you can do dynamic checks (that's what as? is) but the context in which you perform the check is separate from where the type was defined, or where it was coerced into an Any. This can be a problem if there's more than one conformance available (something that only happens today with retroactive conformances, but which would apply to mixin conformances as well).

In my case, though, there isn't a witness record (conformance descriptor) for Concrete: Mixin, only Concrete: A, Concrete: B, A: Mixin, and B: Mixin. Should dynamic casts do an unbounded amount of work to figure out whether Concrete: Mixin can be formed?

(The answer could be yes! Since it can easily be cached, at least. I'm not sure whether that would be my answer, since it makes the cost of as? go up, but this particular issue has a solution that doesn't immediately contradict a design goal. as? may already have to scan through all loaded libraries if it's the first time a particular dynamic check is performed.)

Well, yes, Swift shouldn't be a copy of one other language, but it is absolutely a synthesis of good ideas from other languages (struct semantics are very much like copyable types in Rust, enums are almost directly sum ADTs from several other languages, classes are a lot like Objective-C's but with a C++-style dispatch, protocols behave a lot like ML conformances but associating them with a particular type like Java/C#). If there's one thing that Swift has really innovated on, it's having a stable ABI with non-uniformly-sized values and a non-trivial generics system. No other major language does that as far as I know.

If there's a new idea that's really good, we should definitely consider adding it to Swift. But a new idea is harder to evaluate since we don't know its trade-offs yet.

@Kentzo's comparison to Scala implicits is spot-on, and I'm sorry I didn't concur with that sooner. I think such a feature can be reasonably designed—"if the compiler can't see any other way that this type conforms to this protocol, use this implementation" or even "use this implementation instead of how the type normally conforms to the protocol"—but the trade-off we get is that the conformance might behave differently than one used elsewhere in the program, and that's not currently something that happens in Swift (except, again, in the retroactive conformance case).

What if we split it up into two proposals: a way to use alternate conformances explicitly, and then after that a way to do that implicitly? I should warn that the second one is likely to get a lot more resistance; besides overloading, there aren't that many features in Swift that kick in implicitly. But the first step is something the runtime already (mostly) supports, and you could see types that look something like this (not really proposing this syntax, just for understanding):

Set<Int where Element: Hashable = CustomHashable> // replacing a specific conformance requirement
Set<Int in IntWrapper> // replacing a type, along with all its conformances
Set<@IntWrapper Int> // using property wrapper syntax

It's less obvious how to apply this to generic functions, but :-/

1 Like

Perhaps one way to address it is to extend Swift to have final extension for retroactive protocol conformances in the translation unit where the protocol itself is defined. The assumption is that partial or full redefinition of a (retroactive) protocol conformance implies a weighted judgement by the user and should only be disallowed when library's author has reasons to disallow it.

Doesn't "closest namespace then closest match" suffice?

Ah, I just meant the explicit syntax, since Swift functions today don't allow explicitly providing generic arguments.

Alright, at least to name the problem is better to do nothing. Do you know where are templates for proposals are located? I failed to find the thing on github. : \

Swift Evolution Process: How to propose a change.

Terms of Service

Privacy Policy

Cookie Policy