Extension methods for non-nominal types

They support the == operator but do not conform to Equatable.

Tuples are not Equatable. The standard library has to implement == for tuples like this:

public func ==<T: Equatable>(lhs: (T, T), rhs: (T, T)) -> Bool {
  // implementation here
}
public func ==<T: Equatable>(lhs: (T, T, T), rhs: (T, T, T)) -> Bool {
  // implementation here
}
// and so on up to 6
func checkIfBothAreEqual<T: Equatable>(lhs: T, rhs: T) -> Bool {
  return lhs == rhs
}

checkIfBothAreEqual(lhs: (16), rhs: (16)) // This does not work
1 Like

Confused as to why would this be so difficult, when it's already possible to write generic operators for non-nominal types. Surely there's some similarity between the two? or am I missing something obvious?

Separate topic, please can we not muddy this discussion with a feature that really deserves its own thread.

1 Like

One of the difficulties is how protocol conformances are implemented in the compiler. If you dig around in include/swift/AST/ProtocolConformance.h, there's a hierarchy of ProtocolConformance classes that determine how various types conform to protocols (for example, does a type conform directly, or does it conform via its inheritance to another type that conforms to it, or does it conform via a specialization of a generic type that declares conformance to it).

It was recommended by one of the core team members that one could add a TupleProtocolConformance that would define a conformance derived from the conformance of its elements. (Of course, this derivation only makes sense for protocols like Equatable and Hashable, and you'd still want to be able to make an arbitrary type T conform to an arbitrary protocol P without any derivation, but rather by just providing the implementation as you would any other protocol extension.)

I tried diving into this and quickly found that one big problem is that all of those classes are written with the assumption that you can walk up the hierarchy to a root conformance that is on a nominal type. Removing that assumption rippled quite a bit through the rest of the implementation and broke a lot of things, and unfortunately I didn't have the bandwidth to dig into it further.

10 Likes

Thanks, yeah, that finally makes sense; in both C# and Kotlin, Tuples and Functions are nominal types hence they could exploit it and Swift couldn't.

Surely this is a discrepancy, that when spare bandwidth presents, needs to fixed. i.e. ideally with no differentiation between the two.

I trust this is not something that Swift's ABI is going to make more difficult to accomplish..

1 Like

I think that the current situation makes tuples really hard to work with in Swift. For examples, due to tuples not being Equatable, they can't really be used in a good way with libraries like Nimble, e.g. the following won't work:

expect(("a", "b")).to(equal(("a", "b")))

But more generally this applies to any kind of code that you might naturally want to extend to tuples. It also means that, while individual tuples can be checked for equality, arrays of tuples can't, etc.

Tuples are the most boilerplate-free version of product types and thus incredibly useful; the fact that they're so limited is really a big weakness of Swift currently, IMHO.

7 Likes

It's probably not feasible anymore to have an anonymous counterpart for each entity (method, class, struct, enum...), but having tuples with extensions would even be useful if they aren't anonymous anymore:

typealias Point2D<T: Numeric> = (x: T, y: T)

I don't think return (x: 4, y: 2) is a big win over return Point2D(x: 4, y: 2) - but structural types are compatible as long as their structure is identical, so you wouldn't need to agree on a common Point-lib and still be able to use values from one library with functions from an unrelated framework.

Agreed. Comparatively with other language implementations, this still feels very much half cooked.

Agreed... and the same goes for functions with extensions... and I'm assuming that generic typealias-ing with this, would also work if the more general non-nominal type issues were addressed.

There are infinite reasons to have (Int, Int), so an extension (Int, Int) seems dangerous to me.

let x = (0, 0)
x.something()

Since Xcode will autocomplete all of the members available through extensions, one would have to be careful that they were using the member(s) they meant to interpreted (Int,Int) with the same semantics as the extensions they're defined in.

typealias XYCoordinates = (Int, Int)

extension XYCoordinates {
    var isAtOrigin: Bool { return self.0 == 0 && self.1 == 0 }
}

let pair: XYCoordinates = (0,0)
let b = pair.isAtOrigin

let tuple: (Int, Int) = (0,0)
let b = tuple.isAtOrigin // error, property not found

I can see the value in having extensions on anonymous tuple shapes like (Int, Int) or (T, T) where T: Comparable, but if we could also constrain extensions to named types we could be more disciplined about what we accept in anonymous extensions to avoid accidental programmer error.

2 Likes

I think something that could help with your concerns here is newtype, which would basically be enhanced typealiases that look like separate types while inheriting the original type's functionality. That means you can add independent protocol conformances for what is essentially the same underlying type. I think it could also be a more general solution to the one type, one conformance problem.

8 Likes

Something like swift-tagged?

As for

you can already define functions with tuples as arguments, which by that logic should be considered "dangerous" as well.
Of course, it wouldn't make too much sense to add semantic methods/extensions to tuples, but there's structural things that can be done, e.g. making tuples equatable and comparable.

1 Like

Exactly re functions, but so too operators; the ability to extend non-nominal types is a limitation in Swift solely due to its underlying implementation. Whereas in Kotlin, and C# this limitation doesn't exists because both Function and Tuples are essentially nominal types.

As for it wouldn't make too much sense to add comment; that's an opinion I don't share, but its inconsequential here because the topic at hand is about ensuring non-nominal types are as full featured as nominal types.

Tuples in Swift have/had a number of syntactic inconsistencies; many of which the core team and others have strived to remedy (with some push back).

So this particular track is certainly not exposing any new "danger" that wasn't already there. Plus for example: there's nothing stopping the implementation from requiring double wrapping of rounded brackets as has been proposed before, and similarly the agreed amendments should carry forward for any other inconsistencies.

Edit:
Extension methods essentially shouldn't behave any different to this.

typealias XYCoordinates = (Int, Int)

func isAtOrigin(_ tuple: XYCoordinates) -> Bool {
  return tuple.0 == 0 && tuple.1 == 0
}

let pair: XYCoordinates = (0,0)
let b = isAtOrigin(pair) // OK

let tuple: (Int, Int) = (0,0)
let b = isAtOrigin(tuple) // OK

If you can tolerate a postfix operator like eg , then you can extend (an "invisible" wrapper of) any type and write eg:

let myView = UIView()•.configure {
    $0.translatesAutoresizingMaskIntoConstraints = false
    $0.backgroundColor = UIColor(red: 1, green: 0, blue: 0.2, alpha: 0.4)
}
Here's the code that will make that work.
struct NominalWrapper<InnerSelf> {
    let innerSelf: InnerSelf
    init(_ innerSelf: InnerSelf) { self.innerSelf = innerSelf }
}

postfix operator •
postfix func •<T>(lhs: T) -> NominalWrapper<T> { return NominalWrapper(lhs) }

extension NominalWrapper where InnerSelf: AnyObject {
    func configure(_ block: (InnerSelf) -> Void) -> InnerSelf {
        block(innerSelf)
        return innerSelf
    }
}

I guess the same concept could be taken further, to allow:

(1, 2)•
    .scaled(by: (3, 4))•
    .translated(by: (-3, -8))•
    .isAtOrigo()
    .print() // Prints true

or

(1, 2)• * (3, 4)• - (3, 8)• == (0, 0)• // true

or whatever (note that (1, 2)• is of a type that can conform to protocols etc, unlike (1, 2)).

EDIT: But playing around with this will probably mean running into this and other tuple related compiler bugs.

1 Like

I haven't thought about operators in this context, and the trick with a helper type makes it much more easy to see what is going on (hardly any custom operator has an obvious meaning, and they are hard to look up :frowning: ).

Another alternative are plain old free functions (something along the lines of func with<T>(_ input: T, block: (inout T) -> Void) -> T), which can be defined for all kinds of types...
For reference: Request for Discussion: Setup closures

Returning to the original topic ;-): Is anyone using free functions to work with tuples or closures which would work better as extensions?

The reason I used a postfix operator rather than a free function as a workaround (for the current inability to extend non-nominal types) was that it makes it less cumbersome to both implement and use: You'll get code completion for your extension methods by simply adding eg a where the cursor is, instead of having to back up and prepend the non-nominally typed value with a free function and wrap it in another set of function-application-parentheses. Free functions are also hard to lookup/discover (they have no context for code completion except the module they are defined in).

That is, something like the previous example:

(1, 2)•
    .scaled(by: (3, 4))•
    .translated(by: (-3, -8))•
    .isAtOrigo()
    .print() // Prints true

would look less clear and require a more involved implementation using free function(s) instead of a postfix operator. (Try implementing it both ways and you'll see what I mean.)

Sure a lot of stuff can be tagged on with custom operators and/or wrappers; and Swift's history is littered with many examples including e.g. this more recent one for turning a keypath into a function .

1 Like

we’ve had this discussion before but since SIMD types look like they will be landing very soon this point (no pun intended) seems a bit moot.

I would however very much like to see ExpressibleByTupleLiteral support in the compiler, so we could return (4, 2).

4 Likes

Would like to know what's the status for this thread, in particular, what are the obstacles to make a function / closure a nominal type.

This is related to my recent work with UnboundedRange, different from PartialRangeUpto or PartialRangeFrom or Range, this is a function, hence, cannot conform to a particular protocol. It presents challenges when supporting it because it requires to enumerate all possible combinations to support multi-dimensional array indexing.