David Rönnqvist
David Rönnqvist

Reputation: 56625

Can a condition be used to determine the type of a generic?

I will first explain what I'm trying to do and how I got to where I got stuck before getting to the question.


As a learning exercise for myself, I took some problems that I had already solved in Objective-C to see how I can solve them differently with Swift. The specific case that I got stuck on is a small piece that captures a value before and after it changes and interpolates between the two to create keyframes for an animation.

For this I had an object Capture with properties for the object, the key path and two id properties for the values before and after. Later, when interpolating the captured values I made sure that they could be interpolated by wrapping each of them in a Value class that used a class cluster to return an appropriate class depending on the type of value it wrapped, or nil for types that wasn't supported.

This works, and I am able to make it work in Swift as well following the same pattern, but it doesn't feel Swift like.


What worked

Instead of wrapping the captured values as a way of enabling interpolation, I created a Mixable protocol that the types could conform to and used a protocol extension for when the type supported the necessary basic arithmetic:

protocol SimpleArithmeticType {
    func +(lhs: Self, right: Self) -> Self
    func *(lhs: Self, amount: Double) -> Self
}

protocol Mixable {
    func mix(with other: Self, by amount: Double) -> Self
}

extension Mixable where Self: SimpleArithmeticType {
    func mix(with other: Self, by amount: Double) -> Self {
        return self * (1.0 - amount) + other * amount
    }
}

This part worked really well and enforced homogeneous mixing (that a type could only be mixed with its own type), which wasn't enforced in the Objective-C implementation.

Where I got stuck

The next logical step, and this is where I got stuck, seemed to be to make each Capture instance (now a struct) hold two variables of the same mixable type instead of two AnyObject. I also changed the initializer argument from being an object and a key path to being a closure that returns an object ()->T

struct Capture<T: Mixable> {
    typealias Evaluation = () -> T
    let eval: Evaluation

    let before: T
    var after: T {
        return eval()
    }

    init(eval: Evaluation) {
        self.eval = eval
        self.before = eval()
    }
}

This works when the type can be inferred, for example:

let captureInt = Capture {
    return 3.0
}
// > Capture<Double>

but not with key value coding, which return AnyObject:\

let captureAnyObject = Capture {
    return myObject.valueForKeyPath("opacity")!
}

error: cannot invoke initializer for type 'Capture' with an argument list of type '(() -> _)'

AnyObject does not conform to the Mixable protocol, so I can understand why this doesn't work. But I can check what type the object really is, and since I'm only covering a handful of mixable types, I though I could cover all the cases and return the correct type of Capture. Too see if this could even work I made an even simpler example

A simpler example

struct Foo<T> {
    let x: T
    init(eval: ()->T) {
        x = eval()
    }
}

which works when type inference is guaranteed:

let fooInt = Foo {
    return 3
}
// > Foo<Int>
let fooDouble = Foo {
    return 3.0
}
// > Foo<Double>

But not when the closure can return different types

let condition = true
let foo = Foo {
    if condition {
        return 3
    } else {
        return 3.0
    }
}

error: cannot invoke initializer for type 'Foo' with an argument list of type '(() -> _)'

I'm not even able to define such a closure on its own.

let condition = true // as simple as it could be
let evaluation = {
    if condition {
        return 3
    } else {
        return 3.0
    }
}

error: unable to infer closure type in the current context

My Question

Is this something that can be done at all? Can a condition be used to determine the type of a generic? Or is there another way to hold two variables of the same type, where the type was decided based on a condition?


Edit

What I really want is to:

  1. capture the values before and after a change and save the pair (old + new) for later (a heterogeneous collection of homogeneous pairs).
  2. go through all the collected values and get rid of the ones that can't be interpolated (unless this step could be integrated with the collection step)
  3. interpolate each homogeneous pair individually (mixing old + new).

But it seems like this direction is a dead end when it comes to solving that problem. I'll have to take a couple of steps back and try a different approach (and probably ask a different question if I get stuck again).

Upvotes: 4

Views: 351

Answers (2)

matt
matt

Reputation: 534885

in my full example I also have cases for CGPoint, CGSize, CGRect, CATransform3D

The limitations are just as you have stated, because of Swift's strict typing. All types must be definitely known at compile time, and each thing can be of only one type - even a generic (it is resolved by the way it is called at compile time). Thus, the only thing you can do is turn your type into into an umbrella type that is much more like Objective-C itself:

let condition = true
let evaluation = {
    () -> NSObject in // *
    if condition {
        return 3
    } else {
        return NSValue(CGPoint:CGPointMake(0,1))
    }
}

Upvotes: 0

Vik
Vik

Reputation: 1927

As discussed on Twitter, the type must be known at compile time. Nevertheless, for the simple example at the end of the question you could just explicitly type

let evaluation: Foo<Double> = { ... }

and it would work.

So in the case of Capture and valueForKeyPath: IMHO you should cast (either safely or with a forced cast) the value to the Mixable type you expect the value to be and it should work fine. Afterall, I'm not sure valueForKeyPath: is supposed to return different types depending on a condition.

What is the exact case where you would like to return 2 totally different types (that can't be implicitly casted as in the simple case of Int and Double above) in the same evaluation closure?

Upvotes: 4

Related Questions