How about: subtypealias Farenheit = Double

Nonetheless, the resulting Double is almost certainly meaningless as a generic number, and you would get no safeguard from the additional type from using it as such. If you had some sort of dimensional analysis going on then maybe you would implement * on the unit types to produce another appropriate composed unit type, which you would want to replace the plain numeric * rather than be available in addition to it.

1 Like

Between sub-types I don't know but if multiplying two Fahrenheit's was really a problem couldn't you define an extension on Fahrenheit with an unavailable * operator to prevent it?

I think that there are probably times (especially in application development) where you might need to represent a value in a network model for example, so I think there are cases where even items with semantically malformed operations can still benefit from being distinct types. My read of this thread is that it's trying to solve the same issue that GitHub - pointfreeco/swift-tagged: 🏷 A wrapper type for safer, expressive code. is trying to address.

1 Like

Dimensionless values have to fall out naturally from dividing dimensioned values. e.g. dividing Celsius by Celsius. You can't get them with disparate units (nor via multiplication, for that matter).

Not disagreeing with your overall point; in fact emphasising it with this clarification.

Addendum: although in retrospect I quickly realise that I likely misinterpreted your statement (dimensional analysis != dimensionless values a.k.a. ratios). Sorry about that!

2 Likes

But then it's no longer a subtype, is it?

3 Likes

My question would be, to what extend would your proposal limit what the subtypealias can do vis a vis the aliased type?

If they exhibit the same behavior, then I expect confusion: the subtypealias will undoubtedly describe something domain specific. Which for me (ie as user of an API) leads to expect domain specific behaviors and constraints. Which they only have if there is some way of influencing behavior of the subalias.

If we don’t, I expect confusion for readers/users of the code where this is feature is used.

I am not necessarily pro or against this feature, but would note that it opens quite interesting and previously unforeseen possibilities. A few examples:

class Class { ... }
struct Struct { ... }
enum Enum { ... }
typealias Proc = () -> Void
typealias Tuple = (Int, Int)

type C: Class { // instead of "class C: Class"
    // subclass as usual
}
type S: Struct { // 🆕 as if "struct S: Struct" was allowed to make a sub-struct
    // can add methods or static members, but not stored properties
}
type E: Enum { // 🆕 as if "enum E: Enum" was allowed to make a sub-enum
    // can add methods or static members but not new cases
}
type P: Proc { // 🆕 sub-closure
    // ditto, perhaps
}
type T: Tuple { // 🆕 sub-tuple
    // ditto, perhaps
    func foo() {}
}
(1, 2).foo() // 🛑
var tuple: T = (1, 2)
tuple.foo() // ✅ 🆕

Edit: on the second thought this possibility could exist without involving subtypes:

extension T { bar() {} }
(1.2).bar() // ✅ 🆕
1 Like

But what is the difference between this and subclassing, really? I never understood the point of those Cat/Animal example is programming books, but it would seem that the only real difference between this idea and subclassing, which we do have, is that these subtypes wouldn't have access to the supertypes version of a method, right? It seems to have basically the same benefits and dangers as subclassing, no?

1 Like

When I have wished for this feature, I have always wanted to add member to the subtype that would not be exposed on the supertype, like a Fahrenheit accessor on a Celsius type.

This should be illegal, unless T: ExpressibleByTupleLiteral. The main point of the feature is the absence of implicit conversion between types of the same raw type.
Everything else is debatable.
Another issue this could address is preserving value/ref semantic of a type.
Take a look at this struct or its closed source variant - they are structs with in fact ref semantic. But weak/unowned modifiers can't be applied to them, and they don't conform to AnyObject.

I think it is the same for classes and extends the idea of subclassing to all types.

I'd say calling "super.method()" could be allowed for all subtypes, similar to how it is allowed for subclasses.

Yes

Absolutely.

This works with subclasses:

class C: ExpressibleByIntegerLiteral {
    required init(integerLiteral value: Int) { ... }
}
class D: C {}
var d: D = 1 // ✅

so logically it should work with subtypes as well.

Oops. I meant if the type is initialized with a literal, it's fine. But if it's initialized with another type, which is compatible by raw type - this should be explicit. You used a literal which makes it fine. But this shouldn't work:

var c: C
var d: D = c
1 Like

Then along came the chemists with their “mole”…

1 Like

In all seriousness, the only subtyping feature I’d really like to see is the one Pascal has where you can define restricted range integer subtypes (and even use them as array indices, so you can have your array indexed from -17 to 36 if that’s what you want).

Using this for units is wrong; as others have said, you really want to use structs for that. Also, if anyone is doing that, I’d much rather see e.g. a Length struct rather than a set of Inch, Foot, Yard, Metre and so on. (Put another way, that Farenheit vs Celsius example is even more ridiculous than others have already pointed out; it should just be struct Temperature.)

4 Likes

...or an enum with an associated type.

The temperature example illustrates so nicely how subtyping is an attractive but incorrect way to model the desire to add a property (e.g., a unit) when it functions as a discriminant, because I want to create something that's related to an existing type "with a twist" and subtyping is a kind of "something with a twist"—just not the right kind.

6 Likes

I think that RawRepresentable would be the right way to go for "newtypes", since it's already used for enums with raw values, and option sets, which in a way serve as "newtypes" for their corresponding raw value types. Plus, default protocol conformances for RawRepresentable types that "delegate" to the raw value already exist for Equatable, Hashable, and Codable.

We could add some syntactic sugar for conforming structs to RawRepresentable, like what we have for enums, in order to make creating "newtypes" more ergonomic. And maybe we can open up "delegating" default protocol conformances for RawRepresentable types to more protocols (like AdditiveArithmetic), or even have them synthesized by the compiler.

So this:

struct Fahrenheit: Double, AdditiveArithmetic {}

extension Fahrenheit: ExpressibleByFloatLiteral {}

extension Fahrenheit: ExpressibleByIntegerLiteral {}

could be syntactic sugar for:

struct Fahrenheit: RawRepresentable {
    var rawValue: Double
    
    init(rawValue: Double) {
        self.rawValue = rawValue
    }
}

extension Fahrenheit: AdditiveArithmetic {}

extension Fahrenheit: ExpressibleByFloatLiteral {}

extension Fahrenheit: ExpressibleByIntegerLiteral {}

with some default protocol conformances:

extension RawRepresentable {
    func withRawValue(
        _ body: (RawValue) throws -> RawValue
    ) rethrows -> Self? {
        Self(rawValue: try body(self.rawValue))
    }
}

extension RawRepresentable where Self: AdditiveArithmetic,
                                 RawValue: AdditiveArithmetic {    
    static func + (lhs: Self, rhs: Self) -> Self {
        lhs.withRawValue { $0 + rhs.rawValue }!
    }
    
    static func - (lhs: Self, rhs: Self) -> Self {
        lhs.withRawValue { $0 - rhs.rawValue }!
    }
    
    static var zero: Self { Self(rawValue: .zero)! }
}

extension RawRepresentable where Self: AdditiveArithmetic,
                                 Self: ExpressibleByIntegerLiteral,
                                 RawValue: AdditiveArithmetic {
    static var zero: Self { Self(rawValue: .zero)! }
}

extension RawRepresentable where Self: ExpressibleByIntegerLiteral,
                                 RawValue: ExpressibleByIntegerLiteral {
    init(integerLiteral: RawValue.IntegerLiteralType) {
        self.init(rawValue: .init(integerLiteral: integerLiteral))!
    }
}

extension RawRepresentable where Self: ExpressibleByFloatLiteral,
                                 RawValue: ExpressibleByFloatLiteral {
    init(floatLiteral: RawValue.FloatLiteralType) {
        self.init(rawValue: .init(floatLiteral: floatLiteral))!
    }
}
1 Like

i want to push back a little on the basic premise of this idea, which is that stronger typing is always better.

i think that if you are someone who has chosen swift as your favorite programming language, as opposed to something like python, this isn’t even an opinion, it is accepted as a basic truth. and because of that i think it’s really valuable to be aware of overtyping as something that afflicts swift developers.

i’m going to give an example inspired by my own experience as someone susceptible to overtyping. let’s suppose we’re writing a library that interacts with websites in the swift ecosystem. for type safety, you might assume some useful URL types might include:

/// A URL pointing to a forums.swift.org page.
public
struct SwiftForumsURL:RawRepresentable<PercentEncodedString>
/// A URL pointing to a swift.org page.
public
struct SwiftOrgURL:RawRepresentable<PercentEncodedString>
/// A URL pointing to a en.wikipedia.org page.
public
struct WikipediaURL:RawRepresentable<PercentEncodedString>

is this a good idea? of course it’s a good idea! how else would you express

func loadSwiftForumsPost(_ url:SwiftForumsURL) -> SwiftForumsPost?

?

you certainly wouldn’t want to Accidentally Pass a wikipedia URL to loadSwiftForumsPost(_:). it’s hard to see any downsides to such a statically-sound arsenal of types.

and yet there are downsides, they come from the basic fact that code must eventually interface with other code. let’s suppose you’re now writing the logic that renders forum posts.

import HTML
import SwiftURLTypes

struct DiscourseForumsPost
{
    let url:SwiftForumsURL
    let content:HTML
}

but do you really want the HTML rendering module to have a dependency on the SwiftURLTypes module? why should DiscourseForumsPost care what kind of URL it has? this is an unnecessary coupling between two components of an app that really ought to function independently. if you had not written the SwiftURLTypes beforehand, you would have simply represented the url property with a String. instead, SwiftURLTypes has become a universal dependency that every other target in your project must import.

at this point, you might spend some time refactoring the project into a more sophisticated dependency graph.

//  URL module
.target(name: "SwiftURLTypes", dependencies: ["PercentEncoding"]),

//  Discourse module
.target(name: "DiscourseForums", dependencies: ["HTML"]),

//  Overlay module for API sugar
.target(name: "_DiscourseForums_SwiftURLTypes", 
    dependencies: ["DiscourseForums", "SwiftURLTypes"]),

you might even convince yourself you’ve gotten the best of both worlds with the overlay module. but this took a lot of time and you added a lot of public API cruft to get SwiftURLTypes to interoperate smoothly with all the other components of your project.

was it worth it?

1 Like

This is certainly a valid point. I can quite easily see the real danger of such lightweight sub-typing might be someone starting obsessing and creating an elaborate myriad of type hierarchies and a maintenance nightmare for others rather than someone inadvertently multiplying two Fahrenheit's. I do think though, giving people the option of a moderate increase in typing would help avoid situations like this:

Just to restate the pith of the pitch (trying to steer away from the weakness of my example):

  • It is an additive change - it doesn't break anything
  • It is concise and easy to understand for all levels of programmer.
  • It works for value types.

I think whether to implement it in Macros is a bit of a red herring as you'd still need to decide whether the "new type" inherited all attributes and conformances of the original type anyway with the pros and cons that entails. I also have the impression by the time you brought Macros up to the job you might as well implement it as a core feature of the language anyway though it could be time well invested.

1 Like

There are ways around this, though, which some people think are very inherently Swifty (I'm less enthused, to be honest, but it's one of those things where swimming against the tide is tiring).

import HTML
import URLCore

struct DiscourseForumsPost {
    let url: any URLProtocol
    let content: HTML
}

Or:

import HTML
import URLCore

struct DiscourseForumsPost<URLType: URLProtocol> {
    let url: URLType
    let content: HTML
}

(which just pushes the existentials up to the users)

While this doesn't directly address the principled question of whether URLs should be represented by more than one type, I think that's probably context-specific, so the point is just that you can use more than one type without necessarily compromising heavily in other areas.

And that tends to be true of many concepts, not just URLs, of course.

I probably wouldn't, because URLs are nasty little buggers from a security and correctness perspective, so it's wise to validate them as soon as possible after they enter your code, and to enforce that through the type system via a formal URL type (not necessarily Foundation's URL, but something which at least cannot be constructed from / to be an invalid URL).

2 Likes

There’s a great library from the Pointfree guys that I use when I need this type of typing:

4 Likes