An analog to Common Lisp EQL specialization?

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? :face_with_monocle:

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.