Swiftier? implementation of Measurement and Unit in Foundation


(Joanna Carter) #1

Having had to implement convertible measurements and units for a project a couple of years ago, I thought I would take a look at the upcoming Measurements and Units implementations as found in the Github archive.

Since we are continually being told that protocol-oriented code and the preferred use of structs were good things, I was surprised at the class inheritance I found.

As a long-time proponent of OO design (some 29 years) I know that classes are not "evil" but was interested to see if Swift 2.2 could manage to implement Measurements and Units without falling into the inheritance trap where unnecessary implementation has to be included throughout a class hierarchy.

I replaced the name Dimension with ConvertibleUnit as I felt a Dimension was not truly a Unit; in fact, a Dimension could be better described in terms of composition rather than inheritance i.e. a Dimension "has a" Unit rather than a Dimension "is a" Unit.

Using a protocol for ConvertibleUnit also removes the requirement for methods in a base class that were either empty or that carried out a conversion at a coefficient of 1, which is essentially no conversion at all.

Therefore, if I may indulge, here is my attempt at a more protocol-orientd version. The code is not complete with regards to bridging but, looking at the bridging code in Github, that should not be a problem.

public protocol UnitConverter
{
  func baseUnitValue(fromValue value: Double) -> Double
  
  func value(fromBaseUnitValue baseUnitValue: Double) -> Double
}

// example converters
public struct UnitConverterLinear : UnitConverter
{
  private let coefficient: Double
  
  private let constant: Double
  
  public init(coefficient: Double, constant: Double)
  {
    self.coefficient = coefficient
    
    self.constant = constant
  }
  
  public init(coefficient: Double)
  {
    self.init(coefficient: coefficient, constant: 0)
  }
  
  public func baseUnitValue(fromValue value: Double) -> Double
  {
    return value * coefficient + constant
  }
  
  public func value(fromBaseUnitValue baseUnitValue: Double) -> Double
  {
    return (baseUnitValue - constant) / coefficient
  }
}

public struct UnitConverterReciprocal : UnitConverter
{
  private let reciprocal: Double
  
  public init(reciprocal: Double)
  {
    self.reciprocal = reciprocal
  }
  
  public func baseUnitValue(fromValue value: Double) -> Double
  {
    return reciprocal / value
  }
  
  public func value(fromBaseUnitValue baseUnitValue: Double) -> Double
  {
    return baseUnitValue * reciprocal
  }
}

// base protocols
public protocol Unit
{
  var symbol: String { get }
}

public protocol ConvertibleUnit : Unit
{
  var converter: UnitConverter { get }
  
  static var baseUnit : Self { get }
}

// generic Measurement
public struct Measurement<UnitType : ConvertibleUnit>
{
  var value: Double
  
  let unit: UnitType
  
  public init(value: Double, unit: UnitType)
  {
    self.value = value
    
    self.unit = unit
  }
  
  public func canBeConverted<TargetUnit : ConvertibleUnit>(to unit: TargetUnit) -> Bool
  {
    return unit is UnitType
  }
  
  public func converting<TargetUnit : ConvertibleUnit>(to unit: TargetUnit) -> Measurement<TargetUnit>
  {
    if !canBeConverted(to: unit)
    {
      fatalError("Unit type not compatible")
    }
    
    let baseUnitValue = self.unit.converter.baseUnitValue(fromValue: value)
    
    let convertedValue = unit.converter.value(fromBaseUnitValue: baseUnitValue)
    
    return Measurement<TargetUnit>(value: convertedValue, unit: unit)
  }
}

// example implementing ConvertibleUnit

public struct LengthUnit : ConvertibleUnit
{
  public let symbol: String
  
  public let converter: UnitConverter
  
  private struct Symbol
  {
    static let kilometers = "km"
    static let meters = "m"
    // ...
  }
  
  private struct Coefficient
  {
    static let kilometers = 1000.0
    static let meters = 1.0
    // ...
  }
  
  public static var kilometers: LengthUnit
  {
    return LengthUnit(symbol: Symbol.kilometers, converter: UnitConverterLinear(coefficient: Coefficient.kilometers))
  }
  
  public static var meters: LengthUnit
  {
    return LengthUnit(symbol: Symbol.meters, converter: UnitConverterLinear(coefficient: Coefficient.meters))
  }
  
  // ...

  public static var baseUnit : LengthUnit
  {
    return LengthUnit.meters
  }
}

···

--
Joanna Carter
Carter Consulting


(Tony Parker) #2

Hi Joanna,

An important requirement of the design of measurements and units was that it had to work in Objective-C as well, where protocols do not have as many capabilities as they do in Swift. The bridging into Swift can only do so much, and frankly it didn’t seem to be the case that traditional inheritance was really much of a problem for this API in the first place.

The approach of separating out the coefficient was one that the HealthKit unit API took, but we did not. Some units do lend themselves well to metric prefixes, but others do not (angles, for example).

We did make some changes upon import to Swift. Most importantly, Measurement is indeed a struct in Swift while a class in Objective-C.

- Tony

···

On Aug 11, 2016, at 2:49 AM, Joanna Carter via swift-corelibs-dev <swift-corelibs-dev@swift.org> wrote:

Having had to implement convertible measurements and units for a project a couple of years ago, I thought I would take a look at the upcoming Measurements and Units implementations as found in the Github archive.

Since we are continually being told that protocol-oriented code and the preferred use of structs were good things, I was surprised at the class inheritance I found.

As a long-time proponent of OO design (some 29 years) I know that classes are not "evil" but was interested to see if Swift 2.2 could manage to implement Measurements and Units without falling into the inheritance trap where unnecessary implementation has to be included throughout a class hierarchy.

I replaced the name Dimension with ConvertibleUnit as I felt a Dimension was not truly a Unit; in fact, a Dimension could be better described in terms of composition rather than inheritance i.e. a Dimension "has a" Unit rather than a Dimension "is a" Unit.

Using a protocol for ConvertibleUnit also removes the requirement for methods in a base class that were either empty or that carried out a conversion at a coefficient of 1, which is essentially no conversion at all.

Therefore, if I may indulge, here is my attempt at a more protocol-orientd version. The code is not complete with regards to bridging but, looking at the bridging code in Github, that should not be a problem.

public protocol UnitConverter
{
func baseUnitValue(fromValue value: Double) -> Double

func value(fromBaseUnitValue baseUnitValue: Double) -> Double
}

// example converters
public struct UnitConverterLinear : UnitConverter
{
private let coefficient: Double

private let constant: Double

public init(coefficient: Double, constant: Double)
{
   self.coefficient = coefficient

   self.constant = constant
}

public init(coefficient: Double)
{
   self.init(coefficient: coefficient, constant: 0)
}

public func baseUnitValue(fromValue value: Double) -> Double
{
   return value * coefficient + constant
}

public func value(fromBaseUnitValue baseUnitValue: Double) -> Double
{
   return (baseUnitValue - constant) / coefficient
}
}

public struct UnitConverterReciprocal : UnitConverter
{
private let reciprocal: Double

public init(reciprocal: Double)
{
   self.reciprocal = reciprocal
}

public func baseUnitValue(fromValue value: Double) -> Double
{
   return reciprocal / value
}

public func value(fromBaseUnitValue baseUnitValue: Double) -> Double
{
   return baseUnitValue * reciprocal
}
}

// base protocols
public protocol Unit
{
var symbol: String { get }
}

public protocol ConvertibleUnit : Unit
{
var converter: UnitConverter { get }

static var baseUnit : Self { get }
}

// generic Measurement
public struct Measurement<UnitType : ConvertibleUnit>
{
var value: Double

let unit: UnitType

public init(value: Double, unit: UnitType)
{
   self.value = value

   self.unit = unit
}

public func canBeConverted<TargetUnit : ConvertibleUnit>(to unit: TargetUnit) -> Bool
{
   return unit is UnitType
}

public func converting<TargetUnit : ConvertibleUnit>(to unit: TargetUnit) -> Measurement<TargetUnit>
{
   if !canBeConverted(to: unit)
   {
     fatalError("Unit type not compatible")
   }

   let baseUnitValue = self.unit.converter.baseUnitValue(fromValue: value)

   let convertedValue = unit.converter.value(fromBaseUnitValue: baseUnitValue)

   return Measurement<TargetUnit>(value: convertedValue, unit: unit)
}
}

// example implementing ConvertibleUnit

public struct LengthUnit : ConvertibleUnit
{
public let symbol: String

public let converter: UnitConverter

private struct Symbol
{
   static let kilometers = "km"
   static let meters = "m"
   // ...
}

private struct Coefficient
{
   static let kilometers = 1000.0
   static let meters = 1.0
   // ...
}

public static var kilometers: LengthUnit
{
   return LengthUnit(symbol: Symbol.kilometers, converter: UnitConverterLinear(coefficient: Coefficient.kilometers))
}

public static var meters: LengthUnit
{
   return LengthUnit(symbol: Symbol.meters, converter: UnitConverterLinear(coefficient: Coefficient.meters))
}

// ...

public static var baseUnit : LengthUnit
{
   return LengthUnit.meters
}
}

--
Joanna Carter
Carter Consulting

_______________________________________________
swift-corelibs-dev mailing list
swift-corelibs-dev@swift.org
https://lists.swift.org/mailman/listinfo/swift-corelibs-dev