Is this pure?


#1
Base.@pure which_key(tuple::NTuple{N, Symbol}, key::Symbol) where N =
    map(x -> x == key, tuple)

#2

According to the definition in #414, further discussed in #14324, it seems so:

julia> which_key(tuple::NTuple{N, Symbol}, key::Symbol) where N =
           map(x -> x == key, tuple)
which_key (generic function with 1 method)

julia> a1 = (:x,:y,:z); a2 = (:x,:y,:z);

julia> k1 = :x; k2 = :x;

julia> a1 === a2
true

julia> k1 === k2
true

julia> which_key(a1, k1) === which_key(a2, k2)
true

But I see you commented on that second issue, so I doubt this comment is of any use to you!


#3

I would generally discourage using the @pure annotations. It’s primary usage is for inference and it is hard to use correctly.


#4

You need the @pure annotation here to get constant propagation.


#5

It’s true that it is computing a pure result, since we can inline the map call to create an equivalent function that makes it more apparent that we’re only using pure functions:

which_key(tuple::Tuple{}, key::Symbol) = ()
which_key(tuple::Tuple{Vararg{Symbol}}, key::Symbol) =
    (first(tuple) === key, which_key(Base.tail(tuple), key)...)

Although, if you try code_typed on this, you’ll soon discover that IPO constant-propagation gets disabled by recursion, so I’d guess that probably doesn’t help as much as you might have liked. (and after several minutes of playing with this, I can say that it’s surprisingly hard to bypass that heuristic – which is nice since it means that at least it’s fairly robust to style changes.)

In the end, I decided the most feasible way to bypass it was just to give in and use a generated function after moving everything into the type-domain, so, um, yeah, this:

which_key4(tuple::NTuple{N, Symbol}, key::Symbol) =
    _which_key4(Val(tuple), Val(key))
_which_key4(::Val{tuple}, ::Val{key}) where {tuple, key} =
    ntuple(i -> tuple[i] === key, Val(length(tuple)))

addendum:
An even more minimal implementation (e.g. the same as above after more manual inlining):

which_key(tuple::Tuple{Vararg{Symbol}}, key::Symbol) =
    _which_key(key, tuple...)
_which_key(key::Symbol) = ()
Base.@pure _which_key(key::Symbol, first::Symbol, tail...) =
    (first === key, _which_key(key, tail...)...)

#6

Ok, great! Since I have an expert here, to get constant propagation to work recursively, I need to define my own tail function:

Base.@pure argtail(x, rest...) = rest
tail(x) = argtail(x...)

Again, a @pure annotation is necessary here, even though argtail seems very pure.


#7

Also, would it be kosher to take off the type restrictions?

Base.@pure which_key(tuple::Tuple, key) where N =
    map(x -> x == key, tuple)

#8

No, it’s not pure without the restrictions.

It seems like argtail just needs an update to the inference code