1

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 a SCNFloat 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 to DistanceUnit 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 converted Int because such conversions would be lossless, but something expecting an Int cannot necessarily take a Float 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 of implicit. 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 use implicit 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?

Community
  • 1
  • 1
Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • I am fairly sure that the answer is “no”. Very early Swift versions had the ability for custom conversion methods (see https://stackoverflow.com/a/24646202/1187415) but that was removed even before Swift 1.0. – Martin R Jun 17 '20 at 07:33
  • Shame! I get why they want things to be explicit, but if you are the one guaranteeing that explicitness, then IMHO, it really should be allowed. After all, one of the mantras of Swift is don't write more than is actually needed. – Mark A. Donohoe Jun 18 '20 at 00:07

0 Answers0