Single-element labeled tuples?

Continuing the discussion from More consistent function types:

What are the reasons that single-element tuples are forbidden from the language?

For example, tuple labels can help clarify the role of a single discardable return value.

var counter = 1
var global: UInt = 0

@discardableResult
func bar(_ x: UInt) -> (counter: Int) { // error: cannot create a single-element tuple with an element label
    global += x
    defer { counter += 1 }
    return counter
}
11 Likes

Was it just to avoid the complexity that comes about from resolving ambiguities?

If so, would it be enough to go the Python route and make it so 1-tuples have to be explicit? i.e (1,)

Well the single labeled tuple was bugged from the beginning and the literal was probably removed after I had a short chat with @Joe_Groff on Twitter a couple of years ago. I could successfully find that conversation.

Another use-case, single-element tuples can be useful when you prepare for having more elements in the future.

typealias Options = (option1: String)

And you plan to end up with this:

typealias Options = (option1: String, option2: URL, option3: Number)

I realize this could be a struct, but I prefer the simplicity of tuples.

My current dirty workaround is to just have a moot element:

typealias Options = (option1: String, 🐐: Int)
6 Likes

This seems like a workaround for creating a struct... Why not go with:

struct Options {
    let option1: String
}

instead? In the same module, you get the initializer automatically and you can add more functionality to it (protocol conformance, etc.)...

1 Like

Here is one reason against the struct, it would require a special rule for disambiguation though.

class SomeClass {
  var content: (offset: CGPoint) { ... }
  var content: (size: CGSize) { ... }
}

someInstance.content.offset.x = 42
someInstance.content.size.width

You can have a subclass and override these members with observable functions like willSet/didSet. Whenever one of these members is mutated you want get unwanted notification on the other member. IMHO a struct per single labeled tuple is simply an overkill. We use tuples where we do not want any conformances to protocols or extra functionality.

This could be annoying, because it would break some of my expectations.

What if I wanted to do:

a.content = b.content

I think it would have to be disallowed, but it wouldn't be obvious from the callsite.

It's a bit more code, but I would have preferred:

class SomeClass {
  var content: Content
  var offset: CGPoint { get { return content.offset }; set { content.offset = newValue } }
  var size: CGSize { get { return content.size }; set { content.size = newValue } }

    struct Content {
        var offset: CGPoint
        var size: CGSize
    }
}

I think this could be mitigated if there was something like C#'s expression bodied members.

class SomeClass {
  var content: Content
  var offset: CGPoint => content.offset // (bad) strawman syntax
  var size: CGSize => content.size

    struct Content {
        var offset: CGPoint
        var size: CGSize
    }
}

I'm sorry, but I don't see the usefulness in your example - it seems like a confusing hack that should be handled the way @AlexanderM has shown.

The usage of a single-label tuple has been narrowed down in this thread to these two:

  • returning something that may be extended in the future. Then I don't think a tuple is a way to go - you want a structured return value...
  • further clarifying return value's role. In this case the method/var may not be well-named. In any case, I would find it more confusing to finding out the return value wrapped in a tuple.

To be clear, I'm not opposed to single element tuples. I don't see a reason why we should break consistency and special case them like this. We have 0 element tuples, 2+ element tuples, but not 1 element tuples. It's odd.

I just don't like that particular use @DevAndArtist suggested

3 Likes

No, the motivation I mentioned is a discardable return value, which wouldn't typically be reflected in the name (since it's discardable).

2 Likes

First of all offset and size should not be direct members of SomeClass. Then using a Content struct does not allow you to observe the size and offset values independently on a subclass of SomeClass. Content cannot be a struct because in reality it would probably contain even more members like inset, size, offset, layoutGuide etc. If it was a struct it would mean that whenever a single portion of the struct is mutated the whole struct will be copied, which would be pretty expensive.

var content: (offset: CGPoint) was just to showcase that you could use a single labeled tuple to allow independent mutation and observation in subclasses.

class SubClass : SomeClass {
  override var content: (offset: CGPoint) {
    didSet { /* do something only when content.offset has changed */ }
  }

  override var content: (size: CGSize) {
    didSet { /* do something only when content.size has changed */ }
  }
}

That should look like this:

a.content = b.content.size // will assign to `(size: CGSize)`
// or
a.content = b.content.offset // will assign to `(offset: CGPoint)`

// or more precisely
a.content.size = b.content.size 
// or
a.content.offset = b.content.offset

This is consistent with var tuple: (x: Int, whatever: String) = (42, "Swift")


Anyway this would require a special rule that I previously mentioned but did not described. That rule would require us to allow overloading constants and variables by it's type, which is current not supported or may never be supported in first place. This was simply all theory. ;)

I've never really felt a need for such a thing

Say we have something like this:

class RootController: UIViewController {
    var data: (Int, Int)?
    func present() {
        let destination = DetailController()
        destination.data = data
    }
}

class DetailController: UIViewController {
    var data: (Int, Int)?
}

I might prefer to use tuple instead of struct in this case. Because currently I can use destination.data = data but if I change the type of one data tuple (e.g. destination.data is now (Int, Int, String)), the Compiler can still emit an error to alert me.

Single tuple with name can make this pattern generic while still providing a declarative name. I have a specific use case here (Resource/README.md at master · TintPoint/Resource · GitHub). Please check out the last DataRecevingController example. With single tuple it’s possible to write self.data.text instead of self.data.

Ok, I see your point now, though it still seems like a hack to me that can be easily done by something like:

class Foo {
	
	struct Content {
		var offset: CGPoint = .zero
		var size: CGSize = .zero
	}
	
	var content: Content = Content() {
		didSet {
			if oldValue.offset != self.content.offset {
				// Offset changed.
				print("offset")
			}
			if oldValue.size != self.content.size {
				// Size changed.
				print("size")
			}
		}
	}
	
}

let foo = Foo()
foo.content.offset.x = 6.0 // offset
foo.content.size.width = 20.0 // size
foo.content = Foo.Content(offset: CGPoint(x: 3.0, y: 5.0), size: CGSize(width: 20.0, height: 10.0)) // offset + size

If has the disadvantage of requiring the subparts to be Equatable, but I don't see that as much of a limitation. This way you can also replace the entire Content and get notified about both...

1 Like

Yes, I fully understand and support usage as such, though in this case I'd be more inclined to fixed-size arrays that have been discussed on this forum a few times - i.e. something link var data: Int[2]... Note that I've never said that tuples should not be used at all, nor have I said that I'm against single-element tuples - just that the examples raised here did not convince me about the importance of them.

@Michael_Ilseman - sorry, I missed that part, which is quite important in your argument. My apologies. I kind of see the point in using the single-element tuple here.

Since we can have n length tuples, including 0 length, it is weird that a length of 1 is excluded. I vote to include this.

5 Likes

I agree, as a humble end user without knowing anything about the compiler.

It seems like a weird special-case that spreads ripples of complexity through the implementation of the compiler and, if so, that should imho outweigh any concerns about possible use cases for labeled single-element tuples, and the question should perhaps rather (have) be(en): Are special casing labeled single (and zero?) element tuples worth the complexity?

If it would simplify the implementation (and the concept of tuples), I would even think labeled zero-element tuples would be fine, even if nobody would ever use them.

It would of course be more interesting to hear what people with insight into programming language theory had to say about this

I'm also keen to see this feature, for both the @discardableResult case, and tuple values which are to be extended in the future.

Rather than restore the feature exactly as it was, I'd advocate keeping single-element tuples distinct from the raw type.

This would prevent this questionable feature: value.0.0.0.0; and presumably eliminate a source of compiler bugs.


To enumerate the possibilities:

//disabled - no `anyValue.0`
let tuple = 1 //clearly no intent to cast
let tuple = (1) //collides with regular brackets
let tuple = 1.0 //I don't even…

//allowed (?)
let i: Int = tuple //1, 2b
let tuple = 1 as (label: Int) //1, 2a
let tuple: (label: Int) = 1 //1, 2a
let tuple = (label: 1) //1, 2(a|b|ab)
//allowed (3)
let i: Int = tuple.label
func tupleReturning() -> (label: Int) {return 1}
func tupleReturning() -> (label: Int) {return (label: 1)}
  1. Allow all use cases, and simply disable the equivalence to the raw type (which is out of the question at this stage anyway).

  2. Re-enable single-element tuples as a separate type, with one/both restrictions:
    a. require accessing .label to convert (label: Int) to Int
    b. require (label: 1), by disabling conversion through type inteference.

  3. Re-enable only the last set of use-cases, or a subset with restrictions a/b, and restrict construction of single-element tuples to @discardableResult functions. The more conservative, but restrictive, approach.

Personally, I'm a fan of 2a, as it works nicely with use in discardable functions, and isn't overly restrictive.

3 Likes

Seems self-evident to me that single-element tuples are a good thing.

Because of the label, its more useful than a one item array. I wouldn't want an array where the count can't be one. I don't like messing around with my code to accommodate surprising rules about tuples either. It's ugly, and it's likely more cognitive load for new Swift devs than confusion over the parens would be.

5 Likes

They were banned because the "classic" function-argument-tuple model and tuple conversion rules caused type system problems, particularly because every T was implicitly convertible to (label: T) and back. We've since shored up the argument label model, and various people have discussed limiting the tuple conversion rules to be less unpredictable and less straining on the type checker. If we solve that problem, then there's no technical reason remaining for single-element tuples to be banned.

7 Likes