Best way to organize code using `self` and context in tandem?

i have a large number of (non-mutating) methods that all look like some variation of

self.computeSomething(context, ... )

the context is immutable, but self gets periodically mutated in between various compute{...} calls.

im continually struggling between organizing it the way it is above, and the alternative:

context.computeSomething(self, ... )

since none of these methods decisively “belong” to the self or context types.

if self and context were both immutable, i would create a new type that contains both, like

struct Tandem 
{
    let local:LocalThing 
    let context:GlobalContext
}

and place the methods under Tandem. but self is mutable, and i’m terrified this will cause copy-on-write issues.

If the methods aren’t naturally part of either self or context…you could just make them top-level functions. This isn’t Java; not everything has to be an object.

Alternatively, you could just make these methods static methods of some namespace-ish type and give them parameters for both context and self (which wouldn’t actually be self at this point but you know what I mean).

1 Like

With given scenario, probably

enum Tandem {

    static func computeSomething(local: LocalThing, context: GlobalContext) -> Result { 
        ...
    }
}

which is actually what Agent is suggesting.

well part of the motivation for organizing by object is because the actual methods have lots of moving parts; there would be four or five additional parameters after context. so introducing a Tandem identifier would just make the calls even bigger in source.

Could you elaborate on what type of COW issues you're anticipating?

combining self and context into temporary Tandem aggregates would retain self and context. context is immutable, so this is not a problem, but self (and its allocations) will mutate between various stages of the computation.

If self is a local variable then isn’t the retain count already 1? For example,

let array = [1] // Already references

func takeArray(_ array: [Int]) {
  _ = array.append(2) // Creates a copy
}

takeArray(array)

Perhaps your methods should take self as inout.

SwiftUI's UIViewRepresentable protocol passes the context as a parameter in methods like makeUIView(context:) and updateUIView(_:context:). This is also true for UIViewControllerRepresentable, NSViewRepresentable, NSViewControllerRepresentable, and WKInterfaceObjectRepresentable.

A benefit of self.computeSomething(context, ...) is that it can be shortened to computeSomething(context, ...).

With that being said, I don't think either approach is wrong.