Pair<A,B>, Nominal Tuples and Variadic Generics

Of course there's quite a difference, and in a flattened-tuples-only world your first typealias wouldn't be allowed.

But you would easily solve the problem by defining an actual struct for that case, instead of a typealias, and that would be the proper way to represent a DirectedForce type, also considering that labels have no type-level meaning, and your labels could be lost when reassigning an "instance" of DirectedForce, like the following:

typealias DirectedForce = (force: Float, direction: (Float, Float, Float))

let x1: DirectedForce = (force: 1, (1,2,3)) /// no enforced label, which is confusing

x1.direction /// but this compiles

let x2: (Float,(Float,Float,Float)) = x1 /// this is perfectly fine

x2.force /// compilation error

My point is: the convenience gained by only allowing flattened tuples would be problematic only in cases where tuples shouldn't be used at all.

It's a nonstarter to remove basic features of the language that aren't doing harm. Overall, this seems like a diversion from the overarching points about tuple conformance to basic protocols and about variadic generics.

3 Likes

As a matter of fact, people have different opinions on nearly everything - including wether tuples should be used in a certain situation or not.

I think the first question to answer is if tuples should really be some kind of "structs without name":
If there is any possibility that tuples should get extensions in the future, imho the forced flattening would be completely contra-productive.

4 Likes

Thanks for making your reasoning explicit. Now, "should" does not turn into "must" easily. Tuples have their utility. Even tuples that some people would turn into structs. Who knows? Maybe that tuple is just some code that has organically grown from its simple early days? Maybe one is experimenting with how far tuples can be used? You see, our present tuples scale: you can extend them at will. The language does not punish you when tuples temporarily turn big, for example in the middle of a refactoring. It is important to let one decide, on his own, if and when a tuple should turn in a struct. This is not the compiler's business. And not yours either.

1 Like

I would say that flattened tuples can be an actual feature of the language, and that the meaningless distinction from, say ((A,B)) and (A,B) makes tuple destructuring problematic in the first case, so it does harm. But I agree that this discussion is beyond the point of the thread.

It's business of the language and the community as a whole. A lot of approved proposals removed features from the language, thus removing support for certain styles and encouraging others.

1 Like

I think it's important to separate the two features under discussion here: extensions and conformances for non-nominal types and variadic generics.

The two features compose nicely, but their are individually useful. I think extensions of structural types is the more important one, for precisely the reason @Logan_Shire notes: it is weird and annoying that you can use == on two tuples but the same types aren't Equatable. It's a sharp bend in the learning curve that makes the language less composable, and feels non-orthogonal.

It's also a much easier feature to design and implement. @allevato is right that the "root conformance is a nominal type" assumption is buried fairly deeply in the compiler, but it's also something that we can refactor away over time. From the implementation standpoint, it's a matter of first generalizing extensions (so one can extend tuple/function/meta- types) to remove the "nominal type" assumption, then do the same for protocol conformances. Much of the runtime infrastructure is already in place.

If someone is interested in chipping away at this, we can chat over on the compiler development part of the forum about details. We'd also want to turn the sketch into a proper pitch/proposal.

Doug

14 Likes

As extra argument in favour of doing something here (not that we particularly need one)... I believe this doesn't quite work: if the sequences are different lengths, but the common prefix is the same, it will return true. E.g. Foo(orderedPairs: [("x", 1)]) == Foo(orderedPairs: [("x", 1), ("y", 2)]).

Hmm. There might be ways to make that not true, but even if there aren't, you could use a slightly more complex recursive definition:

protocol TupleElements {
  associatedtype Element
  associatedtype Rest: TupleProtocol
  
  var element: Element { get set }
  var rest: Rest { get set }
}

struct Tuple<Elements: TupleElements> {
  var elements: Elements
}
struct Pair<Element, Rest: TupleProtocol>: TupleElements {
  var element: Element
  var rest: Rest
}
struct End: TupleElements {
  var element: Never { get { fatalError() } set { fatalError() } }
  var rest: None { get { return None() } set {} }
}

// () => Tuple<End>
// (T) => Tuple<Pair<T, End>>, convertible to and from plain T
// (T, U) => Tuple<Pair<T, Pair<U, End>>>
// (T, U, V) => Tuple<Pair<T, Pair<U, Extra<V, End>>>>

extension Tuple: Equatable where Elements: Equatable {
  static func == (lhs: Tuple, rhs: Tuple) -> Bool { return lhs.elements == rhs.elements }
}
extension Pair: Equatable where Element: Equatable, Rest: Equatable {
  static func == (lhs: Pair, rhs: Pair) -> Bool {
    return lhs.element == rhs.element && lhs.rest == rhs.rest
  }
}
extension End: Equatable {
  static func == (lhs: End, rhs: End) -> Bool { return true }
}

The idea is that, since only the top-level Tuple type is convertible to T—not the inner Pair type which needs the distinction—it wouldn't get confused in the same way.

On the other hand, that's a pretty esoteric design. A better solution might be to make auto-derivation first-class for types whose properties all conform to the same protocol, and then make tuples automatically conform to all auto-derivable protocols shared in common by their elements. For example:

// These three extensions not only apply to nominal types, but also to tuples:
public derivable Equatable {
  public static func == (lhs: Self, rhs: Self) -> Bool {
    // For enums, `#keyPaths(_:)` would give a set of key paths for each 
    // enum case of type T?, where T was the type of the associated values.
    #for prop in #keyPaths(Self)
      if lhs[keyPath: prop] != rhs[keyPath: prop] { return false }
    #endfor
    return true
  }
}
public derivable Hashable {
  public func hash(into hasher: inout Hasher) {
    #for prop in #keyPaths(Self)
      hasher.combine(self[keyPath: prop])
    #endfor
  }
}
public derivable Comparable {
  public static func < (lhs: Self, rhs: Self) -> Bool {
    #for prop in #keyPaths(Self)
      if lhs[keyPath: prop] < rhs[keyPath: prop] { return true }
      if rhs[keyPath: prop] < lhs[keyPath: prop] { return false }
    #endfor
    return false
  }
}

Hopefully the optimizer would be able to convert the keypath lookups into direct references and otherwise make this fast, but I think you can see what I'm getting at.

3 Likes

Being able to express conformance to protocols for functions would be an absolutely amazing feature.

I'm currently forced to wrap functions in a Function type literally just for this, for example for making functions conform to algebraic structures, like the following:

/// assuming struct Function<Source,Target>

extension Function: Semigroup where Target: Semigroup { ... }

/// in magical future Swift

extension <Source, Target> (Source) -> Target: Semigroup where Target: Semigroup { ... } 
4 Likes