Accessors.jl icon indicating copy to clipboard operation
Accessors.jl copied to clipboard

What exactly set(obj, f, val) means?

Open aplavin opened this issue 1 year ago • 3 comments

The question may sound strange, but what exactly y = set(x, f, val) means/supposed to mean? The contract specifies that f(y) == val, but what about other parts of the object? Consider this hypothetical scenario:

struct Ellipse1
    major_axis
    minor_axis
end

struct Ellipse2
    major_axis
    minor_major_ratio
end

major_axis(e::Ellipse1) = e.major_axis
major_axis(e::Ellipse2) = e.major_axis

minor_axis(e::Ellipse1) = e.minor_axis
minor_axis(e::Ellipse2) = e.major_axis * e.minor_major_ratio

minor_major_ratio(e::Ellipse1) = e.minor_axis / e.major_axis
minor_major_ratio(e::Ellipse2) = e.minor_major_ratio

What should set(ellipse, minor_axis, val) do: keep the ratio or the major axis constant? Same question for set(ellipse, minor_major_ratio, val): does it keep major axis? Minor? Average?..

Should it be the same or different for Ellipse1 vs 2? If one implements set() based on the implementation (internal fields) of these objects, it will result in a weirdly different set() behavior for two ellipse types that are otherwise treated equivalently.

aplavin avatar Jul 18 '24 18:07 aplavin

I expect from y = set(x, f, val) that it satisfies the lens laws, unless f is some exotic optic. But for a function, I definitely expect the lens laws.

In some cases like f = @o _.major_axis there is a unique natural product decomposition of the object and then I expect that decomposition to be respected.

In other cases like your examples, there is no unique natural decomposition. In these cases, it is best to make clear what is happening, by documentation, comments, more explicit names.

In your example, I would have no expectation of what minor_major_ratio does exactly. For minor_axis I would expect that it respects the major_axis. But it is a bit subjective to consider ellipse = minor_axis x major_axis more natural than ellipse = major_axis x minor_major_ratio.

jw3126 avatar Jul 18 '24 18:07 jw3126

I expect from y = set(x, f, val) that it satisfies the lens laws

Yeah, that part is clear – but I don't think lens laws specify what happens to the part of x "orthogonal" to f.

In some cases like f = @o _.major_axis there is a unique natural product decomposition of the object

With fields/properties/collection indices there's this natural decomposition, true. I think Accessors do a good job at preserving it!

Maybe, the only right answer here is that one needs to think carefully when defining function setters, to ensure there's a natural "orthogonal" decomposition. I found myself with a more involved case of this "ellipse example", where I defined several setters to do whatever appeared natural at different times, and found out they aren't quite consistent.

Unfortunately, doesn't seem like there is a way to programmatically test this kind of properties, they are hard to even define in text. Do you happen to know of solutions in "stricter" languages like Haskell/Scala/... ?

aplavin avatar Jul 18 '24 19:07 aplavin

I don't know about other languages. One approach to make the decomposition used obvious would be something like:

@set Ellipse2(ellipse).minor_major_ratio = ...
@set Ellipse1(ellipse).minor_radius = ...

More mathematically this makes explicit the isomorphism used between typeof(ellipse) and a type for which the decomposition is obvious.

jw3126 avatar Jul 18 '24 19:07 jw3126