"Strict" inout parameters

Problem

Imagine this simple scenario:

protocol Named {
	var name: String { get set }
}

struct Person: Named {
	var name: String = "John Doe"
}

struct Cat: Named {
	var name: String = "Missy"
}

func rename(_ named: inout Named) {
	named.name = "Foo"
}

var person = Person()

// Error: Cannot pass immutable value as inout argument: 
// implicit conversion from 'Person' to 'Named' requires a temporary
rename(&person)

This is because the rename function can theoretically assign a Cat() into the inout parameter and we'd be ending up with a cat inside a person which is just not right. This was previously discussed here: SR-8148 and here: SR-8155

The current solution is this:

var named = person as Named
rename(&named)
person = named as! Person // We simply assume that the type is unchanged

This seems to me as suboptimal and potentially dangerous in case the rename function is from a 3rd party library and can potentially both change in the future and also we have no control over it.

Solution

Introduce strict (or some better name) inout parameters. It would work like this:

  • it would not be possible to re-assign strict inout parameter
  • it would only be possible to call setters and mutating methods (aside from the obviously nonmutating stuff)
  • it would only be possible to pass the parameter to other inouts that are also strict (just like passing a nonescaping closure)

Generally something like:

var person = Person()
rename(&person) // Now OK as the inout parameter is strict

func rename(_ named: strict inout Named) {
	named.name = "Foo" // OK
	named = Cat() // Error - cannot assign strict inout parameter

	otherStrictRename(&named) // OK as the parameter is also marked as strict
	otherNonstrictRename(&named) // Error - passing named to non-strict inout parameter
}

func otherStrictRename(_ named: strict inout Named) {
	named.name = "Bar" // OK
}

func otherNonstrictRename(_ named: inout Named) {
	named.name = "Earl"
}

Any thoughts on this? Are there others who also find the current solution limiting, unsatisfying and un-Swifty?

Maybe your example was just trivial, but couldn't you simply write:

func rename<T: Named>(_ named: inout T) {
  named.name = "Foo"
}

var person = Person()

rename(&person) // ok

to achieve your desired result? Maybe there is some example out there that breaks this, but I would be interested in seeing it.

5 Likes

Yes, this was a trivial example, there are also some in the linked bug reports.

I personally have encountered this in a scenario where I have some prototypes creating closures that are assigning various properties. While it's probably possible to solve my issues with generics as well, I find the suggested solution more straightforward. :man_shrugging:

Though perhaps it's just me :slight_smile:

I don't like 'strict'. Out of context, I would never have guess what a 'strict inout' was.

I prefer using mutating, as it properly define what you want: calling mutating method on the object.

func rename(_ named: mutating Named) {
  named.name = "Foo" // OK
  named = Cat() // Error - cannot assign mutating parameter

  otherStrictRename(&named) // OK as the parameter is also marked as strict
  otherNonstrictRename(&named) // Error - passing named to non-strict inout parameter
}

Or maybe "mutating inout" to make clear the variable must be passed as a reference.

The constraints here really are the same as a generic function, as Alejandro pointed out. That's what generics do: they preserve type information you have at the call site. You can argue that the syntax to define the appropriate generic function could be better, but I don't think a new keyword or a new type kind is the right answer.

2 Likes

I agree with Jordan completely; this entire pitch seems to based around not wanting to write something as a generic function when it really needs to be one.

I think there's a legitimate concern underlying the request here, which is that the generic syntax looks a lot more complicated than the existential syntax. One could imagine generalizing the idea of opaque result types to arguments, to allow you to write:

func foo(x: inout opaque P) { ... }

as shorthand for:

func foo<T: P>(x: inout T) { ... }

which is similar to proposals to extend Rust's impl Trait and C++'s concept-in-type-position features.

6 Likes