How about: subtypealias Farenheit = Double

Hi S/E,

I've been wondering if we could introduce a sibling to the typealias declaration of Swift as a lightweight way to declare a "sub-type" of an existing type. It seems so obvious this would be useful I'm sure it's been suggested before and I'd be curious what fate any discussion met.

In the example below Celsius is a distinct identifier that is a Double and acquires all its attributes and behaviour but a Double is not always always a Celsius as with an existing typealias. As such, a Double or a Farenheit cannot be passed to a function expecting a Celsius without an explicit cast (which would always succeed). Conversely in good sub-type tradition a Celsius can always be passed to a function expecting a Double. Ideally you could extend the new "sub-type" without polluting the original type and perhaps even dynamic casting could be supported at runtime though that wouldn't be essential.

In this way, not just the storage of the type but the significance of its contents could be type checked in a lightweight way and in the example below the function call that would likely destroy the kettle wouldn't even compile.

subtypealias Celsius = Double
subtypealias Farenheit = Double

let boilingPoint: Farenheit = 212.0

func britishKettle(heatTo: Celsius) {
    // ...
}

britishKettle(heatTo: boilingPoint) 🍿
// Other potential uses 
subtypealias FilePath = String
subtypealias URLString = String
subtypealias NewsURL = URLString
subtypealias Email = String
subtypealias UUID = String
etc...
9 Likes

I believe the term "new-type" is used in reference to this feature. I don't think its utility could possibly be in doubt, and I'm not sure what may be the reason or reasons why it has not yet been added to Swift.

I have a package called DistinctType-module which I use like this:

// Celsius.swift
public typealias Celsius = DistinctType<Double, _Celsius>
public enum _Celsius { }

It's not as pretty as a built-in solution would be, and what's worse is that the diagnostics use the full type name DistinctType<Dobule, _Celsius> instead of the much more readable Celsius, but at least it does the job of keeping semantically incompatible values separate despite having the same underlying type.

My number one use case for this is with UUID as the underlying type. This approach continuously saves me from what would otherwise be insidiously subtle bugs caused by accidentally passing the wrong identifier.

P.S. I believe macros can improve significantly upon my DistinctType approach, I just haven't gotten around to implementing it yet.

3 Likes

Whenever I've had a need for this, I just create a wrapper struct. I think that's the Swiftiest thing to do - one of the original tenets of the language was that these kinds of wrappers should be "zero cost".

So for your example, I'd do something like:

struct Temperature {
  var celsius: Double

  var fahrenheit: Double {
    get { 32 + (celsius * 9/5) }
    set { celsius = (newValue - 32) * 5/9 }
  }
}

And I find that it tends to pay off to look for those kinds of abstractions. For instance, I think it makes sense to have a Temperature value which is distinct from, say, a Length or Mass value, and to represent that in the type system. I don't think it makes as much sense to make temperatures in Celsius a distinct type from temperatures in Fahrenheit. Subtyping isn't really the relationship we want here.

IIRC, the one annoying thing about this pattern is that the synthesised Codable conformance uses a keyed container, so you need to override it and used a single-value container instead. It's easy enough to make a protocol which will do that for you, though.

And that extends to things like ID numbers. The language has long had synthesis for memberwise initialisers and common standard library protocols in structs; it's been optimised so these kinds of wrappers really do not require much code at all:

struct UserID: Equatable, Hashable /*... etc */ {
  var wrappedValue: UUID
}
8 Likes

The problem with making a simple wrapper type like this is that you have to reference the underlying value explicitly.

massOfRocket.value + massOfFuel.value

You can of course make your wrapper type conform to AdditiveArithmetic, but that's a big burden if you're creating many of these wrapper types for various semantically distinct Double-like types.

4 Likes

Yeah I'm (personally) not really bothered by that.

And sometimes it can be quite useful - for instance, types which represent physical quantities can often give meaningful names to the properties which provide numeric values. So it isn't just .value, but rather .kilograms, .celsius, etc.

For things like UserID, there isn't really a meaningful name for the wrapped property, but that also doesn't tend to be a big problem in practice because you generally want to keep the value wrapped, and operations on the underlying value tend to be domain-specific and best written as member functions on the wrapper itself (e.g. UserID.redactedDescription).

3 Likes

It's somewhat telling that every time people request this feature, they choose an example for which it should absolutely not be used. E.g. Fahrenheit = Double is semantically bogus, because it would allow you to multiply two temperatures and get a temperature, take the square root of a temperature, etc. Like Karl said, this example is much better modeled with a wrapping struct, and some good tools to reduce the amount of boilerplate needed for that would be most welcome.

FWIW, I think this would be genuinely useful to have this, but we would have to be pretty careful not to encourage people to use it as commonly suggested.

23 Likes

Agree this would be useful at times, I just did a few wrapper structs, e.g.

// We need type-specific property identifiers here to allow for proper type completion in the IDE for the auto codegen
public typealias PropertyIdentifier = UInt64

public struct InstrumentPropertyIdentifier: Hashable, Equatable {
    var value: PropertyIdentifier
}

public struct TransactionPropertyIdentifier: Hashable, Equatable {
    var value: PropertyIdentifier
}

public struct PluginPropertyIdentifier: Hashable, Equatable {
    var value: PropertyIdentifier
}

...

But then need to add boilerplate like this:

extension InstrumentPropertyIdentifier {
    init(_ value: UInt64) {
        self.value = value
    }
}

To allow for convenient init, e.g.

    static let testIdentifier: InstrumentPropertyIdentifier = InstrumentPropertyIdentifier(xxx.propertyIdentifier)
/// xxx.propertyIdentifier is a PropertyIdentifier
1 Like

I like it. For class types it would be equivalent to subclassing:

// bike-shedding:
class C {}
subtype D: C
class D: C {} // equivalent for all intents and purposes
var c = C()
var d = D()
c = d // βœ…
d = c // πŸ›‘

for value types it would be a new feature we didn't have before:

class S {}
subtype D: S
var c = S()
var d = D()
c = d // βœ…
d = c // πŸ›‘

Extending a subtype:

extension D {
    func foo() {}
}
d.foo() // βœ…
s.foo() // πŸ›‘
1 Like

You don't generally want the subtyping, though, since as Steve noted there are almost always operations on the original type that don't make semantic sense on the newtype. To me, this seems like an ideal case for a macro, now that we have them: there's no reason in Swift for newtype to exist as a new kind of type, since single-field structs and enums already take on the exact layout of their one field, so a macro that takes an original type name and a set of protocols and/or individual methods to forward onto the newtype seems like it should be able to create the single-field struct type, the initializers to convert to and from the new type, and forwarding protocol conformance implementations for the chosen protocols.

13 Likes

I do not think that Steve example should be used as a reason to not have subtyping in the language. Similarly we do not rule out subclassing just because someone somewhere might write:

class Number {
    func sqrt() -> Self { ... }
}

class Fahrenheit: Number {}

var fahrenheit = Fahrenheit()
let fahrenheit2: Fahrenheit = fahrenheit.sqrt()
// how odd. let's not have subclassing at all!
4 Likes

But no one should use subclassing for that either, since that's just bad design no matter how you express it. Imagine if you had two of these subtypes, Fahrenheit and Kilometers. If they are both subtypes of some number type, then fahrenheit * kilometers is valid because they are both still the plain number type, despite it making no sense.

5 Likes

Right. My point is: just because someone can misuse it doesn't mean we should not have it in the language at all. There could be other reasons why not to have it, a potential of misuse is just not one of those as my example with the "wrong" subclassing shows.

This is another example where it seems like a macro would be a great solution, but unfortunately, our current syntax-only macros don't actually get us very far toward completing the job. Right now, a macro wouldn't have access to:

  • the wrapped type's initializers to mimic on the new type's declaration
  • a protocol's members for forwarding the implementation to the wrapped type
  • whether or not the wrapped type already conforms to the given protocols, resulting in wonky errors when the macro speculatively tries to forward those implementations

It would be great to use this use case as a guide toward macros that would let us expand the language in this way.

21 Likes

Right. But when you suggest subclassing, people immediately come forward with a bunch of examples where it (at least sort of) makes sense (Cat: Animal, Car: Vehicle, whatever). Whereas, when you suggest this feature, experience shows the first few examples people use are pretty much always semantically bunk. Again, that's not to say that it's not a good feature--rather it's to say that it's a feature that does not actually solve the problems that usually motivate it.

1 Like

To be honest, I'm more and more reluctant to see any such additions (like subtyping) as a benefit to Swift.

If there is a trick to achieve the desired purpose, we should use it. In my view, the DistinctType-module is just the right thing for the job.

In distant past, I have been using

struct ID<Parent>: ... {
  let id: Int
}

struct User {
  let id: ID<User>
}

I think these solutions are even superior to using some subtyping.

The subclassing example I used was deliberately bonkers to prove the single point: just because I can come up with a bonkers example doesn't mean we should not have the feature at all. Here's another one for you:

typealias Fahrenheit = Double
let fahrenheit: Fahrenheit = 100
let fahrenhet2: Fahrenheit = sqrt(fahrenheit)

Does that mean that we should not have "typealias" feature in the language (if we didn't have it already)? No!

As for a more sane cat/dog example, here you go:

struct Animal {}
subtype Cat: Animal
extension Cat {
    func meow() {...}
}
subtype Dog: Animal
extension Dog {
    func bark() {...}
}
let cat = Cat()
cat.meow() // βœ…
cat.bark() // πŸ›‘
2 Likes

This would have type Double so you wouldn't be able to pass it to something expecting Fahrenheit or Kilometers. Reuse of abstractions is not zero cost but it is also generally far from zero benefit.

3 Likes

Maybe better than having a specific macro - or otherwise requiring bespoke macros for each sort of construct like this - we could have a way to declare that a given type forwards (statically) to an underlying stored property? e.g.:

struct Celsius {
    let value: Double supplies AdditiveArithmetic,
                               Comparable,
                               CustomStringConvertible,
                               etc
}

That's probably not the best syntax, and I don't really care what the syntax is - the point is that this would be super handy. I often need this kind of functionality and I don't think a macro is a sufficiently flexible or elegant way to solve it. e.g. there might be multiple properties and more than one of them might provide conformances:

struct User {
    let id: UUID supplies Equatable, Comparable, Hashable
    let name: String supplies CustomStringConvertible
    let admin: Bool
}
6 Likes

BTW, this is not true with multiply:

Q = C x M x dT
Q/C = M x dT

The left and right parts of the equation here are measured in [K x kg] which or equivalently to [Fahrenheit x kg] units.

Your example would read better with addition or subtraction.

2 Likes

Which is helpful but I think not sufficiently protective, because lots of things would accept that Double without complaint, like string interpolation. I think the point is valid that you want the type system to enforce either:

  1. You can't do multiplication of two units that are (arbitrarily) considered "incompatible".

  2. The type of the result is not a dangerously generic underlying type (Double in this case) but the correct third type that represents the combination, in this case FahrenheitMeters or somesuch.

    Which wouldn't prevent it being used in e.g. string interpolation, but presumably its CustomStringConvertible conformance would include the units, so you'd at least get a result like "5.38 kmΒ°F" which is technically correct and if nothing else alerts the reader that something dumb / weird happened.

Tangentially, the Measurement types in Foundation try to accomodate this particular use case, although they're crippled by some nasty behaviour (for a start, there's two Measurement types, one a generic and one the Objective-C class, and the Swift compiler is readily confused about the two).

2 Likes