Omegaman Omegaman - 2 months ago 8
Swift Question

How can I convert between related types through a common initializer?

I'm trying to build up a family of types that can be converted to each other. For example, Float and Double can be converted to each other through their initializers. I'd like to not have to create an exhaustive list of initializers showing that each type can convert to every other type.

I tried to do something like this in a Playground, but it crashes:

protocol FloatConvertible {
init(_ x:FloatConvertible)
}

extension FloatConvertible {
init(_ x:FloatConvertible){self.init(Self(x))}
}

extension Float:FloatConvertible {}
extension Double:FloatConvertible {}

func transmute<T:FloatConvertible, U:FloatConvertible>
(a:T, b:U) -> T {
return T(b)
}

transmute(Float(3.1), b: Double(2.6))


My eventual goal isn't just to do the conversion, but to multiply
a
by
b
like so:

func *<T:FloatConvertible, U:FloatConvertible> (a:T, b:U) -> T{
return a * T(b)
}


So that I can express the multiply.

Is there a way to do this? I think part of the problem is winding up with a structure that looks like
Double(Double(Double(Double(...)))
, but I don't think I can put a constraint that ensures
T != U
.

Answer

The problem is that in your init(_ x:FloatConvertible), Swift cannot infer what the concrete type of x is. It just knows that it's a FloatConvertible. Therefore when you try to do Self(x), while it can infer the concrete type of Self, it doesn't know which initialiser you want to call, meaning that it will default to your init(_ x:FloatConvertible) initialiser, thus creating an infinite loop.

If you give your custom initialiser an argument name, you'll see that Swift complains that it can't find the correct initialiser:

protocol FloatConvertible {
    init(c x:FloatConvertible)
}

extension FloatConvertible {
    init(c x:FloatConvertible) {
        // error: missing argument name 'c:' in call
        // (i.e it can't find the concrete type's initialiser)
        self.init(Self(x)) 
    }
}

A potential solution therefore is to resolve this at runtime by switching over the concrete types that x could be. However this isn't nearly as good as resolving this statically, as you can benefit from increased safety and in some cases increased performance.

In order to do this statically, you could add a generic _asOther 'shadow' function to your protocol that can convert a given floating point type to another, as well as adding the concrete type's initialisers to your protocol requirement.

This will save you from having to list out all the possible combinations of conversions – you can now just invoke _asOther from your initialiser.

protocol FloatConvertible {
    init(_ other:Float)
    init(_ other:Double)
    init(_ other:CGFloat)
    init(fromOther x:FloatConvertible)

    func _asOther<T:FloatConvertible>() -> T
}

extension FloatConvertible {
    init(fromOther x:FloatConvertible) {self = x._asOther()}
}

// note that we have to implement these for each extension,
// so that Swift uses the concrete types of self, preventing an infinite loop
extension Float : FloatConvertible {
    func _asOther<T:FloatConvertible>() -> T {return T(self)}
}

extension Double : FloatConvertible {
    func _asOther<T:FloatConvertible>() -> T {return T(self)}
}

extension CGFloat : FloatConvertible {
    func _asOther<T:FloatConvertible>() -> T {return T(self)}

    // note that CGFloat doesn't implement its own initialiser for this,
    // so we have to implement it ourselves
    init(_ other:CGFloat) {self = other}
}

func transmute<T:FloatConvertible, U:FloatConvertible>(value: T, to: U.Type) -> U {
    return U(fromOther: value)
}

let f = transmute(value: CGFloat(2.6), to: Float.self)
print(type(of: f), f) // prints: Double 2.59999990463257

In the initialiser, _asOther will be called on the input value, with the type of self being inferred for the generic parameter T (in this context self is guaranteed to be a concrete type). The _asOther function will then get called on x, which will return the value as the given destination type.

Note that you don't have to use the fromOther: argument label for your custom initialiser – this will still work without any label. Although I would strongly advocate for using it to catch any problems with your code at compile time (Swift would accept code that would cause infinite loops at runtime otherwise).


Also as a side note, you should maybe re-think your design for how you want your * overload to work. It would make more sense to be returning the more precise type that you input into it (i.e Float * Double = Double) – otherwise you're just needlessly losing precision.

Comments