Call to static member reaching protocol default implementation even though it is implemented in type

I have a protocol that requires a static var that returns a sequence, and I have an extension that provides a default implementation.

extension DieResultBounds {
    static var sequence: StrideThrough<Bound>  {
        stride(from: bounds.lowerBound, through: bounds.upperBound, by: step)
    }
}

I have a type that conforms to my protocol and provides it’s own sequence (which I’d expected to clue the compiler into the associatedTypes but it made me specify them—that’s a separate issue).

struct DoublingDie: DieResultBounds {
    typealias Bound = Int
    typealias BoundSequence = [Int]
    static let sequence = [2, 4, 8, 16, 32, 64]
}

In my generic type that uses a type T conforming to the protocol there are two places where I get T.sequence.
(1) In an instance variable’s setter:

  if T.sequence.contains(newValue) { … }

(2) In a static var:

    static var allCases: [Self] {
        var cases: [Self] = []
        for value in T.sequence {
            cases.append(Self(rawValue: value))
        }
        return cases
    }

The weird thing that’s happening is that T.sequence is [2, 4, 8, 16, 32, 64] in the setter and stride(from: 2, through: 64, by: 1) in the other call.

How can I get it to only use the default implementation if the type doesn’t have its own implementation, and why is it calling different implementations in the same type?

1 Like

Usually this happens when

  • you didn’t make the member a requirement of the protocol, in which case there’s no code generated to do dynamic dispatch, or
  • you have contextual type information at the use site that the protocol requirement alone doesn’t satisfy (in this case, that would be forgetting the Sequence bound on the associated type, which seems unlikely)

If it is neither of those things, you may have found a bug! Or I may have missed something. Can you post the protocol too?

2 Likes

The most likely scenario is me doing it wrong. This is a whole bunch of protocols and extensions for default implementations with the point of making it simpler at the call site. I will just post the whole mess:

All My Related Code

//NoneRepresentable.swift
public protocol NoneRepresentable {
    static var none: Self {get}
}

//DieProtocol.swift
public protocol Die {
    associatedtype Result
    associatedtype Value
    var result: Result {get}
    var possibleResults: [Result] {get}
    var value: Optional<Value> {get}
    mutating func roll()
    mutating func clear()
    init(result: Result)
}

public protocol NumericDie: Die where Value: Numeric {}

internal protocol MutableDie: Die {
    var result: Result {get set}
}

internal protocol MutableNumericDie: MutableDie, NumericDie {}

// Default implementations

public extension Die where Result: CaseIterable, Result: NoneRepresentable, Result: Equatable {
    var possibleResults: [Result] {
        return Self.Result.allCases.filter { $0 != .none }
    }
}

public extension NumericDie where Result: RawRepresentable, Result: NoneRepresentable, Result.RawValue: Numeric {
    var value: Result.RawValue? {
        guard self.result != .none else { return nil }
        return self.result.rawValue
    }
}

internal extension MutableDie where Result: NoneRepresentable {
    mutating func clear() {
        self.result = .none
    }
    
   mutating func roll() {
        self.result = possibleResults.randomElement() ?? .none
    }
    
    init() {
        self = Self(result: .none)
    }
}

//DieResultsBoundsProtocol.swift
protocol DieResultBounds {
    associatedtype Bound where Bound: Strideable & Numeric
    associatedtype BoundSequence where BoundSequence: Sequence<Bound>
    static var bounds: (lowerBound: Bound, upperBound: Bound) {get}
    static var step: Bound.Stride {get}
    static var sequence: BoundSequence {get}
}

extension DieResultBounds where Bound.Stride: ExpressibleByIntegerLiteral {
    static var step: Bound.Stride {1}
}

extension DieResultBounds {
    static var bounds: (lowerBound: Bound, upperBound: Bound) {
        //HACK - There's probably a better fallback than zeroes
        guard let min = sequence.min(), let max = sequence.max() else { return (lowerBound: Bound.zero, upperBound: Bound.zero) }
        return (lowerBound: min, upperBound: max)
    }
    static var sequence: StrideThrough<Bound>  {
        Swift.stride(from: bounds.lowerBound, through: bounds.upperBound, by: step)
    }
}

//NumericDieResult.swift
struct NumericDieResult<T: DieResultBounds>: CaseIterable, RawRepresentable, NoneRepresentable, Equatable {
    private var _rawValue: T.Bound = T.bounds.lowerBound - 1
    var rawValue: T.Bound {
        get {return _rawValue}
        set {
            if T.sequence.contains(newValue) {
                    self._rawValue = newValue
                } else {
                    self._rawValue = T.bounds.lowerBound - 1
                }
            }
        }
    init(rawValue: T.Bound) {
            self.rawValue = rawValue
        }
    
    static var none: Self { Self(rawValue: T.bounds.lowerBound - 1) }
    static var allCases: [Self] {
        var cases: [Self] = []
        for value in T.sequence {
            cases.append(Self(rawValue: value))
        }
        return cases
    }
}

extension NumericDieResult: ExpressibleByIntegerLiteral where T.Bound: ExpressibleByIntegerLiteral {
    init(integerLiteral value: IntegerLiteralType) {
        guard let rawValue = T.Bound(exactly: value) else { self = .none; return }
        self = Self(rawValue: rawValue)
    }
}

extension NumericDieResult: ExpressibleByFloatLiteral where T.Bound: ExpressibleByFloatLiteral {
    init(floatLiteral value: FloatLiteralType) {
        guard let rawValue = try? T.Bound(floatLiteral: value as! T.Bound.FloatLiteralType) else { self = .none; return }
        self = Self(rawValue: rawValue)
    }
}

//GenericNumericDie.swift
struct GenericNumericDie<T: DieResultBounds>: MutableNumericDie {
    public internal(set) var result = Result.none
    typealias Result = NumericDieResult<T>
}

//DiceTestingPlayground
struct D4Bounds: DieResultBounds { static let bounds = (lowerBound: 1, upperBound: 4) }
struct D6Bounds: DieResultBounds { static let bounds = (lowerBound: 1, upperBound: 6) }
struct D8Bounds: DieResultBounds { static let bounds = (lowerBound: 1, upperBound: 8) }
struct D10Bounds: DieResultBounds { static let bounds = (lowerBound: 1, upperBound: 10) }
struct D12Bounds: DieResultBounds { static let bounds = (lowerBound: 1, upperBound: 12) }
struct D20Bounds: DieResultBounds { static let bounds = (lowerBound: 1, upperBound: 20) }

struct HalfStepsBounds: DieResultBounds {
    static let bounds = (lowerBound: 0.5, upperBound: 3.0)
    static let step = 0.5
}

struct DoublingDie: DieResultBounds {
    typealias Bound = Int
    typealias BoundSequence = [Int]
    static let sequence = [2, 4, 8, 16, 32, 64]
}

typealias D4 = GenericNumericDie<D4Bounds>
typealias D6 = GenericNumericDie<D6Bounds>
typealias D8 = GenericNumericDie<D8Bounds>
typealias D10 = GenericNumericDie<D10Bounds>
typealias D12 = GenericNumericDie<D12Bounds>
typealias D20 = GenericNumericDie<D20Bounds>
typealias DDoubling = GenericNumericDie<DoublingDie>


var die = DDoubling()
die.roll()
//Tracing this we see it get T.sequence as [2, 4, 8, 16, 32, 64] from DoublingDie.sequence

DDoubling.Result.allCases.count
//Returns 63. Tracing this we see it get T.sequence as the stride from the default implementation instead of the [Int] from DoublingDie.sequence, even though T == DoublingDie in both cases

seems like the following change to the allCases implementation may produce the expected behavior:

for value in T.sequence as T.BoundSequence {
    cases.append(Self(rawValue: value))
}

as to why exactly that change is needed, or whether this behavior is expected/erroneous, i'm unsure.

1 Like

spent a little more time looking into this, using a somewhat reduced example of the behavior:

import Foundation

protocol SequenceProvider {
    associatedtype Value where Value: Strideable & Numeric
    associatedtype ValueSequence where ValueSequence: Sequence<Value>
    static var sequence: ValueSequence { get }
}

extension SequenceProvider {
    static var sequence: StrideThrough<Value> {
        Swift.stride(from: 0, through: 100, by: 2)
    }
}

struct SP: SequenceProvider {
    typealias Value = Int
    typealias ValueSequence = [Int]
    static var sequence = [1, 2, 3, 4]
}

struct Wrapper<T: SequenceProvider> {
    static var sequenceValues: [T.Value] {
        var values: [T.Value] = []
        for v in T.sequence { // typechecking resolves this to protocol extension impl
            values.append(v)
        }
        return values
    }

    static var underlyingSequence: T.ValueSequence { T.sequence }
}

let sequenceViaConformance: [Int] = Wrapper<SP>.underlyingSequence
let sequenceViaDefaultExtension: [Int] = Wrapper<SP>.sequenceValues
sequenceViaConformance == sequenceViaDefaultExtension // false

i compiled that snippet with the swiftc -Xfrontend -debug-constraints option to see if the typechecker constraint output might provide any further insights. while i don't really know how to evaluate the output exactly, i did see the following where it looked like the typechecking decisions for the T.sequence implementation were being made:

---Solver statistics---
Total number of scopes explored: 7
Maximum depth reached while exploring solutions: 4
Comparing 2 viable solutions

// `ValueSequence` solution
--- Solution #0 ---
Fixed score: <default 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0>
Type variables:
  $T0 as T.ValueSequence @ locator@0x1356da268 [UnresolvedDot@pcol.swift:24:20 -> member]
... // snip

// `StrideThrough` solution
--- Solution #1 ---
Fixed score: <default 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0>
Type variables:
  $T0 as StrideThrough<T.Value> @ locator@0x1356da268 [UnresolvedDot@pcol.swift:24:20 -> member]
... // snip

// some stuff about comparing the 2 solutions
comparing solutions 1 and 0
Comparing declarations
@inlinable __consuming func makeIterator() -> StrideThroughIterator<Element>
and
__consuming func makeIterator() -> Self.Iterator
(isDynamicOverloadComparison: 0)
comparison result: better
... // snip

// end solution appears to pick the `StrideThrough` implementation
---Solution---
Fixed score: <default 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0>
Type variables:
  $T0 as StrideThrough<T.Value> @ locator@0x1356da268 [UnresolvedDot@pcol.swift:24:20 -> member]
... // snip

i'm not sure if this is a bug or just an ambiguous edge case since there are perhaps multiple different type inference rules at play. definitely curious if someone with more familiarity with the type system has any insight(s) to share!

2 Likes

Thanks for that. I do not have access to any of that debugger stuff so this was helpful to see.