The Essentials section of the Julia documentation suggests that methods can be specalied on Val
types. Perhaps I’m misunderstanding, but this seems analogous to Common Lisp’s EQL specialization.
Assuming that’s the case, I’m trying to use Val
types to specialize the second property
argument ofBase.getproperty
, rather than a conditional tree.
struct MyStruct
a::Int
end
function Base.getproperty(o::MyStruct, prop::Val{:b})
o.a * 2
end
ms = MyStruct(3)
MyStruct(3)
ms.a
3
methods(Base.getproperty, [typeof(ms), Val{:b}])
# 1 method for generic function "getproperty":
[1] getproperty(o::MyStruct, prop::Val{:b}) in Main at none:1
Base.getproperty(ms, :a)
3
Base.getproperty(ms, :b)
ERROR: type MyStruct has no field b
Stacktrace:
[1] getproperty(x::MyStruct, f::Symbol)
@ Base .\Base.jl:33
[2] top-level scope
@ none:1
So, what am I doing wrong?
This works though:
Base.getproperty(ms, Val{:b}())
6
I guess I don’t understand the point of Val
, because I’d have expected
:b isa Val{:b}
false
to be true
, not false
.
Does Julia have something analogous to Common Lisp EQL specialization?
Unfortunately I’m not familiar with Common Lisp EQL specialization. But the behaviour of Julia here is the following. You get the error, because you call the original version of getproperty
. To call your definition, the second parameter needs to be of type Val{:b}
(equivalent to your second call).
julia> getproperty(ms, Val(:b))
6
# because
julia> typeof(Val(:b))
Val{:b}
:b
is of type Symbol
. Docs
Something like this should work. It is very close to your original code, where the only missing piece was to handle the “conversion” from Symbol
values (that the obj.prop
syntax produces) to Val
types (that you want to use for the dispatch in order to avoid a conditional tree).
struct MyStruct
a::Int
end
# Forward `o.propname` to `getproperty(o, Val(:propname))`
#
# Rely on constant propagation to statically know about the type of `Val(prop)`
# when the value of `prop` is statically known
Base.getproperty(o::MyStruct, prop::Symbol) = getproperty(o, Val(prop))
# Default implementation: assume prop refers to a "real" field and call `getfield`
Base.getproperty(o::MyStruct, prop::Val{T}) where {T} = getfield(o, T)
# Specific methods defining new properties
# This is where dispatch avoids having a conditional tree
Base.getproperty(o::MyStruct, prop::Val{:b}) = o.a * 2
Base.getproperty(o::MyStruct, prop::Val{:c}) = o.a * 3.0
julia> ms = MyStruct(3)
MyStruct(3)
julia> ms.a
3
julia> ms.b
6
julia> ms.c
9.0
Overall, this should be optimized away by the compiler, provided that constant propagation (of the property names) can happen:
julia> b(o::MyStruct) = o.b
b (generic function with 1 method)
julia> @code_native b(ms)
.text
movq (%rdi), %rax
addq %rax, %rax
retq
nopw (%rax,%rax)
1 Like
Thanks for the solution, I was looking for something like this myself. However there is a question still hanging
Is this more performant than the usual if...else
structure?
I would think both solutions have the same performance in most cases, since IIUC both implementations rely heavily on optimizations that are enabled by constant propagation.
Expanding on the previous example, we see that the code generated for the b
property is exactly the same for a conditional tree that what we had above (again, provided that we’re in a context where constant propagation can happen):
struct MyStruct2
a :: Int
end
function Base.getproperty(o::MyStruct2, prop::Symbol)
if prop == :b
o.a * 2
elseif prop == :c
o.a * 3.0
else
getfield(o, prop)
end
end
julia> ms = MyStruct2(3, 4.0)
MyStruct2(3, 4.0)
julia> b(o::MyStruct2) = o.b
b (generic function with 1 method)
julia> @code_native b(ms)
.text
movq (%rdi), %rax
addq %rax, %rax
retq
nopw (%rax,%rax)
1 Like
CommonLisp doesn’t have a notion of properties, but for what I’m trying to do, the code would look something line (I’ve not programme3d in CL in about a decade and don’t have a lisp installed on this computer, so excude any errors):
(defclass my-struct ()
((a :initarg :a :reader ms-a)))
(setq ms (make-instance 'my-struct :a 3))
(defmethod get-property((object my-struct) (property (eql :b)))
(* (ms-a object) 2))
(get-property ms :b)
;;; would return 6
Thanks for the solution.
I guess I don’t understand the point of Val
and why this method is needed. I guess so long as the property is known at compile time the access would get in-lined.
If anyone wants to use Val
specialization for getproperty
, I’ve added some support for it in GitHub - MarkNahabedian/NahaJuliaLib.jl: Some utility functions for Julia programming.. In particular,
@njl_getprop MyStruct
will define the three methods needed so that getproperty
and propertynames
will work for Val
specialized getproperty
methods. For any comments or suggestions, feel free to open an issue for that repository or comment on this thread.
Thanks.