Newtype for Swift

Well, since you asked, here's an approach I'm experimenting with based on existing features. It's loosely based on Point-Free's Tagged type, but with my approach:

  • I can choose exactly which protocols each new type conforms to.

  • If the wrapped type conforms to a protocol, I can choose to forward the new type's conformance to the raw type's conformance. This requires boilerplate, but I only have to write the boilerplate once for the protocol and then I can use it for every conforming newtype.

  • If the wrapped type doesn't conform, or if I don't want to use its conformance, I can customize how the new type conforms.

In this system, every “newtype” is defined by a type conforming to this BaseTag protocol:

public protocol BaseTag {
    associatedtype RawValue
}

For convenience, we can create tags by subclassing this base class:

open class TagBase<RawValue>: BaseTag {
    private init() { fatalError() }
}

This class, and its subclasses, should only exist at the type level. There should never be values of a tag type. So we make its init private.

The actual “newtype” is then a typealias for an instantiation of NewType with a tag:

public struct NewType<Tag: BaseTag> {
    public typealias RawValue = Tag.RawValue

    public init(rawValue: Tag.RawValue) {
        self.rawValue = rawValue
    }

    public var rawValue: Tag.RawValue
}

So, for example, we could create a newtype for string order ids like this:

final class OrderIdTag: TagBase<String> { }
typealias OrderId = NewType<OrderIdTag>

This is not as lightweight as a dedicated syntax like newtype OrderId = String, but I don’t think it’s too bad.

However, we want some protocol conformances for the OrderId type. We want it to be Equatable, Hashable, Comparable, and Codable using the raw strings. And we want it to be CustomStringConvertible, but we want to customize the description property.

For Equatable, we create an EquatableTag protocol that inherits BaseTag and mirrors the requirements of Equatable. A NewType conforms to Equatable if its Tag conforms to EquatableTag:

public protocol EquatableTag: BaseTag {
    static func areEqual(_ lhs: RawValue, _ rhs: RawValue) -> Bool
}

extension NewType: Equatable where Tag: EquatableTag {
    public static func == (lhs: Self, rhs: Self) -> Bool {
        return Tag.areEqual(lhs.rawValue, rhs.rawValue)
    }
}

We also write this boilerplate for forwarding to the raw value’s conformance, if the raw value conforms:

extension EquatableTag where RawValue: Equatable {
    public static func areEqual(_ lhs: RawValue, _ rhs: RawValue) -> Bool {
        return lhs == rhs
    }
}

We have similar code for Hashable, Comparable, Codable, and CustomStringConvertible:

public protocol HashableTag: EquatableTag {
    static func hash(_ rawValue: RawValue, into hasher: inout Hasher)
}

extension HashableTag where RawValue: Hashable {
    public static func hash(_ rawValue: RawValue, into hasher: inout Hasher) {
        rawValue.hash(into: &hasher)
    }
}

extension NewType: Hashable where Tag: HashableTag {
    public func hash(into hasher: inout Hasher) {
        Tag.hash(rawValue, into: &hasher)
    }
}

public protocol ComparableTag: EquatableTag {
    static func `is`(_ lhs: RawValue, lessThan rhs: RawValue) -> Bool
}

extension ComparableTag where RawValue: Comparable {
    public static func `is`(_ lhs: RawValue, lessThan rhs: RawValue) -> Bool {
        return lhs < rhs
    }
}

extension NewType: Comparable where Tag: ComparableTag {
    public static func < (lhs: Self, rhs: Self) -> Bool {
        return Tag.is(lhs.rawValue, lessThan: rhs.rawValue)
    }
}

public protocol EncodableTag: BaseTag {
    static func encode(_ rawValue: RawValue, into encoder: Encoder) throws
}

extension EncodableTag where RawValue: Encodable {
    public static func encode(_ rawValue: RawValue, into encoder: Encoder) throws {
        try rawValue.encode(to: encoder)
    }
}

extension NewType: Encodable where Tag: EncodableTag {
    public func encode(to encoder: Encoder) throws {
        try Tag.encode(rawValue, into: encoder)
    }
}

public protocol DecodableTag: BaseTag {
    static func decode(from decoder: Decoder) throws -> RawValue
}

extension DecodableTag where RawValue: Decodable {
    public static func decode(from decoder: Decoder) throws -> RawValue {
        return try RawValue(from: decoder)
    }
}

extension NewType: Decodable where Tag: DecodableTag {
    public init(from decoder: Decoder) throws {
        self.init(rawValue: try Tag.decode(from: decoder))
    }
}

public protocol CodableTag: EncodableTag, DecodableTag { }

public protocol CustomStringConvertibleTag: BaseTag {
    static func description(of rawValue: RawValue) -> String
}

extension CustomStringConvertibleTag where RawValue: CustomStringConvertible {
    public static func description(of rawValue: RawValue) -> String {
        return rawValue.description
    }
}

extension NewType: CustomStringConvertible where Tag: CustomStringConvertibleTag {
    public var description: String { Tag.description(of: rawValue) }
}

It seems like a lot, but we only had to write all that stuff once. Now we can use those tags for any number of NewType instantiations.

Let's add those tag protocols to OrderIdTag to give OrderId the conformances we want. We can add a static method to OrderIdTag to customize how OrderId conforms to CustomStringConvertible:

final class OrderIdTag: TagBase<String>, HashableTag, ComparableTag, CodableTag, CustomStringConvertibleTag {
    static func description(of rawValue: String) -> String {
        return "#" + rawValue
    }
}
typealias OrderId = NewType<OrderIdTag>

Again, this isn’t as lightweight as a dedicated syntax could be. But I think it’s not too bad.

In my real project, I want most of my types to be Hashable, Comparable, and Codable, so I have a convenience protocol to shorten my tag declarations:

protocol CommonTags: HashableTag, ComparableTag, CodableTag { }

final class OrderIdTag: TagBase<String>, CommonTags, CustomStringConvertibleTag {
    static func description(of rawValue: String) -> String {
        return "#" + rawValue
    }
}
typealias OrderId = NewType<OrderIdTag>

Now here’s a more complex example, where there’s a relationship between two NewType instances.

In my project, we need to deal with timestamps that have nanosecond precision and can uniformly represent timestamps across multiple years. A Double requires 30 bits to the right of the decimal point for nanosecond precision, so any Double-based representation (like Foundation’s Date) has only 24 bits (including the sign bit) for the integer part of the timestamp, giving a lifetime of about 194 days. That isn't sufficient, so we represent a timestamp as the number of nanoseconds since the Unix epoch.

I want to represent these timestamps with newtype, NanoTime, that wraps Int64. I don’t want NanoTime to conform to AdditiveArithmetic (what would “Monday at noon + Tuesday at 8am” mean?) but I do want it to be Strideable. I want a NanoDuration newtype for the Stride of NanoTime. NanoDuration should also wrap Int64.

Any Stride must conform to Comparable and SignedNumeric. I have already covered Comparable with ComparableTag. SignedNumeric inherits three protocols I haven’t already covered: Numeric, AdditiveArithmetic, and ExpressibleByIntegerLiteral. I don’t really want NanoDuration to have all the features of those protocols, but I’m going to allow it for now. (Maybe I’ll change my mind later and give up on conforming NanoTime to Strideable.)

So anyway, let’s start with ExpressibleByIntegerLiteral. It has a new complication: the associated type IntegerLiteralType. So we define the tag protocol and the NewType conformance like this:

public protocol ExpressibleByIntegerLiteralTag: BaseTag {
    associatedtype IntegerLiteralType: _ExpressibleByBuiltinIntegerLiteral
    static func integerLiteral(_ literal: IntegerLiteralType) -> RawValue
}

extension NewType: ExpressibleByIntegerLiteral where Tag: ExpressibleByIntegerLiteralTag {
    public typealias IntegerLiteralType = Tag.IntegerLiteralType

    public init(integerLiteral: IntegerLiteralType) {
        self.init(rawValue: Tag.integerLiteral(integerLiteral))
    }
}

For protocol forwarding when RawValue conforms to ExpressibleByIntegerLiteral, we have this extension:

extension ExpressibleByIntegerLiteralTag where RawValue: ExpressibleByIntegerLiteral, RawValue.IntegerLiteralType == IntegerLiteralType {
    public static func integerLiteral(_ literal: IntegerLiteralType) -> RawValue {
        RawValue(integerLiteral: literal)
    }
}

I'd really like a way to set a default typealias IntegerLiteralType = RawValue.IntegerLiteralType, but I couldn’t find a way to make that work (as of Xcode 11.5b2).

The code for AdditiveArithmetic, Numeric, SignedNumeric, and Strideable is similar, although they have more requirements:

public protocol AdditiveArithmeticTag: EquatableTag {
    static var zero: RawValue { get }
    static func addition(_ lhs: RawValue, _ rhs: RawValue) -> RawValue
    static func subtraction(_ lhs: RawValue, _ rhs: RawValue) -> RawValue
}

extension AdditiveArithmeticTag where RawValue: AdditiveArithmetic {
    public static var zero: RawValue { RawValue.zero }
    public static func addition(_ lhs: RawValue, _ rhs: RawValue) -> RawValue { lhs + rhs }
    public static func subtraction(_ lhs: RawValue, _ rhs: RawValue) -> RawValue { lhs - rhs }
}

extension NewType: AdditiveArithmetic where Tag: AdditiveArithmeticTag {
    public static var zero: NewType<Tag> { .init(rawValue: Tag.zero) }

    public static func + (lhs: Self, rhs: Self) -> Self {
        return .init(rawValue: Tag.addition(lhs.rawValue, rhs.rawValue))
    }

    public static func - (lhs: Self, rhs: Self) -> Self {
        return .init(rawValue: Tag.subtraction(lhs.rawValue, rhs.rawValue))
    }
}

public protocol NumericTag: AdditiveArithmeticTag, ExpressibleByIntegerLiteralTag {
    associatedtype Magnitude: Comparable, Numeric
    static func exactly<T: BinaryInteger>(_ source: T) -> RawValue?
    static func magnitude(of rawValue: RawValue) -> Magnitude
    static func multiplication(_ lhs: RawValue, _ rhs: RawValue) -> RawValue
    static func inPlaceMultiplication(_ lhs: inout RawValue, _ rhs: RawValue)
}

extension NumericTag where RawValue: Numeric, RawValue.Magnitude == Magnitude {
    public static func exactly<T: BinaryInteger>(_ source: T) -> RawValue? { RawValue(exactly: source) }
    public static func magnitude(of rawValue: RawValue) -> Magnitude { rawValue.magnitude }
    public static func multiplication(_ lhs: RawValue, _ rhs: RawValue) -> RawValue { lhs * rhs }
    public static func inPlaceMultiplication(_ lhs: inout RawValue, _ rhs: RawValue) { lhs *= rhs }
}

extension NewType: Numeric where Tag: NumericTag {
    public typealias Magnitude = Tag.Magnitude

    public init?<T>(exactly source: T) where T : BinaryInteger {
        guard let rawValue = Tag.exactly(source) else { return nil }
        self.init(rawValue: rawValue)
    }

    public var magnitude: Tag.Magnitude { Tag.magnitude(of: rawValue) }

    public static func * (lhs: NewType<Tag>, rhs: NewType<Tag>) -> NewType<Tag> {
        return .init(rawValue: Tag.multiplication(lhs.rawValue, rhs.rawValue))
    }

    public static func *= (lhs: inout NewType<Tag>, rhs: NewType<Tag>) {
        Tag.inPlaceMultiplication(&lhs.rawValue, rhs.rawValue)
    }
}

public protocol SignedNumericTag: NumericTag {
    static func negate(_ rawValue: inout RawValue)
    static func negative(of rawValue: RawValue) -> RawValue
}

extension SignedNumericTag {
    public static func negate(_ rawValue: inout RawValue) {
        rawValue = Self.subtraction(Self.zero, rawValue)
    }

    public static func negative(of rawValue: RawValue) -> RawValue {
        return Self.subtraction(Self.zero, rawValue)
    }
}
extension SignedNumericTag where RawValue: SignedNumeric {
    public static func negate(_ rawValue: inout RawValue) { rawValue.negate() }
    public static func negative(of rawValue: RawValue) -> RawValue { -rawValue }
}

extension NewType: SignedNumeric where Tag: SignedNumericTag {
    public mutating func negate() { Tag.negate(&rawValue) }
    public static prefix func - (_ value: Self) -> Self {
        return .init(rawValue: Tag.negative(of: value.rawValue))
    }
}

public protocol StrideableTag: ComparableTag {
    associatedtype Stride: Comparable, SignedNumeric

    static func advance(_ rawValue: RawValue, by n: Stride) -> RawValue
    static func distance(from start: RawValue, to end: RawValue) -> Stride
}

extension StrideableTag where RawValue: Strideable, RawValue.Stride == Stride {
    public static func advance(_ rawValue: RawValue, by n: Stride) -> RawValue {
        return rawValue.advanced(by: n)
    }

    public static func distance(from start: RawValue, to end: RawValue) -> Stride {
        return distance(from: start, to: end)
    }
}

extension NewType: Strideable where Tag: StrideableTag {
    public typealias Stride = Tag.Stride

    public func advanced(by n: Tag.Stride) -> NewType<Tag> {
        return .init(rawValue: Tag.advance(rawValue, by: n))
    }

    public func distance(to other: NewType<Tag>) -> Tag.Stride {
        return Tag.distance(from: rawValue, to: other.rawValue)
    }
}

Now we can define the NanoDuration type:

// Since this will be a Stride, it must be at least Comparable and SignedNumeric.
final class NanoDurationTag: TagBase<Int64>, ComparableTag, SignedNumericTag, HashableTag, CodableTag, CustomStringConvertibleTag {
    public typealias IntegerLiteralType = RawValue.IntegerLiteralType
    public typealias Magnitude = RawValue.Magnitude

    public static func description(of rawValue: Int64) -> String {
        rawValue.description + "ns"
    }
}
typealias NanoDuration = NewType<NanoDurationTag>

And then we can use it as the Stride for the NanoTime type. Note that we have to define methods in NanoTimeTag to customize the way NanoTime conforms to Strideable, else its Stride will be Int64.Stride, which is Int.

final class NanoTimeTag: TagBase<Int64>, StrideableTag, HashableTag, CodableTag, CustomStringConvertibleTag {
    public typealias Stride = NanoDuration
    public static func advance(_ rawValue: Int64, by n: Stride) -> Int64 { rawValue + n.rawValue }
    public static func distance(from start: Int64, to end: Int64) -> Stride { .init(rawValue: end - start) }

    public static func description(of rawValue: Int64) -> String {
        rawValue.description + "nse"
    }
}
typealias NanoTime = NewType<NanoTimeTag>

That covers all of the protocol conformances I support in my project so far.

4 Likes