I'm trying to find out if Swift, like C# and other languages, can support implicit conversions between types when those types are known to be fully compatible and lossless. ExpressibleByXxxLiteral
protocols seem close, but I'm not thinking they are actually what I'm after. More on that at the end.
For now however, consider the below DistanceUnit
enumeration. This was created by me to let users of my API for ARKit
--which is based on meters--to specify values in whatever units they choose. If they want something to be moved over by two inches in their augmented scene, they don't have to do the math manually. They just use moveBy(x: .inches(2))
and the enumeration automatically converts it via its exposed meters
property and hands that off to Apple's APIs which expect them.
In short, think of this as a user-friendly, glorified typealias for meters in SceneKit.
enum DistanceUnit {
case meters(SCNFloat)
case centimeters(SCNFloat)
case millimeters(SCNFloat)
case feet(SCNFloat)
case inches(SCNFloat)
case points(SCNFloat)
var meters:SCNFloat {
switch self {
case let .meters(meters) : return meters
case let .centimeters(cm) : return cm * 0.01
case let .millimeters(mm) : return mm * 0.001
case let .feet(feet) : return feet * 0.3048
case let .inches(inches) : return inches * 0.0254
case let .points(points) : return (points / 72) * Self.inch.meters
}
}
// Presets
static let meter : DistanceUnit = .meters(1)
static let centimeter : DistanceUnit = .centimeters(1)
static let millimeter : DistanceUnit = .millimeters(1)
static let foot : DistanceUnit = .feet(1)
static let inch : DistanceUnit = .inches(1)
static let point : DistanceUnit = .points(1)
}
Note: Since SceneKit's types like
SCNVector3
are based on different types based on the platform, I created aSCNFloat
typealias which my API standardizes on so it just works regardless of platform without me having to put those compiler checks throughout my code since it happens once at the typealias definition.
#if os(iOS)
typealias SCNFloat = Float
#else
typealias SCNFloat = CGFloat
#endif
Additionally, since again, this type ultimately represents meters, to make things simpler for the user, especially when adding things like default values to their own functions, I also implement ExpressibleByIntegerLiteral
and ExpressibleByFloatLiteral
, like so...
extension DistanceUnit : ExpressibleByIntegerLiteral {
init(integerLiteral value: Int) {
self = .meters(SCNFloat(value))
}
}
extension DistanceUnit : ExpressibleByFloatLiteral {
init(floatLiteral value: Float) {
self = .meters(SCNFloat(value))
}
}
Back to the problem...
I hate that internally in my API I always have to do value.meters
when passing a DistanceUnit
around. I already know this enum is a glorified typealias for meters as expressed via a SCNFloat
, so why should I always have to type it explicitly?
What I'm trying to find out is if it's possible to somehow decorate this type so it can be passed directly to any API that expects a SCNFloat
like you can in languages like C#.
The C# Way
Now in C#, this is easy with implicit conversion operators. Here's an example of implicit conversion between SCNFloat
and DistanceUnit
(technically, I'm mixing in a little Swift pseudocode here to illustrate the point as C# doesn't have enums with associated values, but you get the point)...
public static implicit operator DistanceUnit(SCNFloat value) => .meters(value);
public static implicit operator SCNFloat(DistanceUnit units) => units.meters;
The first line above says 'Any API that takes a DistanceUnit
can now accept a SCNFloat
directly. When passing a SCNFloat
, the compiler will automatically insert that value into the .meters(x)
enumeration case, then that enumeration value is ultimately passed along to the recipient expecting the DistanceUnit
.'
Conversely, the second line says 'Any API that takes a SCNFloat
can now take a DistanceUnit
directly. When passing a DistanceUnit
, the compiler will automatically extract the .meters
property, which is already a type SCNFloat
and will simply pass that value along to the recipient expecting a SCNFloat
.'
Simple, predictable and fully lossless.
Note: Above I've defined conversions both ways--from
SCNFloat
toDistanceUnit
and vice-versa. However, you don't have to specify both if it doesn't make sense to.For instance, something that expects a
Float
can easily take a convertedInt
because such conversions would be lossless, but something expecting anInt
cannot necessarily take aFloat
without possibly losing some data.In those cases, you can either define it as one-way, or you can still define them as two-way, but you would mark the lossy direction with
explicit
instead ofimplicit
. This makes the user have to explicitly cast one type to the other, warning them it's potentially a lossy operation, but leaving it up to them to make that call. You never useimplicit
if data cannot be guaranteed to be 100% lossless.
Back in Swiftville...
The ExpressibleByXxxLiteral
protocols at first seem like they're half the battle, but in actuality, I think it's a red herring because it's not actually doing the conversion at runtime. The hint is in the name... 'literal', meaning something that's literally typed in code, and not with variables of Xxx
. Playgrounds seem to support this theory as well. That leads me to believe that while convenient for a few simple typed literals, that isn't actually a solution to what I'm after here.
So does Swift have any such feature/capability when you know a type is fully compatible with another type, whether uni- or bi-directional as it is here?