swiftgenericsopaque-types

How do I return an unspecified Measurement<T> from a function?


UPDATE: Okay, I'm confused. My original code did not compile. At some point in cutting it down to a minimal example, it apparently started working. I have rewritten the original version enough now that I've lost track of why it didn't work in the first place. Going to just mark this question resolved.


I am trying to figure out how to write a function that returns an arbitrary Measurement, without having to specify in advance what UnitType the Measurement has. The following is more or less what I want to do, but it does not compile. (The arguments to the function don't matter.)

   func getValue(for x:Int) -> Measurement<Unit>? {
        if x==1 {
            return Measurement(value:5, unit:UnitSpeed.metersPerSecond)
        } else {
            return Measurement(value:2, unit:UnitLength.meters)
        }
    }

I don't want to have to care what the Unit is! Measurements encapsulate their unit, and all I want to do is plug it into something like: Text(reading?.unit.symbol ?? "") at the other end.

func getValue(for measure:TrackedMeasure) -> some Measurement? {} complains "Reference to generic type 'Measurement' requires arguments in <…>"

The suggested fix is func getValue(for measure:TrackedMeasure) -> some Measurement<Unit>? { }, which then complains "An 'opaque' type must specify only 'Any', 'AnyObject', protocols, and/or a base class"

There does not seem to be a protocol for Measurement, and I can't figure out how to define one.

Do I have to, like, reimplement Measurement to make this work?


Solution

  • Measurement<UnitType> is a generic type and it does not extend a common base class or conform to a common protocol, so a concrete type Measurement<UnitLength> is a different type than Measurement<UnitSpeed>.

    The only way around it is to do something like a type erasure, and have them both conform to a common protocol AnyMeasurement:

    protocol AnyMeasurement {
      var baseUnit: Unit { get }
      var value: Double { get }
    }
    

    Then you can conform Measurement to AnyMeasurement:

    extension Measurement: AnyMeasurement {
      // just need to return a base Unit, instead of a concrete UnitType
      // value: Double { get } is already implemented by Measurement
      var baseUnit: { self.unit }
    }
    

    Then, you could do something like this:

    func randomMeasurement() -> AnyMeasurement {
       let speed  = Measurement(value: 10, unit: UnitSpeed.metersPerSecond)
       let length = Measurement(value: 10, unit: UnitLength.meters)
    
       return Bool.random() ? speed : length
    }
    
    print(randomMeasurement().baseUnit.symbol)