Unsure about properties

I like the syntax of properties, like that they’re not the same as fields, and that I don’t have to export them. I’m considering switching from using accessor functions to properties in a large chunk of code, but I’m not sure it’s a good idea.

Suppose I have struct S end.

  1. Is it okay to use underscores as property names? S()._ is currently valid syntax.

  2. Should we be allowed to define properties for Type{S}? This would be useful in, for example, accessing parameters in parametric types (like Array{Int64, 2}.dims). Currently, I can define S._ by overwriting Base.getproperty. The issue is, types already come with these property names (:name, :super, :parameters, :types, :instance, :layout, :hash, :flags) and I don’t know that it’s gaurenteed that they will not change in the future. I don’t think I can redefine Base.getproperty from inside another funtion, so I would need a macro? I’d need to make sure that the existing names don’t conflict the user-defined names. But then also make sure that I’m able to update the definition of properties that I myself defined.

  3. What if I redefine S.name?

  4. If I should be allowed to define properties of types (S._), what’s a good way to update Base.propertynames?

  5. What happens if I don’t update propertynames? I figure it’s a good way to keep track of what properties a type or its subtypes is supposed to implement. Which brings me to:

  6. What’s a good way to verify that a list of properties have been implemented for a type? This is my current approach:

Base.getproperty(s::S, name::Symbol) = getproperty(s, Val(name))       # dispatch on Val{name}
Base.getproperty(s, ::Val{name}) where {name} = Base.getfield(s, name) # default: pass to getfield
Base.getproperty(s, ::Val{:_}) = "underscore" 						   # define property
hasmethod(getproperty, Tuple{S, Val{:_}}) 							   # verify: do this for all properties
  1. What are other things to consider when choosing between (accessor) functions and properties?

I don’t think they’ll change anytime soon, but technically those are internals, and custom field names that don’t conflict with them cannot be public because they can be broken arbitrarily.

You’d break a lot of Base code, like in reflection.jl.

Not sure what’s better than manually mirroring getproperty and propertynames methods, maybe some slick metaprogramming to make one method’s definition redefine the other. It’s not just a matter of checking name literals in the method, properties are technically an instance-wise characteristic, not one of the type. Note how propertynames works on instances and is documented so, while fieldnames works on types.

1 Like
    1. Sure, you can use _ as a field name so I don’t see why you couldn’t use it as a property name. Either case is kind of user unfriendly though, since property destructuring, (; _) = S(), puts your field into a write-only variable.
  • 2-4. Don’t mess with properties of types you don’t own. (S is yours, but not typeof(S)).
    1. The most obvious consequence is that you can’t tab-complete your properties in the REPL.
    1. You need to be very careful with only using getfield to access your real fields inside your getproperty and setproperty! implementations, to avoid performance traps.
2 Likes

Probably a bad idea. I’d swear there’s an issue in the context of #24990 where some argue that S()._ being allowed is a bug, an oversight from when _ variables became forbidden before 1.0, and it might be “fixed” if #24990 ever gets merged.

I think the short answer is: don’t do it.

The more technical answer: as I understand, even if you own S you don’t own Type{S} and the usual piracy rules apply: you’re free to define getproperty(::Type{S}, ::T) where T is a type you own, but that’s largely useless, what you typically want is the case T == String or T == Symbol and that would be type piracy and it will cause problems.

1 Like

Did you mean getproperty(::Type{S}, ::T) here? getproperty(::S, ::Symbol) would not be type piracy, right?

I think it’s starting to sink in that I don’t own Type{S} because I don’t own Type.

1 Like

Thanks, fixed.

You could consider a wrapper around Type{S} that implements the properties you are interested in.

Wrapper as in a wrapper function, right? (still learning programming lingo). I think that’s what I have right now, mostly to get parameters from parametric types. So if S is parametric like S{D}, I have things like dims_of(::Type{S{D}}) where {D} = D.

Could you tell me more about these performance traps? Is there even a way to access fields (not properties) except for getfield? I thought I could do arbitrarily large computations inside getproperty and it gets complied just like any other function.

I know that things can get recursive if I use the dot syntax inside the definition of getproperty, but I haven’t come across talk of any performance issues. Maybe you’re refering to cases where there are lots of properties with lots of if/else branching inside the definition of getproperty. I’m planning to avoid if/else altogether using dispatch as in my example above. So I would be using getproperty inside the definition of getproperty without it being recursive, and without all the branching. I’ll expand on the example here:

Say I have coordinates x and y as fields of my struct S.

struct S
x
y
end

Then, I would define

Base.getproperty(s::S, name::Symbol) = getproperty(s, Val(name))          # dispatch on Val{name}
Base.getproperty(s::S, ::Val{name}) where {name} = Base.getfield(s, name) # default: pass to getfield, works for existing fields unless explicitly overwritten. So s.x, s.y works.
Base.getproperty(s::S, ::Val{:dist}) = hypot(s.x, s.y)                    # define new property that's not a field
hasmethod(getproperty, Tuple{S, Val{:dist}})                              # verify: do this for all properties

The only other issue I’ve seen mentioned namespace management, and I don’t know if I need to worry about that too much. I can’t quite see how properties would be problematic.

struct SType
    S::Type{S}
end
function Base.getproperty(ST::SType, s::Symbol)
    if s in propertynames(S)
        return getproperty(S, s)
    end
    return getproperty(ST, Val(s))
end

This throws UndefVarError: S not defined, field annotations unknowns must be from the type definition’s where clause.

I think @mkitti assumed here that S and getproperty(ST, Val(s)) are already defined. And it makes sense:

struct S
end
struct SType
    S::Type{S}
    SType() = new(S)
end
Base.getproperty(::SType, ::Val{:proper_tea}) = nothing
function Base.getproperty(ST::SType, s::Symbol)
    if s in propertynames(S)
        return getproperty(S, s)
    end
    return getproperty(ST, Val(s))
end

SType().name         # pre-defind
SType().proper_tea   # user-defined
SType().non_existent # error
2 Likes

10 posts were merged into an existing topic: Is dispatch on another type’s parameter (::NotMyType{MyType}) piracy?