generating expressions and then checking them in Julia - julia

My goal is to be able to generate a list of expressions, p.g., check that a number is in some interval, and then evaluate it.
I was able to do it in the following way.
First, a function genExpr that creates such an Expr:
function genExpr(a::Real, b::Real)::Expr
quote
x < $(a + b) && x > $(a - b)
end
end
Create two expressions:
e1 = genExpr(0,3)
e2 = genExpr(8,2)
Now, my problem is how to pass these expressions to a function along with a number x. Then, this function, checks if such a number satisfies both conditions. I was able to achieve it with the following function:
function applyTest(y::Real, vars::Expr...)::Bool
global x = y
for var in vars
if eval(var)
return true
end
end
return false
end
This works, but the appearance of global suggests the existence of a better way of obtaining the same goal. And that's my question: create a function with arguments a number and a list of Expr's. Such function returns true if any condition is satisfied and false otherwise.

This looks like a you are probably looking into using a macro:
macro genExpr(a::Real, b::Real)
quote
x-> x < $(a + b) && x > $(a - b)
end
end
function applyTest(y::Real, vars::Function...)::Bool
any(var(y) for var in vars)
end
Testing:
julia> e1 = #genExpr(0,3)
#15 (generic function with 1 method)
julia> e2 = #genExpr(8,2)
#17 (generic function with 1 method)
julia> applyTest(0,e1,e2)
true
However, with this simple code a function just generating a lambda would be as good:
function genExpr2(a::Real, b::Real)
return x-> x < (a + b) && x > (a - b)
end

Related

Julia Metaprogramming: Function for Mathematical Series

I'm trying to build a function that will output an expression to be assigned to a new in-memory function. I might be misinterpreting the capability of metaprogramming but, I'm trying to build a function that generates a math series and assigns it to a function such as:
main.jl
function series(iter)
S = ""
for i in 1:iter
a = "x^$i + "
S = S*a
end
return chop(S, tail=3)
end
So, this will build the pattern and I'm temporarily working with it in the repl:
julia> a = Meta.parse(series(4))
:(x ^ 1 + x ^ 2 + x ^ 3 + x ^ 4)
julia> f =eval(Meta.parse(series(4)))
120
julia> f(x) =eval(Meta.parse(series(4)))
ERROR: cannot define function f; it already has a value
Obviously eval isn't what I'm looking for in this case but, is there another function I can use? Or, is this just not a viable way to accomplish the task in Julia?
The actual error you get has to do nothing with metaprogramming, but with the fact that you are reassigning f, which was assigned a value before:
julia> f = 10
10
julia> f(x) = x + 1
ERROR: cannot define function f; it already has a value
Stacktrace:
[1] top-level scope at none:0
[2] top-level scope at REPL[2]:1
It just doesn't like that. Call either of those variables differently.
Now to the conceptual problem. First, what you do here is not "proper" metaprogramming in Julia: why deal with strings and parsing at all? You can work directly on expressions:
julia> function series(N)
S = Expr(:call, :+)
for i in 1:N
push!(S.args, :(x ^ $i))
end
return S
end
series (generic function with 1 method)
julia> series(3)
:(x ^ 1 + x ^ 2 + x ^ 3)
This makes use of the fact that + belongs to the class of expressions that are automatically collected in repeated applications.
Second, you don't call eval at the appropriate place. I assume you meant to say "give me the function of x, with the body being what series(4) returns". Now, while the following works:
julia> f3(x) = eval(series(4))
f3 (generic function with 1 method)
julia> f3(2)
30
it is not ideal, as you newly compile the body every time the function is called. If you do something like that, it is preferred to expand the code once into the body at function definition:
julia> #eval f2(x) = $(series(4))
f2 (generic function with 1 method)
julia> f2(2)
30
You just need to be careful with hygiene here. All depends on the fact that you know that the generated body is formulated in terms of x, and the function argument matches that. In my opinion, the most Julian way of implementing your idea is through a macro:
julia> macro series(N::Int, x)
S = Expr(:call, :+)
for i in 1:N
push!(S.args, :($x ^ $i))
end
return S
end
#series (macro with 1 method)
julia> #macroexpand #series(4, 2)
:(2 ^ 1 + 2 ^ 2 + 2 ^ 3 + 2 ^ 4)
julia> #series(4, 2)
30
No free variables remaining in the output.
Finally, as has been noted in the comments, there's a function (and corresponding macro) evalpoly in Base which generalizes your use case. Note that this function does not use code generation -- it uses a well-designed generated function, which in combination with the optimizations results in code that is usually equal to the macro-generated code.
Another elegant option would be to use the multiple-dispatch mechanism of Julia and dispatch the generated code on type rather than value.
#generated function series2(p::Val{N}, x) where N
S = Expr(:call, :+)
for i in 1:N
push!(S.args, :(x ^ $i))
end
return S
end
Usage
julia> series2(Val(20), 150.5)
3.5778761722367333e43
julia> series2(Val{20}(), 150.5)
3.5778761722367333e43
This task can be accomplished with comprehensions. I need to RTFM...
https://docs.julialang.org/en/v1/manual/arrays/#Generator-Expressions

Changing the input (i.e. x+2y) of a macro to an expression ( :(x+2y)), How to produce the same output?

The code at the end of this post constructs a function which is bound to the variables of a given dictionary. Furthermore, the function is not bound to the actual name of the dictionary (as I use the Ref() statement).
An example:
julia> D = Dict(:x => 4, :y => 5)
julia> f= #mymacro4(x+2y, D)
julia> f()
14
julia> DD = D
julia> D = nothing
julia> f()
14
julia> DD[:x] = 12
julia> f()
22
Now I want to be able to construct exactly the same function when I only have access to the expression expr = :(x+2y).
How do I do this? I tried several things, but was not able to find a solution.
julia> f = #mymacro4(:(x+2y), D)
julia> f() ### the function evaluation should also yield 14. But it yields:
:(DR.x[:x] + 2 * DR.x[:y])
(I actually want to use it within another macro in which the dictionary is automatically created. I want to store this dictionary and the function within a struct, such that I'm able to call this function at a later point in time and manipulate the objects in the dictionary. If necessary, I may post the complete example and explain the complete problem.)
_freevars2(literal) = literal
function _freevars2(s::Symbol)
try
if typeof(eval(s)) <: Function
return s
else
return Meta.parse("DR.x[:$s]")
end
catch
return Meta.parse("DR.x[:$s]")
end
end
function _freevars2(expr::Expr)
for (it, s) in enumerate(expr.args)
expr.args[it] = _freevars2(s)
end
return expr
end
macro mymacro4(expr, D)
expr2 = _freevars2(expr)
quote
let DR = Ref($(esc(D)))
function mysym()
$expr2
end
end
end
end

Break Function without return

I have a function
function foo(a)
if a > 5
a = 5
end
some_more_code
end
If the if-statement is true I would like to end the function but I don't want to return anything - to change the value of a is all I need.
How do I do that?
You can write (note that I have also changed the syntax of function definition to make it more standard for Julia style):
function foo(a)
if a > 5
a = 5
return
end
# some_more_code
end
Just use the return keyword without any expression following it. To be precise in such cases Julia returns nothing value of type Nothing from a function (which is not printed in REPL and serves to signal that you did not want to return anything from a function).
Note though that the value of a will be only changed locally (within the scope of the function), so that outside of the function it will be unchanged:
julia> function foo(a)
if a > 5
a = 5
return
end
# some_more_code
end
foo (generic function with 1 method)
julia> x = 10
julia> foo(x)
julia> x
10
In order to make the change visible outside of the function you have to make a to be some kind of container. A typical container for such cases is Ref:
julia> function foo2(a)
if a[] > 5
a[] = 5
return
end
# some_more_code
end
foo2 (generic function with 1 method)
julia> x = Ref(10)
Base.RefValue{Int64}(10)
julia> foo2(x)
julia> x[]
5

Problem with declaring and changing the value of a variable in julia

What is wrong with my code? Do I have to declare x before using it?
function f(n::Int64, t::Int64)
A = ones(n,n)
for i=0:t
if i > 0
A[x,a] = rand()*A[x,a] + rand()
end
y = rand(1:n)
b = rand(1:n)
if i > 0
A[x,a] = rand()*A[x,a] + rand()*A[y,b]
end
x = y
a = min(b, rand(1:n))
end
return A
end
Here is the error thrown when trying to call f:
UndefVarError: x not defined
I think that the reason is more complex, as similar code in Python would work.
For example compare (Python):
>>> def f():
... for i in range(3):
... if i > 0:
... print(a)
... a = i
...
>>> f()
0
1
to (Julia):
julia> function f()
for i in 0:2
if i > 0
println(a)
end
a = i
end
end
f (generic function with 1 method)
julia> f()
ERROR: UndefVarError: a not defined
So what is the difference? As the Julia manual explains here you have:
for loops, while loops, and comprehensions have the following behavior: any new variables introduced in their body scopes are freshly allocated for each loop iteration, as if the loop body were surrounded by a let block
This means that in your code variables a and x as they are local to the for loop are freshly allocated in each iteration of the loop. Because of this the variable has to be assigned to before it is accessed inside the loop.
Therefore it is not needed to assign a value to x and a before the loop. It is enough to define them in scope outer to the loop (even without assigning of the value). For example like this:
julia> function f(n::Int64, t::Int64)
A = ones(n,n)
local x, a
for i=0:t
if i > 0
A[x,a] = rand()*A[x,a] + rand()
end
y = rand(1:n)
b = rand(1:n)
if i > 0
A[x,a] = rand()*A[x,a] + rand()*A[y,b]
end
x = y
a = min(b, rand(1:n))
end
return A
end
f (generic function with 1 method)
julia> f(1,1)
1×1 Array{Float64,2}:
0.94526289614139
Now it works because x and a are not freshly allocated in each iteration of the loop.
In my original toy example it would look like:
julia> function f()
local a
for i in 0:2
if i > 0
println(a)
end
a = i
end
end
f (generic function with 2 methods)
julia> f()
0
1
and you see that you get exactly what you had in Python.

Keyword Arguments - Functions

I am a beginner in Julia,
how can I create functions with keywords for arguments without having to initialize these arguments in function?
A very simple example:
function f(;a = 1, b = 2)
a+b
end
I would like to do:
function f(;a, b)
a+b
end
Best regards.
This is a new feature in version 0.7 — you can actually write it just as you'd like.
Julia's syntax on versions 0.6 and prior require you to give them a default value, but since that default value is evaluated at call time, you can actually use an error function to require them:
julia> function f(;a=error("a not provided"), b=error("b not provided"))
a+b
end
f (generic function with 1 method)
julia> f()
ERROR: a not provided
Stacktrace:
[1] f() at ./REPL[1]:2
julia> f(a=2)
ERROR: b not provided
Stacktrace:
[1] (::#kw##f)(::Array{Any,1}, ::#f) at ./<missing>:0
julia> f(a=2, b=3)
5
This is comming in Julia 0.7 line:
Keyword arguments can be required: if a default value is omitted, then an exception is thrown if the caller does not assign the keyword a value (#25830).
So:
function f(;a, b)
a+b
end
Will become syntax sugar for:
function f(;a = throw(UndefKeywordError(:a)), b = throw(UndefKeywordError(:b)))
a+b
end
Another workaround is to create a function with variadic keyword arguments and leave any requirements over the expected keyword inputs as assertions inside the code. E.g.
function f( ; kwargs... )
V = Dict( kwargs )
try; assert( haskey( V, :a ) ); assert( haskey( V, :b ) )
catch e; throw( AssertionError("KWargs need to be a and b") )
end
V[:a] + V[:b]
end
f(a=1, b=2) #> 3
f(a=1, c=2) #> ERROR: AssertionError: KWargs need to be a and b
Or even as simple as:
function f( ; kwargs... )
V = Dict( kwargs )
a = V[:a]
b = V[:b]
a + b
end
f(a=1, c=2) #> ERROR: KeyError: key :b not found
Disclaimer: I'm not recommending this, I'm just saying it's another workaround to consider depending on what functionality you have in mind.

Resources