How to write a simple macro

I have lines similar to this a lot in my code:

if chi_factor > 0.85
    chi_factor = 0.85
end

I would like to write it like this:

@limit chi_factor 0.85

I cannot use a function for this because a function cannot change the value of a scalar argument.

Any idea how to write such a macro?

Something like this should work:

macro limit(name::Symbol, value)
    return esc(quote
        if $name > $value
            $name = $value
        end
    end)
end
julia> x = 1; @limit(x, 0.4); x
0.4

julia> x = 1; @limit(x, 1.4); x
1
1 Like

why not just chi_factor = min(chi_factor, .85)?

2 Likes

Because then you need to write the name of the variable twice and my colleagues who love C will be laughing at me if I tell them thats the way to do it in Julia…

And I also sometimes need a macro that limits in a range, and then it would be

chi_factor = max(min(chi_factor, 0.85), 0.4)

which is hard to read. Easier to read:

@limit chi_factor 0.4 0.85

This works for me:

macro limit(name::Symbol, value)
    return esc(quote
        if $name > $value
            $name = $value
        else
            $name
        end
    end)
end

height = 12.0
@limit height 10.0
println(height)

height = 5.0
@limit height 10.0
println(height)

Thanks for giving me a starting point for writing macros! :slight_smile:

2 Likes

This works:

clamp!([x],0.85,Inf)

I’m sure a function could be used as well as an @macro here too.

1 Like

Sorry, this does NOT work:

height = 12.0
# println(@limit height 10.0)
clamp!([height], 5.0, 10.0)
println(height)

height = 4.0
# println(@limit height 10.0)
clamp!([height], 5.0, 10.0)
println(height)

It does NOT change the variable height.

Bonus question:

How can I write a macro @limit that excepts either two or three parameters, such that

@limit height 10.0

and

@limit height 5.0 10.0

both work?

It looks like I got caught by the [] making a copy.
You are correct, it only worked from the top level of the global-ish REPL.

I think this should work:

macro limit(name::Symbol, min, max=min)
    return esc(quote
        $name = clamp($name, $min, $max)
    end)
end
2 Likes

Incidentally, what is the C way to write chi_factor = clamp(chi_factor, low, high) without writing the variable twice, once to access it and once to reassign it?

PS this has been implied already, but clamp works on scalars while clamp! works on arrays.

With a macro,

#define LIMIT(X,LOW,HIGH) ((X) = ((X)<(LOW)? (LOW) : ((X)>(HIGH) ? (HIGH) : (X)))

LIMIT(chi_factor, low, high);
3 Likes

Isn’t the intent to ignore the max if not provided? As in

macro limit(name::Symbol, vmin, vmax=typemax(vmin))
     return esc(quote
          $name = clamp($name, $vmin, $vmax)
     end)
end

It would probably be better to use typemax(name) as the default for vmax, but I don’t know how to implement this.

1 Like

You can’t do this with an ordinary macro because types aren’t known at macro expansion time. However, you could just use two methods:

macro limit(name::Symbol, vmin, vmax)
     return esc(quote
          $name = clamp($name, $vmin, $vmax)
     end)
end

macro limit(name::Symbol, vmin)
     return esc(quote
          $name = min($name, $vmin)
     end)
end

Edit: fixed 2-argument version to replace max by min. I misunderstood the OP’s intent for the 2-argument version.

2 Likes

With your version, there’s a problem when using with only 2 arguments:

julia> x = 4
4

julia> @limit x 2
4

julia> x # here, x should be 2, because we are limiting it to a max value of 2
4

julia> y = 8
8

julia> @limit y 9
9

julia> y # y should still be 8 here
9

Also, if the user chooses to pass a variable as argument instead of a number:

julia> max = 10
10

julia> @limit y max
ERROR: LoadError: MethodError: no method matching typemax(::Symbol)
Closest candidates are:
  typemax(::Union{Dates.DateTime, Type{Dates.DateTime}}) at C:\Users\jorge\AppData\Local\Programs\Julia-1.7.2\share\julia\stdlib\v1.7\Dates\src\types.jl:453
  typemax(::Union{Dates.Date, Type{Dates.Date}}) at C:\Users\jorge\AppData\Local\Programs\Julia-1.7.2\share\julia\stdlib\v1.7\Dates\src\types.jl:455
  typemax(::Union{Dates.Time, Type{Dates.Time}}) at C:\Users\jorge\AppData\Local\Programs\Julia-1.7.2\share\julia\stdlib\v1.7\Dates\src\types.jl:457
  ...
Stacktrace:
 [1] var"@limit"(__source__::LineNumberNode, __module__::Module, name::Symbol, vmin::Any)
   @ Main .\REPL[1]:2
in expression starting at REPL[11]:1

And yes, my version of the macro is still wrong, (it only works properly with 3 arguments):

julia> x = 4
4

julia> @limit x 2
2

julia> x # should be 2, so here it works
2

julia> y = 8
8

julia> @limit y 9
9

julia> y # should be 8 here
9

But this version works with both 2 and 3 arguments:

macro limit(name, min, max=nothing)
    if isnothing(max)
        max = min
        min = :(typemin($min))
    end

    return esc( :($name = clamp($name, $min, $max)) )
end
julia> x = 4
4

julia> @limit x 2
2

julia> x
2

julia> y = 8
8

julia> @limit y 9
8

julia> y
8

julia> z = 10
10

julia> @limit z 5 8
8

julia> z
8

julia> w = 5
5

julia> min, max = 2, 10
(2, 10)

julia> @limit w min max
5

julia> w
5
4 Likes

Could you make chi_factor and array with one entry ie

chi_factor=zeros(1)

and then you can update that entry as you would any array.

function limit!(chi_factor,clim)
    if chi_factor[1] > clim
       chi_factor[1]=clim
   end
end

Could you make chi_factor and array with one entry ie

I know, but that is not a good, practical solution. I have lots of values I want to limit, they are all scalars and are passed around from other functions and modules…

1 Like

The practical solution would probably be to create a mutable struct, or a Dict, with all the values and restrict on them. But a macro is probably a solid second place.

1 Like

I misunderstood what the 2 argument version of @limit was mean to do. Thanks for correcting.

1 Like

I think it is worth pointing out that when using macros you should take care not to make the code more challenging to read. x = clamp(x, lo, hi) takes much less effort to understand than @limit x lo hi, particularly for a newcomer to the code base, because you don’t then have to go to the macro and figure out what it means. The macro is also then another piece of code you have to write, test and maintain. I have certainly encountered cases where I am trying to debug or modify someone else’s Julia package and have struggled to figure out what a poorly-documented macro is actually doing.

In this case, the macro is relatively simple, and if this operation is done all over the place it might be worth it. But it is always worth thinking carefully about how macros impact readability. Reducing number of characters typed is not usually the best optimization strategy. Nor is avoidance of teasing from unenlightened C programmers :wink:

6 Likes