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

`hasproperty()` analogue for optics

Open staticfloat opened this issue 2 years ago • 3 comments

Sometimes I'd like to check beforehand whether an optic is truly applicable to a type; especially when dealing with deeply-nested optics. It would be quite useful if there existed a hasproperty() analogue for optics on a type. I've made a simple version for PropertyLens and ComposedFunction, but I'm not sure about the rest of the optic types. I'm opening this issue so that if someone else is interested, they can carry this to completion.

function Base.hasproperty(x, o::PropertyLens)
    return typeof(o).parameters[1] ∈ propertynames(x)
end
function Base.hasproperty(x, o::ComposedFunction)
    return hasproperty(x, o.outer) &&
           hasproperty(o.outer(x), o.inner)
end

This allows tests such as the following:

using Accessors

struct T
    a
    b
end

x = T(T(1,2), 3)

@show hasproperty(x, @optic _.a)
@show hasproperty(x, @optic _.b)
@show hasproperty(x, @optic _.c)
@show hasproperty(x, @optic _.a.a)
@show hasproperty(x, @optic _.a.a.a)
@show hasproperty(x, @optic _.b.a)

staticfloat avatar Mar 21 '23 04:03 staticfloat

Thanks @staticfloat ! Would be nice to have such functionality. I thing it should however not be a method of Base.hasproperty. One reason is that it can be generalized to handle other lenses like IndexLens and then the name makes less sense. The other reason is that overloads like Base.hasproperty(x, o::ComposedFunction) are type piracy.

jw3126 avatar Mar 25 '23 20:03 jw3126

This function is now available as hasoptic(obj, optic)::Bool in AccessorsExtra. Also, see maybe(optic) there: it's often more convenient than manually handling has/hasn't cases. I think more real-world usage and testing of the interface is needed before inclusion in Accessors proper (or it shouldn't be included here at all).

aplavin avatar Apr 23 '23 00:04 aplavin

I've been using maybe-optics for some time already, and quite like them. They are based on hasoptic check, through I didn't really find myself using this check explicitly. Would be nice to include into Accessors proper at some point, but there are a few questions on the semantics of hasoptic and of maybe. Here's a list of concerns about the "get" part alone:

  • maybe(o)(x) returns nothing when !hasoptic(x, o). So, composability requires hasoptic(::Nothing, _) = false to be able to combine multiple maybe optics. Presumably it's fine to use nothing in this role of sentinel, and I didn't experience any issues with that - but technically it is a limitation for when someone wants to actually operate on nothing. I don't see other alternatives, but may be missing something.
  • It's clear what hasoptic should do on specific optics like PropertyLens, IndexLens and similar. But what to do in the general case?
    • Not define hasoptic(_, _) fallback? Then any optic without a specific hasoptic method wouldn't work with hasoptic/maybe, and the majority of optics won't have it.
    • Define hasoptic(_, _) = true? That's how it's done in AccessorsExtra for now. One can put arbitrary optics into maybe, like maybe(@optic _.a + 1). However, there are cases when hasoptic(x, o) == true but o(x) fails - so maybe(o)(x) does as well. The majority of optics, both in Accessors and user-defined, presumably work on all instances of corresponding types, so true is a reasonable fallback imo.
  • How should hasoptic/maybe treat multivalued optics like Elements() and Properties()? Currently, it just errors.

aplavin avatar Aug 10 '23 13:08 aplavin