Getproperty, decorations, inheritance in 0.7


#1

I wanted to ask whether there is a sensible way of decorating functions.

That is, if I have a function f(x)=false, then I can clearly define g(x)=(x==42?f(x):true), and hence have decorated f. However, I want to do this within the same function.

This is motivated by https://discourse.julialang.org/t/composition-and-inheritance-the-julian-way/11231/9.

To repeat:

struct foo1 <:AbstractFoo
x::Int 
end
struct foo2 <: AbstractFoo 
parent::foo1
y::Int 
end

Now, I would of course like to set Base.getproperty(a::foo2, ::Val{:x})=a.parent.x, but for some reason getproperty passes its argument by value, instead of value-type. And defining one getproperty-method with a huge switch sounds very unappealing. So, what I’d like to do is to chain: I need to preserve the old getproperty method, check whether someone asked for :x, and otherwise fallback (via world-age?) to the automatically defined getproperty functions from the struct definition.

Or, in other words: Julia has beautiful type-based multiple dispatch, and getproperty wants to be resolved at compile-time anyway. Why is this not used for getproperty? Why is it taking the symbol instead of a val-type and then relies on IPO / constant propagation? Can’t we do something like “attempt valtype if present in the method table, otherwise take user-defined fall-back for dynamic resolution”, in order to allow users to avoid hand-writing giant switch statements?


Composition and inheritance: the Julian way
#2

Maybe use this pattern: https://github.com/JuliaLang/julia/pull/24960#issuecomment-349921490


#3

You can lift the argument to type domain, for example:

struct Foo
    x::Int
end
Base.getproperty(foo::Foo, s::Symbol) = bar(foo, Val(s))
bar(foo::Foo, ::Val{T}) where {T}  = getfield(foo, T) # fall back to getfield
bar(foo::Foo, ::Val{:y}) = println("Hello, world!")
julia> foo = Foo(4)
Foo(4)

julia> foo.x
4

julia> foo.y
Hello, world!

Edit: Which is what Mauro linked to.


#4

Thanks!
So the canonical way to add fields to a type is to do this?

Base.getproperty(x::foo2, s::Symbol)=Base.getproperty(x::foo2, Val(s))
Base.getproperty(x::foo2, ::Val{:y})=Base.getfield(x, :y);
Base.getproperty(x::foo2, ::Val{:parent})=Base.getfield(x, :parent);
Base.getproperty(x::foo2, ::Val{T}) where T =Base.getproperty(x.parent, T);

which needs a tiny bit of boilerplate for every new field, but compiles entirely away in simple examples. But most importantly, the amount of boilerplate does not scale in the number of inherited fields, only in the number of new fields.


#5

It might be “safer” to delegate to your own function (i.e. my bar), if nothing else it is useful to avoid ambiguities.


#6

Why would you think that? Because it doesn’t try to construct a type just to call a method, it can be just as fast with runtime-dispatch as at compile-time. The same would not have been true in reverse.

val-type is just a crappy (imho) version of constant propagation. Now that the compiler can do proper constant propagation, it’s just excess line-noise

The canonical way to add a field is not use getproperty, it’s to add an actual field. This function exists for cases such as JuliaDB (which prefers this syntax over [] for referencing column names) and PyCall (other other language interop). In both cases, wrapping it in Val would be a hindrance to easy, performant, generic coding.


#7
  1. But how am I supposed to tell the compiler which constants to propagate in 0.7? What are the rules for which constants propagate how far? Is there some macro to tell the compiler to specialize on a constant? (these are honest questions, and a link to somewhere would be cool)

  2. Types and dispatch are an awesome way of writing switch statements that can be resolved at compile-time. Even if constant propagation is capable of optimizing the switch away, it is really convenient to express switch statements by dispatch/types.

  3. Getproperty via val-type is also cool for @generated stuff. For example, if my getproperty involves offset calculations; say, I want to work with (pointers/refs to) weirdly packed structs or unions, for C interopability. Then my getproperty wants to be @generated and wants to know the desired field at compile-time. And a @generated giant switch statement looks like a lot more work to write and maintain than having a small bunch of @generated get/set functions.

But the example where people want to use e.g. dot-dicts is compelling: This would produce overhead with val-types. But mauro3’s variant of lifting to the type domain looks fine to me?


#8
  1. You couldn’t do any of those things before for Val, I don’t know why it should suddenly become important now for everything else. Let’s suffice to say that it’s now capable of much better inference in the absence of a Val wrapper to get in the way.

  2. Huh? Dispatch is not a switch statement. And if it’s resolved at compile time, I’m not sure that’s really a switch statement either. The compiler recently improved it’s ability to convert dispatch into switch statements. But it’s much better at removing actual switch statements based on discovered constants than it is at inserting switch statements extracted from Val-based dispatch.

  3. Sounds like a feature to me :). You’re mixing many concepts here and not making total sense. It sounds most like you want a macro that helps declare and manage safe C-interop, but are misunderstanding how all these concepts are intended to be used and are trying to hammer nails with a keyboard and a sledgehammer. :slight_smile:

I’m not sure what your definition of “fine” is. It is valid code. It will work. It’s not canonical, and thus would generally not be accepted into a base PR. But there’s no “code police” to make you obey my preferences or anything in your own code. Is that fine?


#9

Perhaps another way of explaining the constant propagation versus Val issue might be helpful:

  • Regardless of Julia version, Val{x} is no more or less statically predictable than x: if you can specialize code for one then you can do it for the other—they’re pretty clearly equivalent.

  • Val{x} was introduced as a way to hint to the compiler that it should try to specialize on the value of x. On 0.6 and earlier, without this hint the compiler wouldn’t even try to specialize on a value x, only on a type like Val{x}. This is not magical, however: if x is not statically predictable, Val{x} won’t be either.

  • On 0.7, the compiler has been taught (by @jameson) to specialize on values that it can statically predict, not just types. Thus, in the cases where it could have special on Val{x} it can now just as easily specialize on x instead without needing to wrap x in a type.

  • Therefore Val{x} is unnecssary on 0.7 since the hint is no longer needed in order to coax the optimizer into specializing on x: it will do so automatically where possible and the Val wrapper doesn’t magically make it any easier to do so.


#10

Note that Val is not deprecated in 0.7, at least as of a few commits ago. It might be a good idea to do this unless there is some other internal use for it.


#11

I’m not sure that we need to deprecate it, it’s just of significantly lesser utility than before.


#12

AFAIK it’s still useful if you want to leave open the option to handle new values through dispatch later. That is, even if the compiler can generate efficient code for:

julia> function foo(x)
         if x == :foo
           1
         elseif x == :bar
           2
         end
       end
foo (generic function with 1 method)

julia> foo(:foo)
1

If I instead use a Val:

julia> foo(::Val{:foo}) = 1
foo (generic function with 2 methods)

julia> foo(::Val{:bar}) = 2
foo (generic function with 3 methods)

then someone else (or future me) can later implement foo(::Val{:baz}).


#13

In this case, maybe it would be better to define ones own type?


#14

How about if I want to dynamically specialise on a certain value?

using StaticArrays

function f(n::Int)
    b = rand(SVector{n})
    for i in 1:10000
        b += rand(SVector{n})
    end
    return b
end

function f(_n::Val{n}) where n
    b = rand(SVector{n})
    for i in 1:10000
        b += rand(SVector{n})
    end
    return b
end

g1() = f(rand(4:4))
g2() = f(Val(rand(4:4)))

@time g1()
@time g2()
julia> @time g1();
  0.015060 seconds (140.02 k allocations: 8.851 MiB)

julia> @time g2();
  0.000249 seconds (5 allocations: 208 bytes)

#15

@jameson where can I read more about proper constant propagation and how it makes val-type obsolete for dynamic dispatch?


#16

Yes, it’s still useful for that. I wasn’t trying to say that Val has no utility anymore, but that it’s often unnecessary for the kind of specialization hint that it was originally introduced for.


#17

Yeah, probably :slightly_smiling_face:


#18

That’s certainly how I feel sometimes :wink:

But @rdeits and @kristoffer.carlsson made my points (2) and (1) much better than me. The code-pattern where an unpredictable Val-type is used as an entrypoint for an expensive function is something I actually use (if I spend 10ms on a type-stable computation, then the dynamic dispatch in between does not really hurt), and the code-pattern (1) where I add new cases all over the place instead of having to group them is one of my favorite things about julia.

I mean, if property-names were val-types I could even use method-extension to add aliases to field-names of base-types. I know, terrible type-piracy, but privately fixing perceived shortcomings in Base is my hobby and it would be really really convenient to do e.g.

julia>capacity(v::Vector)=unsafe_load(convert(Ptr{UInt64},pointer_from_objref(v)),5)

julia> v=collect(1:10);capacity(v)
0x000000000000000a

julia> push!(v,3);capacity(v)
0x0000000000000014


julia> Base.getproperty(v::Vector, ::Val{:capacity}) = capacity(v); 
julia> getproperty(v,Val{:capacity}())
0x0000000000000014

julia> v.capacity
ERROR: type Array has no field capacity

And the capacity wants to be a dot-access/field: It has the same @code_native and calling convention as any access to a field in a mutable struct, so the syntax should reflect that, imho. (I think most core devs decided that exposing the internals of arrays in julia is not desirable, which is fine; people can always do this themselves)

Regardless, thanks to all for the explanations!