How to "source" a string?

I have 2 functions that are highly similar, except that there’s a block of codes appears only in one of the functions.
For example, something like:

function fun()
  XXXXXX
  XXXXXX

  YYYYYY
  YYYYYY
end

function gun()
  XXXXXX
  XXXXXX

  ZZZZZZ    # the extra block of codes
  ZZZZZZ

  YYYYYY
  YYYYYY
end

I want to factorize the functions by defining two strings, and then “source” them inside the functions like:

str1 = "XXXXXX
        XXXXXX"

str2 = "YYYYYY
        YYYYYY"

function fun()
  source(str1)
  source(str2)
end

function gun()
  source(str1)
  ZZZZZZ
  ZZZZZZ
  source(str2)
end

the above “source()” works like string interpolation. how to do it in Julia?

Noted that include() not work as it involves evaluation of the string.

On the other hand, defining sub-function also not work as it would create local variables that are not reachable by the other sub-functions…

thanks.

I think what you are looking for is Meta.parse to convert the string to an expression and eval to run it

See: Metaprogramming · The Julia Language

1 Like

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.

6 Likes

Maybe you could also define “XXX…” “YYY…” and “ZZZ…” as functions and then use them within the two functions as needed.

3 Likes

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
1 Like

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

Thanks for all replies!

@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 carefully pass 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.

9 Likes

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 :slight_smile:

8 Likes

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.

4 Likes

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
7 Likes

@lmiq, @carstenbauer all u said are absolutely correct, in theory at least.

Imagine, block A and block B share tons of local variables: it would make properly defining functions for them really painful.

In practice, the solution by @ffevotte is quite genius I think.

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.

7 Likes