Default `inout` values and/or consuming `inout`?

I occasionally come across situations where inout is just a formality. SystemRandomNumberGenerator is one such case in the standard library. It stores nothing and its initializer does nothing, yet its methods are mutating to satisfy the requirements of RandomNumberGenerator.

I think it would be nice if it were possible to skip the inout ceremony when you don't need the inout argument after a function call. Swift recently added the consume keyword, which has similar semantics. Imagine something like this:

UInt8.random(in: 0...255, using: consume SystemRandomNumberGenerator())

This particular example is still a bit of a mouthful, but notice how we skipped the usual local variable binding. Perhaps a more compelling example is using this syntax to declare default inout values:

func random<RNG: RandomNumberGenerator>(
    using randomness: inout RNG = consume SystemRandomNumberGenerator()
) -> UInt8 {
    randomness.next()
}

I suspect most agree that default values are convenient, but there's currently no way of specifying default inout values. Instead, you must define separate functions that perform the inout ceremony. A consuming inout syntax would bridge this gap.

5 Likes

The API of RandomNumberGenerator is correct to require inout/mutating, because state mutation is typically required to implement it. The “out” is a newly mutated number generator.

Just because an implementation like SystemRandomNumberGenerator doesn’t actually (visibly) mutate state today, doesn’t mean it’s correct to bake that assumption into your code, as it’s valid for that to change in a library. For this particular struct, adding a stored property is unlikely since it’s frozen, but I’m speaking in general.

So I generally think the proposed shorthand, where a fresh instance is passed every time to an inout, isn’t a pattern that should be encouraged. If this particular frozen struct is annoying, I think writing an extension with alternate functions that hide the extra var binding at least makes it clear that the “out” state is thrown away after each call. I can imagine people believing the state of the default input argument is shared between all calls that don’t provide that argument (as if it were global).

6 Likes

I agree that default inout values may require deliberation when other kinds of default values don't. I also don't think defaulting empty structs is the only use case; I only posit that such bindings are commonly dropped. A function with inout argument is in many ways similar to a function returning multiple values, but you can only drop bindings you don't care about in the latter case. The inout tax is un-Swift-y, in my opinion. I suspect the most common use case would be something index-related. Imagine something like this:

extension Collection {

    /// Returns a thingy or nil.
    ///
    /// The index is set to the end of the first thingy in
    /// the given suffix or the first non-thingy element.
    ///
    func thingy(from index: inout Index) -> Thingy? { ... }
}

// short, sweet (thanks to consuming inout convenience)

let thingy = source.thingy(from: consume source.startIndex)

// and powerful

var index = source.startIndex
var thingies: [Thingy] = []

while let next = source.thingy(from: &index) {
    thingies.append(next)
}

if  index != source.endIndex {
    print("\(source) is not a proper thingy sequence!")
}