Lots of the answer is that you definitely shouldn’t be using strings for this. Instead, (if meta-programming is the answer), you should be using expressions instead of strings.
I don’t think it a good approach to use strings for code reuse, but you can still do it in this way.
macro source(str)
str |>
__module__.eval |> # evaluate macro arguments in top level
Meta.parse |> # it should be a string, parse it to Julia Expr
esc # do not rename symbols in the Expr
end
str = "a + 1"
function f(a)
@source(str) # equals to `a + 1`
end
yes, but you can define the functions locally to a function and then share variables within that function, e.g.
function one()
x= 5
y= 7
function two()
# x , y are visible
end
function three_a()
# the same x,y are visible
end
function three_b()
# the same x,y are visible
end
function four()
# etc...
end
function combo1()
two()
three_a()
four()
end
function combo2()
two()
three_b()
four()
end
combo1()
combo2()
end
@thautwarm’s answer is “my” solution: it works perfectly with local variables. It has no running cost penalty and no warntype also.
function fun(x, y)
z = 2x + y # block A
z *= 3 # block B
return z
end
function gun(x, y)
z = 2x + y # block A
z -= 5 # the extra block of codes
z *= 3 # block B
return z
end
const strA = "z = 2x + y"
const strB = "z *= 3"
function fun1(x, y)
@source(strA)
@source(strB)
return z
end
function gun1(x, y)
@source(strA)
z -= 5 # the extra block of codes
@source(strB)
return z
end
julia> fun(1, 2)
12
julia> fun1(1, 2)
12
julia> gun(1, 2)
-3
julia> gun1(1, 2)
-3
@code_warntype fun(1, 2)
@code_warntype fun1(1, 2)
@code_warntype gun(1, 2)
@code_warntype gun1(1, 2)
julia> @btime fun($1, $2);
0.022 ns (0 allocations: 0 bytes)
julia> @btime fun1($1, $2);
0.022 ns (0 allocations: 0 bytes)
julia> @btime gun($1, $2);
0.021 ns (0 allocations: 0 bytes)
julia> @btime gun1($1, $2);
0.021 ns (0 allocations: 0 bytes)
On the other hand, I think a “proper” solution is to define a function for each re-using block. But we have to carefullypass in any needed locals as arguments and return any locals that are needed in later codes. For example:
function _blockA(x, y)
z = 2x + y
return z
end
function _blockB(z)
z *= 3
return z
end
function fun2(x, y)
z = _blockA(x, y) # NEED to pass in x & y AND return z
z = _blockB(z) # NEED to pass in and return z
return z
end
function gun2(x, y)
z = _blockA(x, y)
z -= 5 # the extra block of codes
z = _blockB(z)
return z
end
@code_warntype fun2(1, 2)
@code_warntype gun2(1, 2)
julia> @btime fun2($1, $2);
0.022 ns (0 allocations: 0 bytes)
julia> @btime gun2($1, $2);
0.022 ns (0 allocations: 0 bytes)
@purplishrock has a clever suggestion, but it’s not working if the two highly-similar functions are to be called in different times.
Function parameters and return values are for that. You are probably not using the correct approach. I think one can bet without risk that the “sourcing” of strings is not used at all in any of the important Julia packages.
Please, just use functions. There is no need for any metaprogramming/parse/eval magic here. The mere fact that you couldn’t come up with a fancy macro for this should tell you that you shouldn’t go down this road. In particular because there is a super simple alternative: functions. Bundling code for (multiple) reuse is the central purpose of functions!
I go even further than @lmiq: If you need to parse/eval a string you are almost always doing it wrong. Not just in Base or Julia but in general
This is the proper solution, without quotes. And that careful passing of parameters and return values is what makes the function reusable. Otherwise you don’t know what the code expects to use, and what it returns as a result, which variables it modifies, you very rapidly would have a code impossible to maintain.
One solution that I don’t think has been mentioned yet: using higher-order functions (HOF). In my experience, it is frequently the case that most of the code can be factored out, and only a small part of it needs to change. It can then be beneficial to factor out the large, common parts in a higher-order function; the small, “extra” part can then be passed as a function argument to the HOF.
Minimal example:
# A higher-order function to factor out all common code
julia> function common_parts(extra, x, y)
z = 2x + y # block A
z = extra(z) # extra block
z *= 3 # block B
end
common_parts (generic function with 1 method)
# No extra part: z = identity(z)
julia> fun(x, y) = common_parts(identity, x, y)
fun (generic function with 1 method)
# I tend to like the `do` block better, but you could also write
# gun(x, y) = common_parts(z -> z-5, x, y)
julia> gun(x, y) = common_parts(x, y) do z
z - 5
end
gun (generic function with 1 method)
julia> fun(1, 2)
12
julia> gun(1, 2)
-3
Then you should use a data structure. The do syntax is a nice way to pass an anonymous function as a parameter, and z there is the parameter of that anonymous function, thus it does not solve the problem of the “tons of locals”.
Never a code that depends on many variables will be easier to maintain if those variables can be arbitrarily modified in different files with unpredictable effects on independent chunks of code. That will be worse the more variables you have.