Newtype for Swift

What’s the difference between a newtype with complete duplication and a typealias?

newtypes are new types (for the purposes of type-checking), unlike typealiases:

newtype Meters = Int allows you to create functions taking and returning Meters without accepting Int, while keeping Meters zero-cost.

2 Likes

Is that really worth it, though?

Let’s take your example of newtype-ing Int. You couldn’t perform any extra validation or whatever, because of ExpressibleByIntegerLiteral and the various mathematics methods and operators. Even if the base type didn’t have that conformance, I could create an equally validation-breaking conformance (perhaps by mistake, or imported from a library) and suddenly see all kinds of methods and initialisers you didn’t expect me to use.

So complete duplication ends up being pretty useless in practice, IMO. If you’re making a new type, you essentially always want to limit the functionality it exposes - even if that’s a long list.

I don't really have a horse in this race, but I can share my thoughts.

I mostly agree. I believe validation is possible, and that validation and limiting functionality are both common use cases.

Opaque type aliases are the equivalent feature in Scala 3. The opaque type Logarithm = Double example shows both validation and limiting functionality:

  • Validation: make "underlying type to newtype" conversion be failable. Double -> Logarithm? is the only public conversion API.
  • Limiting functionality: in Scala 3, I think newtypes inherit no methods or operators. Logarithm's public APIs are all defined via extensions.
1 Like

I think we should do a better job answering to Slava and Karl's comments. They're quite right suggesting this proposal adds complexity, and whether the benefits are worth it we can only weigh by looking at concrete and conplete examples of potentiall uses of the proposed newtype.

Take this example for a metaexample:

Quantities with units in particular are a great example of types which ought not to automatically conform to all protocols of their wrapped type. For length, it might be better to have typealias Meters = Double, but the problem here is that we probably want Meters(10) * Meters(10) to equal to SquareMeters(100) and Meters(10) / Meters(2) to the unitless value 5.

This gets us back to the unsaid motivation I had for my requirement 3 of no autoconformances by default. What newtype is probably most used for in Haskell is to give type-checked semantics to function arguments. If a function needs to do more than forward the values of another such function and no such conformances exist on the newtype, then it can unwrap the value (let meters: Int = length.value) and do the arithmetics on the standard type instead. This of course is less of a problem with Swift because at least we can convey this information in argument labels all the way between function implementation and the call site (still contrived example):

func areaSqMeters(widthMeters: Int, heightMeters: Int) -> Int {
    widthMeters * heightMeters
}
let areaM2 = areaSqMeters(widthMeters: widthMeters, heightMeters: heightMeters)

but of course this becomes not only a lot more pleasant with types but also the information is moved to the type checker:

func rectangleArea(width: Meters, height: Meters) -> SquareMeters {
    // Whether Meters conforms to a protocol mostly matters here,
    // and I'd say if the wrapped type can be unwrapped by this code,
    // then it's a better default to not autoinherit conformances.
}
let area = rectangleArea(width: width, height: height)

Still, this is a toy example; we'd probably get our units from a units library and newtype would insted be used for one-off cases like newtype AgeInCatYears = Int where an extra dependency isn't wanted or the unit alone isn't precise enough.

So, I welcome discussion on concrete usage examples and benefits of newtype. Without that, I don't think it's a feature important enough to include.

3 Likes

My concrete example using a concept similar to newtype is for adding type safe identifiers for model entities.
For convenience I use the Tagged framework by pointfree. https://github.com/pointfreeco/swift-tagged

This framework forwards Hashable, Equatable, Codable and adds RawRepresentable as well as literal conversion.

This is the only real life use case that I have ever wanted myself - and the Tagged library fulfills this need just fine.

My only issue (which is a bit off-topic for this thread) is that there is currently no way to use these types as keys in Dictionaries and have them encode to Dictionaries - they will be encoded as arrays of key value pairs. If newtype was a non-backwards compatible feature of the language perhaps it would be possible to remedy this behavior for newtypes of Strings and Ints in the same go?

I’m not very familiar with Scala, but this sounds like a wrapper struct in Swift.

That’s another, big problem - let’s say I have a Meters newtype, but now I need to pass that in to a maths library for some calculations. Now we need to add a way to undo the newtype.

That presents a bunch more issues - is it an instance member? If so, what is it called and how do we avoid conflicts? What access control does it have? Or is it a new expression, like #unwrap(meters) or something?

All of this makes me think it’s not worth it.

Selective, protocol-based forwarding seems like the way to go IMO. It defines a clear set of functionality you want to “inherit” and provides clear answers to all of the other problems, in a way that is consistent with the rest of the language. The only downside is that protocols cannot have non-public requirements right now, but that’s a long overdue feature anyway.

1 Like

Unlike single-field wrapper structs (which representationally are an extra layer of indirection), Scala's opaque type aliases and Haskell's newtype are representationally identical to the wrapped type - it's a new type just for type-checking purposes.

The former may be (guaranteed to be?) optimized away by Swift, but the latter is a representational guarantee.


Some reading that helped my understanding:

  • Difference between data and newtype in Haskell
    • This is the difference between a wrapper struct (data) and an opaque type alias (newtype).
    • There are some Haskell-specific details about strict vs lazy evaluation, which aren't relevant in the context of Swift.
  • Roles in GHC
    • This blog post explains "nominal equality" (newtype is different from underlying type) versus "representational equality" (newtype is same as underlying type). It also explains how "protocol conformance correctness" is impacted by these definitions of type equality.
3 Likes

This is why I've been saying the conversions need to be allowed two ways: both wrapping (init(_:) or whatever it'd be called) and unwrapping (.value) should be public.

1 Like

This I (also) agree with.

Making unconditional "newtype <-> underlying type" conversions be public may be not a good idea. For validation purposes, we may want to make public APIs for "underlying type to newtype" conversion to be failable.

// Example adapted from: https://dotty.epfl.ch/docs/reference/other-new-features/opaques.html
newtype Logarithm = Double

extension Double {
  // We want the public conversion API to be failable.
  public var logarithm: Logarithm? { self > 0 : log(self) ? nil }
}

Opaque type aliases (the equivalent newtype feature in Scala) is very nuanced about access control. As others have stated, newtypes often want to provide intentionally designed APIs rather than forwarding all functionality, so limiting access is key.

1 Like

Oh, you're right. In fact, even in Haskell you could do what's essentially init(_:) throws as in Haskell at Work.

So the would-be newtype construct would need more versatility to support that too, with reasonable defaults.

The more we discuss this the less benefits the one-liner seems to have, compared to a simple struct.

The layout of a Swift struct is a straightforward aggregate (modulo any field ordering concerns, which don’t apply for single member structures). Thus, the layout of a struct wrapping a single T is the same as the layout of a T. This is a guarantee in Swift.

(Digression: strictly speaking resilient libraries are entitled to hide this fact from you, but in that case add the word @frozen to the above comment and it becomes true again.)

This is an observable part of Swift’s ABI and is therefore a guarantee, short of an ABI breakage.

6 Likes

Protocol Forwarding

I firmly believe that tackling generic limitations, such as the ones to be discussed below is better than solving protocol forwarding, which is a very specific problem.

I propose this:

typealias ID = String as Hashable & Comparable
// is equivalent to:
typealias AlsoID = Abstract<String, Hashable & Comparable>

var foo: ID = ""
// provides initialisers for `ExpressibleBy...Literal` types 
// (such as String: ExpressibleByStringLiteral)

var bar: CustomType as Hashable & Comparable = .init(CustomType())
// when using custom type the initialiser:
// `.init(_:)` of Abstract will be used

For Abstract to be implemented new language features need to be in place (the following are some features I used my implementation):

  1. Protocol Generic Requirements
    struct Foo<Root, Protocol: AnyProtocol> where Root: Protocol { ... }
    
  2. Key Paths to PATs (Protocols with Associated Types)
    protocol Pat {
        associatedtype A
    }
    
    let foo: Pat = ...  // ERROR!
    
    So this isn't allowed:
    PartialKeyPath<Pat> // ERROR!
    
    Of course, this feature would require changes not only to KeyPaths, but also to Generics.

A possible Abstract implementation:

@dynamicMemberLookup
struct Abstract<Root, Protocol: AnyProtocol> where Root: Protocol {
    private let root: Root
    init(_ value: Root) { ... }
    
    subscript<Value>(dynamicMember member: KeyPath<Protocol, Value>) -> Value {
        ...
    }
}

New Type Creation

I like this feature. There have been many times when I wanted to do something like this:

func startTimer(for time: Time) { ... }

startTimer(for: .short)
startTimer(for 0.5)

Still, though, I think that the need for such a feature is rare. Additionally, wrapper types can use the following protocol:

protocol Wrapper {
    associatedtype WrappedProperty
    static var wrappedKeyPath: KeyPath<Self, WrappedProperty> { get }
}

extension Wrapper: ExpressibleBy...Literal  
    where WrappedProperty: ExpressibleBy...Literal { ... }
// Provides initialisers for literal types.
// This may be rendered useless with Static Dynamic Member Lookup.

which would allow for this:

@dynamicMemberLookup
struct Time: Wrapper {
    static var wrappedKeyPath = \Self.seconds
    
    let seconds: Double

    static let short: Self = 0.2
}

I don't see considerable improvements between the above and this, though:

newtype Time = Double

extension Time {
    static let short: Self = 0.2
} 

This is slightly cleaner, I agree. But Time doesn't need to be a completely new type, it just needs to wrap its underlying value of type Double .

The post that this one is a reply to (#15) is the last one I read before going on a research binge. Of course, this thread ran its course in the meantime with a lot of new posts. I still want to express my thoughts.


I looked through my old posts for previous thoughts on newtype/strong type-alias and/or reduced-state types.

I only looked through threads I started, not one proposed by others (whether or not I participated in those). The last post in that list inspired some of my thoughts below.


Generalizing

A general reduced-state type can accommodate newtypes by simply not excluding any states. Everyone before my last post in this thread seemed to want only in same-state-count types, so I'm going to restrict myself to that. Some of the later posts do mention wanting to disallow some states of the original type into the new type. I think we should consider reduced-state types later, although there is the problem to duplicating effort if we have separate means of creating same-count newtypes and reduced-state new types.

Reduced state types also will require us to resolve the co-variance vs. contra-variance for the trampoline members. Technically, same-count new types do have the same issue, but there's more at stake for reduced-state ones.

Declaring Conformance

Unfortunately, I closed the tab, but I read a thread on this forum that copied a response from another thread that stated that the Core Team wants to keep the conformance of a protocol to a type explicit. New ways of declaring/constructing types shouldn't secretly add conformances. I already expressed this wish too, but that other thread lets me emphasize it more. That means the newtype must have a ": MyProtocol" somewhere; even if we let a new type copy all of its source's members, it doesn't copy its conformances implicitly.

Note that the new requested feature for tuples to conform to Equatable, Comparable, and Hashable violates this, but it's more limited and may be rectified in later versions of Swift (if added at all) if general tuple conformance is added.

Acknowledging New-Types

This relates to the last thread in my list above.

  • Whether or not a type is a newtype is an implementation detail.
  • This means that a new type that has explicit cases will be declared as an enum, otherwise as a struct. There won't be a newtype sibling construction.
  • The declaration needs to specify the access level of the newtype-ness. For example, if the aliasing is declared fileprivate, then anything from a different file will only see the new type as an ordinary struct/enum, and only items within the source file can access the new type's init(rawValue:) initializer for forward conversion and use as/as!/as? for backward conversion.

(Despite the init(rawValue:) initializer, a new type doesn't conform to RawRepresentable by default. That conformance has to be explicitly added. If it's added then the new type's newtype-ness has to have the same publicity as the new type.)

Expression of newtype-ness

I'm thinking of a form like:

struct MyType: @newtype(internal) MySourceType, MyProtocol { /*...*/ }

where the access level may be omitted. If it is, the default is the most restrictive of the new type's publicity, the source type's publicity, and internal. Remember that access level determines who can see init(rawValue:) and use as/as!/as?.

The source type may be a struct, enum, tuple, or a single enum case. The last one has the same interaction model as a tuple of the same shape, but the storage is the source's enum type. (Any other structural value types we add, like sum-tuples or fixed-sized arrays, may also be a source type.) Within any members of the new type, access to the internal state as the source type is done through "super". If we specify RawRepresentable conformance without giving a definition, we will have rawValue automatically forward to super.

If the source type is a single enum case, the core initializer is fail-able, where it fails only if the input is of a different case.

For the items that can see the new type's as/as!/as?, their code can reinterpret an instance of the new type as the source type with "as". If the source type is itself a new type, it can convert down the "inheritance" chain. It can cross- or up-convert to a different new type that shares an ancestor. (The last two reinterpretations apply only when the original item's code has enough access to see the destination type's "inheritance" chain, at least to the common ancestor). If the up-cast phase of an reinterpretation involves an enum case, then "as?" or "as!" is required.

If the new type is a struct, then any items that cannot see the init(rawValue:) initializer can only reuse values you give a high-enough access to, or create values through functions you give a high-enough access to. (You can make this an empty list to keep your new type practically private.) If the new type is an enum and the source is an enum too, then you get the same cases, but you can override the names:

enum MyType: @newtype MyOldEnum {
    // Prototype syntax
    publish case old(hello: Int, Double) as new(Int, world: Double)
}

If a new case name conflicts with a different case of the source, then you need to rename that other old case too. If the new type is an enum and the source is anything else, then you need to specify the name of the sole case:

enum MyType: @newtype MyOldStruct {
    publish super as single
}

That sole case will have a payload with one unlabeled item of the source type. (If the source is an enum case, the payload is one item of the equivalent tuple.)

I'll leave specification of trampoline members for a later thread.

Addition: I forgot to write about new-typing a singular enum case. They work the same as if Void was their equivalent tuple.

1 Like

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

I like the concept, it is very useful in addressing the problem of primitive obsession (https://wiki.c2.com/?PrimitiveObsession).
Unfortunately, I have no ideas about the preferred way of language design. But I can provide example and some wishes.

For now, we use the following structure as domain model of Email:

public struct Email: Hashable {
  public let wrappedValue: String

  public init(rawString: String) throws {
    if rawString.isValid() {
      wrappedValue = rawString
    } else {
      throw ValidationError()
    }
  }
}

Another one is ObjectIdentifier:

public struct ObjectIdentifier: Hashable {
  public let wrappedValue: UInt64

  public init(rawValue: UInt64) throws {
    // I our case we assume that 0 and UInt64.max are invalid values
    if rawValue > 0, rawValue != UInt64.max {
      wrappedValue = rawValue
    } else {
      throw ValidationError()
    }
  }
}

Our needs and requirements for now are:

  1. newType doesn't fully behave like underlying type.
    It is wrong to do math operations between two ObjectIdentifiers like ObjectIdentifier + ObjectIdentifier or ObjectIdentifier * ObjectIdentifier.
    [Email, Email, Email].joined(separator:) is also what we want to avoid.

  2. newType doesn't inherit initializers form the original type. We want such UInt64's initializers as init(bitPattern x: Int64), init(_source: Float), init() and others where not available by default. The motivation is that domain model has often additional restrictions and provides its own initializer.

  3. newType conforms to CustomStringConvertible by default if underlying type conforms to CustomStringConvertible. The same rule is preferred for CustomDebugStringConvertible, Decodable, Encodable, Equatable, Hashable. In our case, we need easily to check if two ObjectIdentifiers are equal, or encode them when sending to backend.

  4. newType optionally provides the access to wrappedValue if needed

I guess that we need to aggregate real world examples from different people. Maybe, it is good idea to aggregate those examples on separate page and adding link to it at the top of this one. Thus we separate examples form discussion.

2 Likes

If the tags are never meant to be instantiated, why not make them enums instead of classes?

1 Like

Excellent question!

You can use an enum for a tag, but you have to write a typealias to define RawValue:

enum OrderIdTag: CommonTags {
    typealias RawValue = String    
}

If you subclass TagBase, you save a little typing because the type parameter specifies RawValue:

final class OrderIdTag: TagBase<String>, CommonTags { }

Incidentally, I tried using an empty enum with a RawValue as a tag, like this:

enum OrderIdTag: String, CommonTags { }

But Swift (as of Xcode 11.5b2) rejects it:

/Users/mayoff/TestProjects/newtype/newtype/main.swift:259:18: error: an enum with no cases cannot declare a raw type
enum OrderIdTag: String, CommonTags { }
                 ^

Giving it an inhabitant fixes the error, but defeats the point of using an enum.