logankilpatrick
logankilpatrick

Reputation: 14501

How to execute some code the first time a Julia function is called?

I have a specific use case where I want a function to basically provide a warning the first time it is called to tell the user some info. Other than using a global counter and keeping track of the number of times the function is called, I am not sure how I can check this. Any ideas on specific Julia syntax that would allow me to check if the function is being called for the first time?

Upvotes: 3

Views: 440

Answers (2)

tholy
tholy

Reputation: 12179

Use the maxlog feature of the logging macros:

julia> function warnfirst(x, y)
           @warn "This is the first time you called this" maxlog=1
           return 2x + y
       end
warnfirst (generic function with 1 method)

julia> warnfirst(1, 2)
┌ Warning: This is the first time you called this
└ @ Main REPL[1]:2
4

julia> warnfirst(1, 2)
4

julia> warnfirst(1.0, 2.0)   # what about different specializations?
4.0

Upvotes: 4

phipsgabler
phipsgabler

Reputation: 20950

Here are some ideas for how to do this when you have the option of rewriting the function body. All of these could also be realized by writing a rather simple macro performing the respective transformation. OK, it's not so trivial if you want to get top-level and local definitions working right.

(Non-)option 1

Conceptually, you could do this with a generated function, and it will work mostly when you try it out:

julia> @generated function dostuff(x)
           @warn "You really shouldn't do stuff!"
           return :(2x + 1)
       end
dostuff (generic function with 1 method)

julia> dostuff(1)
┌ Warning: You really shouldn't do stuff!
└ @ Main REPL[1]:2
3

julia> dostuff(1)
3

But: don't. The compiler is free to choose when to call the "generator", and to quote the docs: it is undefined exactly when, how often or how many times these side-effects will occur. Not a good idea.

Additionally, it is questionable whether @warn will use a printing function that is allowed within a generated function. In earlier Julias, using println instead of Core.println sometimes errored in generated functions, because the former modified the event loop.

Option 2

So for something better. Instead of your idea with a global counter, you can do something similar by defining the function as a closure of a let-bound variable:

julia> let isfirstcall = Threads.Atomic{Bool}(true)
           global function dostuff(x) 
               if Threads.atomic_xchg!(isfirstcall, false)
                   @warn "You really shouldn't do stuff!"
               end
               return 2x + 1
           end
       end
dostuff (generic function with 1 method)

julia> dostuff(1)
┌ Warning: You really shouldn't do stuff!
└ @ Main REPL[16]:4
3

julia> dostuff(1)
3

julia> isfirstcall
ERROR: UndefVarError: isfirstcall not defined

I have here chosen to use atomics just for the fun of atomic_xchg!, but if threading is not an issue, a plain boolean will be fine, too.

Option 3

Also, while avoidable, a global variable isn't too bad if you do it right. Which means: make it a const Ref. And (optionally, but recommended in this case), use a var string to give it a name not usually accessible to the user:

julia> const var"##isfirstcall" = Ref(true)

julia> function dostuff(x)
           if var"##isfirstcall"[]
               @warn "You really shouldn't do stuff!"
               var"##isfirstcall"[] = false
           end
           return 2x + 1
       end
dostuff (generic function with 1 method)

julia> dostuff(1)
┌ Warning: You really shouldn't do stuff!
└ @ Main REPL[22]:3
3

julia> dostuff(1)
3

Upvotes: 5

Related Questions