For Friday Fun, I wanted to model Angles in interchangable formats. I'm not sure if I've done it in the most Swift idiomatic way, but I'm learning. So I have an Angle
protocol, and then 3 different struct types (Radians, Degrees, and Rotations) which all conform to the Angle protocol. I'd like to be able to add/subtract them, but the trick is, I want the lhs
argument to dictate the return type. For example:
Degrees(180) + Rotations(0.25) --> Degrees(270)
or
Rotations(0.25) + Radians(M_PI) -> Rotations(0.75)
What I was hoping was I could do something like
func + (lhs:Angle, rhs:Angle) -> Angle {
return lhs.dynamicType(rawRadians: lhs.rawRadians + rhs.rawRadians)
}
The Angle
protocol requires a var rawRadians:CGFloat { get }
as well as an init(rawRadians:CGFloat)
I could do this with a Smalltalk-esque double dispatch approach, but I think there most be a more Swift appropriate approach (especially one that requires less code, double dispatch requires a lot of boilerplate code).
You just need a generic addition:
func +<A: Angle>(lhs: A, rhs: Angle) -> A {
return A(rawRadians: lhs.rawRadians + rhs.rawRadians)
}
This way the addition will return whatever type is on the lhs.
Generally speaking, if you're using dynamicType
, you're probably fighting Swift. Swift relies more on generics and protocols (i.e. static type information at compile time) rather than dynamic dispatch (i.e dynamic type information at runtime).
In this code, as you say, A
is a placeholder for "some type, that conforms to Angle
, to be determined at compile time." So in your first example:
Degrees(180) + Rotations(0.25) --> Degrees(270)
This actually calls a specialized function +<Degrees>
. And this:
Rotations(0.25) + Radians(M_PI) -> Rotations(0.75)
calls a (logically) different function called +<Rotations>
. The compiler may choose to optimize these functions into a single function, but logically they are independent functions, created at compile time. It's basically a shortcut for writing addDegrees(Degrees, Angle)
and addRotations(Rotations, Angle)
by hand.
Now, to your question about a function that takes two Angles and returns.... well, what? If you want to return an Angle
in this case, that's easy, and exactly matches your original signature:
func +(lhs: Angle, rhs: Angle) -> Angle {
return Radians(rawRadians: lhs.rawRadians + rhs.rawRadians)
}
"But..." you're saying, "that returns Radians
." No it doesn't. It returns Angle
. You can do anything "angle-like" on it you want. The implementation details are opaque as they should be. If you care that the underlying data structure is a Radians
, you're almost certainly doing something wrong.
OK, there is one side case where it may be useful to know this, and that's if you're printing things out based on how you go them. So if the user gave you degree information to start, then you want to print everything in degrees (using a description
method that you didn't mention). And maybe that's worth doing in that particular case.If you want to, your original code was very close:
func +(lhs: Angle, rhs: Angle) -> Angle {
return lhs.dynamicType.init(rawRadians: lhs.rawRadians + rhs.rawRadians)
}
But it's critical to understand that this doesn't match your request to have "the lhs argument to dictate the return type." This causes the lhs argument to dictate the return implementation. The return type is always Angle
. If you want to change the return type, you need to use the generics.