Zoë Smith
Zoë Smith

Reputation: 1084

Providing specialized initializers for a generic struct

I have a simple generic struct defined - its only requirements is that its stored properties be Comparable:

struct Bounds<A: Comparable> {
    let lower: A
    let upper: A
}

However, I'd like to provide a couple of specialized initializers for the struct, which would use some math operations to set up the properties.

    init(value: Double, tolerance: Percentage) {
        self.lower = value * ( 1 - tolerance )
        self.upper = value * ( 1 + tolerance )
    }

    init(value: Measurement, tolerance: Percentage) {
        self.lower = value.value * ( 1 - tolerance )
        self.lower = value.value * ( 1 - tolerance )
    }

The result should obviously be two different structs, where A is a Double or a Measurement.

But how do I do this?

I can't provide specialized init methods in the definition as the compiler will complain that Double is not convertible to A. OK...

I can't provide the init methods in individual extensions constrained by specific types (where A == Double) as the compiler complains:

Same-type requirement makes generic parameter 'A' non-generic

Maybe I should be using a protocol to which both Double and Measurement conform in initialization, but that seems odd, since the Bounds struct should just care that they both conform to Comparable.

I feel like I'm either missing something really simple, or trying to do something really misguided with generics. Which is it, SO?

Upvotes: 5

Views: 898

Answers (4)

Michael Morris
Michael Morris

Reputation: 329

The correct answer to this is Martin R.'s "workaround." The issue here is that Comparable does not have any of these math operations defined on it, or even any guarantee that it is a numeric type at all. It could just as easily be a string, or an array, or any other type that implements Comparable.

So, yes, you must write extensions constrained to either a common protocol that implements these operators or to the concrete types in question. For example:

extension Bounds where A: FloatingPoint {
    init(value: A, tolerance: Percentage) {
        self.lower = value * ( 1 - tolerance )
        self.upper = value * ( 1 + tolerance )
    }
}

Or, if Measurement does not conform to FloatingPoint:

extension Bounds where A == Measurement {
    init(value: A, tolerance: Percentage) {
        self.lower = value.value * ( 1 - tolerance )
        self.upper = value.value * ( 1 + tolerance )
    }
}

Also, since you probably don't want to use any non-numeric types for your bounds then I would define it as something along these lines:

struct Bounds<A: Numeric & Comparable> {
    let upper: A
    let lower: A
}

Upvotes: 0

Luca Angeletti
Luca Angeletti

Reputation: 59506

The main problem is that you are performing operations on types where that operations have not been defined.

E.g.

value * ( 1 - tolerance )

The operation - between Int and Tolerance where is defined?

This is how you can fix it

protocol BoundsType: Comparable {
    func *(lhs: Self, rhs: Self) -> Self
    var prev: Self { get }
    var next: Self { get }
    init(double: Double)
    init<M:Measurement>(measurement:M)
}

protocol Percentage {
    associatedtype BoundsType
    var toBoundsType: BoundsType { get }
}
protocol Measurement {
    associatedtype BoundsType
    var toBoundsType: BoundsType { get }
}

struct Bounds<A: BoundsType, P:Percentage, M:Measurement
              where P.BoundsType == A, M.BoundsType == A> {
    let lower: A
    let upper: A

    init(value: Double, tolerance: P) {
        self.lower = A(double:value) * (tolerance.toBoundsType.prev)
        self.upper = A(double:value) * (tolerance.toBoundsType.next)
    }

    init(value: M, tolerance: P) {
        self.lower = A(measurement:value) * tolerance.toBoundsType.prev
        self.upper = A(measurement:value) * tolerance.toBoundsType.next
    }
}

Upvotes: 0

Martin R
Martin R

Reputation: 539765

Not exactly what you are asking for, but a possible workaround (Swift 3):

extension Bounds where A: FloatingPoint {
    init(value: A, tolerance: A) {
        self.lower = value * ( A(1) - tolerance )
        self.upper = value * ( A(1) + tolerance )
    }
}

let b = Bounds(value: 4.0, tolerance: 0.1)
print(b.dynamicType) // Bounds<Double>

Upvotes: 1

dfrib
dfrib

Reputation: 73186

You could restrict your generic more strongly, to access in-protocol blueprinted methods for transforming e.g. a Double value to the type of the generic.

protocol FromDoubleTransformable {
    static func doubleToSelf(from: Double) -> Self
}

/* drawback: only types conforming to 'FromDoubleTransformable'
   will be able to be used as generic in 'Bounds' below          */
extension Int: FromDoubleTransformable {
    static func doubleToSelf(from: Double) -> Int {
        // simple example without any out-of-bounds checking
        return Int(from)
    }
}

struct Bounds<A: protocol<Comparable, FromDoubleTransformable>> {
    let lower: A
    let upper: A

    init(value: Double, tolerance: Double) {
        self.lower = A.doubleToSelf(value * ( 1 - tolerance ))
        self.upper = A.doubleToSelf(value * ( 1 + tolerance ))
    }
}

let foo = Bounds<Int>(value: 120, tolerance: 0.1)
print(foo.dynamicType)      // Bounds<Int>
print(foo.lower, foo.upper) // 108 132

Upvotes: 0

Related Questions