Why aren't default arguments allowed for optional inout parameters?

func makeThing(intermediaries: inout [XYZ]? = nil) -> Thing { … }

:x: Cannot provide default value to inout parameter "intermediaries".

I suspect it's because it's misinterpreting that (w.r.t. my intent) as inout Optional<[XYZ]> instead of Optional<inout [XYZ]>. Is there a way to achieve the latter?

I can work around this by manually creating overloads of the function, but that's of course tedious, complicates auto-complete, is more work to maintain, and triggers my Java PTSD.

func makeThing() -> Thing {
    var hmm: [XYZ]? = nil
    return makeThing(intermediaries: &hmm)
}
1 Like

If that was allowed I'd expect to call it with nil:

makeThing(intermediaries: nil)

which is impossible: inout wants a readable and writeable location. Note that in your further example:

    var hmm: [XYZ]? = nil
    return makeThing(intermediaries: &hmm)

hmm is getting assigned (to either .none or .some) and then getting lost... As a workaround that's probably good unless you consider assigning XYZ and dropping it non-optional (if it is non-optional you'd probably want to have the split at the "makeThing" level).

Yes!

Not directly... A workaround coud be to use UnsafeMutablePointer. Bonus point - you won't have to create and then drop XYZ:

func makeThing(intermediaries: UnsafeMutablePointer<[XYZ]>? = nil) -> Thing {
    if let intermediaries {
        intermediaries.pointee = []
    }
    ...
}

Another workaround is to return a tuple.

1 Like

It doesn't have to be - the real makeThing can check if it's nil and not modify it if so.

Huh, that's not bad… it's not great that it employs an 'Unsafe' construct (although, safely as far as I can tell…?), but otherwise seems to work.

Right, but in cases where it's expensive to provide the optional value, that's not great - you then have to do things like have a parameter for includeIntermediariesInResult: Bool = false and the tuple has to contain an [XYZ]?. Doable, but similarly awkward to the overload workaround.

1 Like

You mean this setup?

func makeThing(intermediaries: inout [XYZ]?) -> Thing {
    if intermediaries != nil {
        intermediaries = .some([XYZ()])
    }
    ...
}

var null: [XYZ]? = nil
makeThing(intermediaries: &null) // stays .none
var xyz: [XYZ]? = []
makeThing(intermediaries: &xyz) // reassigned

It does look a bit strange, similar to, say:

var i = 0, j = 1
foo(&i) // i is not changed
foo(&j) // j is changed to 42

but, if it works for you so be it.

Yes... I wish we had something similar without "Unsafe" connotations, like a subset of UnsafeMutablePointer that only allows working via "pointee" and doesn't allow jumping to arbitrary offsets within the pointer to be truly safe.

Indeed.

Maybe it would be OK to only allow “nil” as default value here, because any other default value would not really be “inout” (no change to the outside possible)?

Right, that's my thinking. The nil (.none) case is special (and particularly useful).

I get why non-optional or non-nil defaults arguably don't make sense (they could be defined as essentially globals, but Python does that and it's shown us that it's a footgun).

It makes more sense if an inout parameter was framed as "a binding to a value of type X" instead of what I see in learning materials as "parameters by default are let, use inout to change them to var".

What would a nil binding even considered to be? (besides almost introducing first-class nil pointers...) That's why the UnsafeMutablePointer<X> workaround makes sense.

An optional inout is already allowed (think: I might give you something from the outside to change, but maybe not), the question is, would then a default “nil” be useful. I think yes.

inout as a binding of an optional type is not the same as an optional binding. I can see where this is useful in the above example of wanting to possibly perform some side effects to a binding.

However nil doesn't feel right here for bindings. I would probably think about something using underscore _ that already has some semantics with variable bindings:

let _ = foo()
for _ in 0 ..< 123 { }
// closures that don't use their argument(s) must at least have "_ in"

// possibly

func bar(x: inout Int = _) { } 

// or to keep the same pattern

func bar(x: inout Int = &_) { } 

However that's probably syntactically too small.

Manual inout implementation without "Unsafe":

typealias XYZ = Int
typealias Thing = Void

class OutputArgument<Pointee> {
    init(_ pointee: Pointee? = nil) {
        self.pointee = pointee
    }
    var pointee: Pointee?
}

func makeThing(intermediaries: OutputArgument<[XYZ]>? = nil) -> Thing {
    intermediaries?.pointee = [XYZ(1), XYZ(2), XYZ(3)]
    return Thing()
}

makeThing() // pass nil
makeThing(intermediaries: nil) // ditto

// pass argument
let argument = OutputArgument<[XYZ]>()
makeThing(intermediaries: argument)
print(argument.pointee) // Optional([1, 2, 3])

Note that I didn't have to create XYZ array (even empty) when passing parameter "in", as it's value is not used inside.

let argument = OutputArgument<[XYZ]>()

It uses "class". Off the top of my head I don't know how to do this with a struct without resorting to either UnsafePointer and/or memory allocation.

The following version doesn't use "visible" memory allocation or classes:

// DO NOT USE THIS!
struct Handle<T> {
    private var pointer: UnsafeMutablePointer<T>
    init(_ pointer: UnsafeMutablePointer<T>) {
        self.pointer = pointer
    }
    var value: T {
        get { pointer.pointee }
        nonmutating set { pointer.pointee = newValue }
    }
}
extension Int {
    mutating func handle() -> Handle<Self> {
        withUnsafeMutablePointer(to: &self) { b in
            // NOTE: DO NOT DO THIS!
            Handle(b)
        }
    }
}
func makeThing(intermediaries: Handle<Int>? = nil) -> Thing {
    intermediaries?.value = 42
    return Thing()
}

Usage:

makeThing() // no result needed
var x = 0
makeThing(intermediaries: x.handle())
print(x) // 42

Could we have something similar to this (no memory allocation/classes), but safe? (As written the above is VERY unsafe as it stores the withUnsafeMutablePointer closure parameter and uses that pointer outside of the closure, so it works only by luck).