Chaining struct-mutating funcs


(Fritz Anderson) #1

Swift 3 as of Xcode 8.0b4

TL;DR: I have a struct value type backed by a copy-on-write mutable buffer. You use it to perform arithmetic on the buffers. The most expressive way to do this efficiently is to chain the arithmetic operators so each mutates the same buffer. Swift does not like to chain mutating operators — it treats the result of each step as immutable, so you can’t continue the chain. I can’t argue; the syntax apparently can't express anything else.

All the alternatives I see are ugly-to-dangerous.

Have I missed something, I hope? Please make a fool of me.

  — F

The details of my use case or implementation are off-topic; even if mine are ill-considered, surely apt ones exist. Unless you can demonstrate there are none.

The vDSP_* functions in Apple’s Accelerate framework are declared in C to operate on naked float or double pointers. I decided to represent such Float buffers in Swift by a struct (call it ManagedFloatBuffer) containing a reference to a FloatBuffer, which is a final specialization of class ManagedBuffer<Int, Float>.

(The names are a work-in-progress. Just remember: ManagedFloatBuffer is a value type that can copy-on-write to a reference to FloatBuffer, a backing store for a bunch of Floats.)

The nonmutating funcs:

    func subtract(_ other: ManagedFloatBuffer) -> ManagedFloatBuffer
    func subtract(_ scalar: Float) -> ManagedFloatBuffer

are straightforward. They return new ManagedFloatBuffer values. You can chain further calls to simplify a complex calculation that is neither intricate nor tied up in temporaries:

    let sum²OfResiduals = speeds
        .subtract(cameraSpeed.mean)
        .multiply(feetToMeters)
        .sumOfSquares

Great. And vDSP gets you about a 40% boost. (The compiler itself seems to do a pretty good job of auto-vectorizing; the unoptimized code is a couple of orders of magnitude slower.) But as you chain the immutables, you generate new FloatBuffers to hold the intermediate results. For long chains, you end up allocating new buffers (which turns out to be expensive on the time scale of vectorized math) and copying large buffers into them that you are about to discard. I want my Swift code to be as performant as C, but safer and more expressive.

So how about some mutating functions to change a ManagedFloatBuffer’s bytes in-place (copying-on-write as needed so you can preserve intermediate values)?

    mutating func reduce(by other: ManagedFloatBuffer) -> ManagedFloatBuffer
    mutating func reduce(by scalar: Float) -> ManagedFloatBuffer

These return self, because I’d hoped I could chain operators as I did with the non-mutating versions.

The compiler doesn’t like this. It says reduce(by:) returns an immutable value, so you can’t chain mutating functions.

(I can see an issue in that when the first func's self is copied as the return value that is used as the second func’s self, that could make two surviving references to the same buffer, so a buffer copy would happen when you mutate the second func’s self anyway. I’m not sure the compiler has to do that, but I can see how it might be hard to account for otherwise. Hey, it’s a tail call, right? SMOP, not source-breaking at all.)

StackOverflow invites me to eat cake: Make the mutable operand inout to funcs I call one by one. Something like:

    multiply(perspectiveCorrections, into: &pixelXes)
    sin(of: &pixelXes)
    multiply(pixelXes, into: &speeds)
    multiply(feetToMeters, into: &speeds)
    subtract(cameraSpeed.mean, from: &speeds)
    let sumSquaredOfResiduals = speeds.sumOfSquares

    // grodiness deliberately enhanced for illustration

I’d rather not. The thing to be calculated is named at the bottom of the paragraph. The intermediate steps must preserve names that change meaning line-by-line. You have to study the code to recognize it as a single arithmetic expression.

And by the by, if a vector operand is itself the result of a mutating operation, the dependency graph becomes a nightmare to read — I can’t be sure the illustration even expresses a plausible calculation.

Thinking up more reasons to hate this solution is a fun parlor game you and your family can play at home.

Strictly speaking, the compiler is right: I don’t see any language construct that expresses that a returned value type that may be mutated by a chained func. Am I correct?

I’m not at all happy with turning ManagedFloatBuffer into a class. Intuitively, this is a value type. Passing a packet of Floats into a func (or into another thread, as one does with math) and finding your Floats had changed in the mean time is… surprising.

I’m not optimistic, but I have to ask: Is there a way to do this — to take mutability down an operator chain?

  — F