Units and measurement: trouble implementing Dimension subclass in Swift 5

I have a UnitCurrency dimension and Money measurement type in RolePlayingCore, here on GitHub:


https://github.com/mrlegowatch/RolePlayingCore


It is failing to build or test in the latest Xcode 10.2 beta with Swift 5, and I'm getting lost in the documentation and error messages.


To get you up to speed where I am at, I'll create a simplified use case, and walk you from Swift 4.2, to my Swift 5 conundrum.


Let's say we are using Xcode 10.1 and Swift 4.2. Say we have two currencies, gp and cp, where one has a coefficient of 1 and the other 0.01:


public class UnitCurrency : Dimension {
   
    static let gp = UnitCurrency(symbol: "gp", converter: UnitConverterLinear(coefficient: 1.0))
    static let cp = UnitCurrency(symbol: "cp", converter: UnitConverterLinear(coefficient: 0.01))

}


Let's implement a unit test point for confirming which one is the base unit, like this:


        XCTAssertEqual(UnitCurrency.baseUnit(), UnitCurrency.gp, "base unit should be gp")


It will fail, as expected:


XCTAssertEqual failed: throwing "*** You must override baseUnit in your class TestDimension.UnitCurrency to define its base unit." - base unit should be goldPieces


The class is currently missing an implementation for the class func baseUnit(). The documentation for Foundation Dimension on https://developer.apple.com/documentation/foundation/dimensiondescribes a class func baseUnit() requirement, but suggests declaring a public static let in the section on "Creating a Custom NSDimension Subclass", like this:


    public static let baseUnit = gp


That doesn't work, as the test will still fail, so it appears that one must implement the class func after all, like this:


    public override class func baseUnit() -> UnitCurrency {
        return gp
    }


With that, the unit test point will pass.


Let's further define a Money measurement for this unit:


public typealias Money = Measurement<UnitCurrency>


And, a unit test point with some assertions about how to combine money:


        let goldPieces = Money(value: 25, unit: .gp)
        let copperPieces = Money(value: 14, unit: .cp)

        let totalPieces = goldPieces - copperPieces
       
        XCTAssertEqual(totalPieces.value, 24.86, accuracy: 0.0001, "adding coins in gp")
       
        let totalPiecesInCopper = totalPieces.converted(to: .cp)
        XCTAssertEqual(totalPiecesInCopper.value, 2486, accuracy: 0.01, "adding coins in cp")


The build and test succeeds. Next, we close the project, open it in Xcode 10.2 beta, and change the Swift version to 5 (note: using Convert > To Current Swift Syntax does not produce any code changes), and now it complains about the declaration for class func baseUnit():


Cannot override a Self return type with a non-Self return type


If I change the return type to "Self", then instead I get this error message on the next line returning gp:


Cannot convert return expression of type 'UnitCurrency' to return type 'Self'


It has a fix-it suggestion to add "as! Self", which then results in a handful of warnings and errors, including a fix-it suggestion to remove "as! Self":


Error: 'Self' is only available in a protocol or as the result of a method in a class; did you mean 'UnitCurrency'? 
Fix-it: Replace Self with UnitCurrency (which gets me back to the original error)
Error: Cannot convert return expression of type 'UnitCurrency' to return type 'Self' 
Fix-it: Insert as! Self (so 'gp as! Self as! Self')
Warning: Forced cast of 'UnitCurrency' to same type has no effect 
Fix-It: Replace 'as! Self' with '' (which gets me back to the previous error).


Trying to use the static let also gets me back to the original problem in Swift 4.2.


I'm kind of stuck now, seeking advice on what to try next.

Accepted Reply

As far as I tried, this compiles.

public final class UnitCurrency : Dimension { //<- Please do not miss `final`
    
    static let gp = UnitCurrency(symbol: "gp", converter: UnitConverterLinear(coefficient: 1.0))
    static let cp = UnitCurrency(symbol: "cp", converter: UnitConverterLinear(coefficient: 0.01))
    
    public override class func baseUnit() -> UnitCurrency {
        return gp
    }
    
}


ArticleSwift 5 Release Notes for Xcode 10.2 beta 2


In Swift 5 mode, a class method that returns

Self
can no longer be overridden with a method that returns a concrete class type that isn’t
final
. Such code isn’t type safe and needs to be updated. ( SR-695) (47322892)

Replies

There are two separate issues here.


1. The documentation that shows "baseUnit" as a class property is apparently incorrect — it does appear that it's actually a class method. Whether that's intentional or an oversight isn't clear.


2. The error message you started with — "Cannot override a Self return type with a non-Self return type" — seems to depend on the override being public across a module boundary. It may have something to do with resiliency in Swift 5.


I suggest you take both questions to forums.swift.org and ask the people who know or can find out a definitive answer. Make sure they try it with your whole project, because the problem doesn't necessarily show up in a code fragment.

As far as I tried, this compiles.

public final class UnitCurrency : Dimension { //<- Please do not miss `final`
    
    static let gp = UnitCurrency(symbol: "gp", converter: UnitConverterLinear(coefficient: 1.0))
    static let cp = UnitCurrency(symbol: "cp", converter: UnitConverterLinear(coefficient: 0.01))
    
    public override class func baseUnit() -> UnitCurrency {
        return gp
    }
    
}


ArticleSwift 5 Release Notes for Xcode 10.2 beta 2


In Swift 5 mode, a class method that returns

Self
can no longer be overridden with a method that returns a concrete class type that isn’t
final
. Such code isn’t type safe and needs to be updated. ( SR-695) (47322892)