Extending methods of pure function

AFAIU adding methods to @pure functions is not allowed. However I have a function mysin that I want to be able to add methods to later on and I also know that certain methods are pure. Is a purity fence like in the following snipped a sane solution to this?

using BenchmarkTools

Base.@pure mysin_pure(x::Float64) = sin(x)
mysin(x) = mysin_pure(x)
mysin(x::String) = some_cracy_method_added_later_on

function doit(f,n=100)
    ret = 0.
    for _ in 1:n
        ret += f(1)
    end
    ret
end

@btime doit(sin)
@btime doit(mysin)
  674.955 ns (0 allocations: 0 bytes)
  52.307 ns (0 allocations: 0 bytes)

If so is it also a sane idea to add a @pure_method macro that expands

@macroexpand @pure_method f(x) = body

into something like

Base.@pure f_pure(x) = body
f(x) = f_pure(x)
1 Like

In short, disrecommended.
(because) You are asking “Is it reasonable to take a @pure function and then make it generic if it is done indirectly?”

A pure function can only depend on immutable information. This also means a @pure function cannot use any global mutable state, including generic functions. Calls to generic functions depend on method tables which are mutable global state. Use with caution, incorrect @pure annotation of a function may introduce hard to identify bugs. Double check for calls to generic functions.

Doing what you explore would violate documented tenants of @purity.

Actually, I think your proposal with mysin and mysin_pure looks totally fine. mysin is a regular generic function, and one of its methods happens to call an @pure function. That all sounds good to me.

I’m not as sure about your proposed macro–if you find yourself needing to use @pure that much, it might be a sign that there’s a better approach out there somewhere.

1 Like

Also, marking “pure” a function called “mysin” is theologically frowned upon.

(sorry)

8 Likes

Maybe unsafe?, considering that sin(Inf) throws and this answer in Can @pure functions throw an error? - #2 by jameson

2 Likes

But I wonder if it is OK to wrap a generic function when all the arguments are concrete immutable and it’s “conceptually” pure (e.g., it doesn’t throw). I mean, is @jw3126’s example fine if sin doesn’t throw? Applying this point in the docstring literally probably indicates no:

This also means a @pure function cannot use any global mutable state, including generic functions.

But I see this in Base:

Ah thanks, I thought sin(Inf) was NaN, but you are right it throws.

But then I found this example where a @pure function calls a generic function which includes a path to throw but actually is impossible to throw inside the context of the @pure function: Rationale for the `@pure`ity of `Rational{T}(x)`. So maybe it’s fine to do this?

Base.@pure mysin_pure(x::Float64) = isfinite(x) ? sin(x) : NaN