SQL-like "trigger" for arrays within a struct

Is there such a thing as a SQL-like “trigger” for arrays within a struct in Julia? I have an immutable struct, foo, that stores lots of pieces of information which get consumed by other functions. Two of the elements of foo are “entangled.” The first piece is “data” (a nxm array that’s input by the user) and the second piece is “fcalc”, a nx(m-1) array that’s calculated by the code. Imagine something like this:

struct foo
    ...
    data
    fcalc
    ...
end

fcalc is simply the columns of data divided by the preceding columns of data, and to construct foo I do something like:

function create_foo(..., data,...)
    ...
    for j = 1:(size(data, 2) - 1)
        for i = 1:size(data, 1)
            fcalc[i, j] = data[i, j + 1] / data[i, j]
        end
    end
    ...
    return foo(..., data, fcalc,...)
end

foo is immutable but the elements of data are mutable, which is desirable, however, if data gets modified then fcalc needs to be updated. How can I ensure that happens? Is there a way to trigger the update of fcalc when an element of data is modified? Is it possible for fcalc to be a “calculated view?” Everything I’ve tried with view, @view, and @views has failed. Any suggestions or options I should explore?

There are a few ways you could refactor this code. Perhaps none are “fully automatic”, but I’d encourage you to consider each one in the wider context of your code.

  1. Don’t precompute fcalc.
struct Foo
    ...
    data
    ...
end

fcalc(foo::Foo, i, j) = foo.data[i, j + 1]/foo.data[i, j]
  1. Implement a “setter” for data.
struct Foo
    ...
    data
    fcalc
    ...
end
function set_data!(foo::Foo, data)
    for j = 1:(size(data, 2) - 1)
        for i = 1:size(data, 1)
            foo.fcalc[i, j] = data[i, j + 1] / data[i, j]
        end
    end
    foo.data .= data
end

You can even allow foo.data = x to call set_data!(foo, x) with

Base.setproperty!(foo::Foo, name::Symbol, x) = name === :data ? set_data!(foo, x) : setfield!(foo, name, x)

though this style is arguably less explicit, and direct mutation like foo.data[i, j] = x would not update foo.fcalc.

  1. Roll your own lazy array type.
struct FcalcMatrix{T} <: AbstractMatrix{T}
    data::Matrix{T}
end

Base.size(fcm::FcalcMatrix) = size(fcm.data, 1), size(fcm.data, 2) - 1
Base.getindex(fcm::FcalcMatrix, i, j) = fcm.data[i, j + 1]/fcm.data[i, j]

struct Foo
    ...
    data
    fcalc
    ...
    Foo(..., data, ...) = new(..., data, FcalcMatrix(data), ...)
end

This last option is most similar to your attempts at using views, I think. The advantage is that you can do array operations with fcalc like matrix multiplication or aggregation as if it were a normal array (unlike option 1).

1 Like

The correct answer is probably one of the ones in the above response, but if you are ok with a dependency, this is a neat library that is used extensively to make interactive plots with Makie:

Thank you both for the suggestions! Option #3 and Observables.jl both look very promising. I think I need to do some benchmarking to really understand what’s best.