Function to string

I have a function

ω = 2
f(t) = cos(3 ω + t) 

I would like a function, say, my_string(fct) that would return “cos(3 ω + t)”.
Is this possible? A simple example would be great.

Alternatively, given a string: strg = "cos(3 ω + t)", I'd like to convert it into a function. I am pretty sure I can use eval`, but is there another way?

I would appreciate example for both cases, if possible. Thanks!

How complicated is your function? Does it fit into Home · Symbolics.jl ?

1 Like

Never used Symbolic.jl. My functions are simple. Somewhat more complicated than cost(t).

Would a symbol work for you? (however, that winds up needing eval)
Base.Meta.parse("cos(3 ω + t)") gives:
:(cos(3ω + t))

julia> dump(the_result)
Expr
  head: Symbol call
  args: Array{Any}((2,))
    1: Symbol cos
    2: Expr
      head: Symbol call
      args: Array{Any}((3,))
        1: Symbol +
        2: Expr
          head: Symbol call
          args: Array{Any}((3,))
            1: Symbol *
            2: Int64 3
            3: Symbol ω
        3: Symbol t```

The precise type of this object is an expression, or Expr, if I’m not mistaken. You can learn more about them at Metaprogramming · The Julia Language to see if that’s enough for your needs

One thing to note is that the current Expr representation of a function is missing a lot of original information - especially white space. This means it’s not possible to rebuild the exact string that created the function. But I believe that JuliaSyntax is set to be added in 1.10 which should preserve all information and make tasks like this much easier/possible.

1 Like

I’m not sure there’s a super nice way to do this. But here’s a macro that gives it a try:

julia> macro stringdef(expr) # TODO: make sure expr is a function definition
               return quote
                       f = $(esc(expr))
                       $(esc(:my_string))(::typeof(f)) = $(string(Base.remove_linenums!(expr)))
                       f
               end
       end
@stringdef (macro with 1 method)

julia> @stringdef cos2x(x) = cos(2x)
cos2x (generic function with 1 method)

julia> cos2x(1.0)
-0.4161468365471424

julia> my_string(cos2x)
"cos2x(x) = begin\n        cos(2x)\n    end"

julia> my_string(cos2x) |> println
cos2x(x) = begin
        cos(2x)
    end

As others said, I think the challenge is that an expression is not the original string and cannot be formatted like the original string. Perhaps the only way to achieve this would be to write a macro that operates on a string that it calls eval on, used like

@stringdefstring "foo(x) = 2x"

Then you could use the original string for defining my_string(::typeof(foo)).

Thanks. Here is the use case. I have an equation with a forcing term, which I define parametrically. When the computation is complete, I plot the solution and would like to see the forcing term (defined as a function) as the title of the plot. Perhaps there is another way to approach this? Thanks.

How about something like the following?

julia> struct CallableString{F}
           str::String
           func::F
       end
       function CallableString(str::AbstractString)
           str = String(str)
           func = eval(Meta.parse(str))
           CallableString{typeof(func)}(str, func)
       end
CallableString

julia> (cs::CallableString)(args...) = cs.func(args...)

julia> CallableString("x->x^2")(5)
25

julia> macro callable_str(ex)
           func = eval(Meta.parse(ex))
           tfunc = typeof(func)
           quote
               CallableString{$tfunc}($ex, $func)
           end
       end
@callable_str (macro with 1 method)

julia> callable"x->x^2"(10)
100

We could keep going, add some printing, and do add the captured variable.

julia> function Base.show(io::IO, ::MIME"text/plain", cs::CallableString)
           print(io, "callable")
           show(io, cs.str)
       end

julia> cs = callable"ω = 2; t->cos(3ω + t)"
callable"ω = 2; t->cos(3ω + t)"

julia> cs(2)
-0.14550003380861354

I ran your code and it worked as advertised until the last line:

callable "x->x^2" (10) 

which gave the error:

ERROR: syntax: extra token """ after end of expression. I must have misunderstood something.

Remove the spaces.

1 Like

Yes, it worked. The following worked as well;

xx = @callable_str "x->x^2" 
xx(10)

I will investigate further. Thanks!

My first experiments:

str = "cos(3*t)"
str = "t->" * str
f = @callable_str "t->cos(3*t)"   # works!!
f(3)
f = @callable_str str  # Generates an error
f(3)

Why does @callable_str generate an error when using a non-literal string?

Here is the error:

ERROR: LoadError: MethodError: no method matching parse(::Symbol)

Closest candidates are:
  parse(::AbstractString; raise, depwarn)
   @ Base meta.jl:266
  parse(::AbstractString, ::Integer; greedy, raise, depwarn)
   @ Base meta.jl:232

Stacktrace:
  [1] var"@callable_str"(__source__::LineNumberNode, __module__::Module, ex::Any)
    @ Main ~/src/2022/rude/giesekus/GE_rude.jl/alex_report_code_2023-03-24/julia19/optimized/macros.jl:18
  [2] eval
    @ ./boot.jl:370 [inlined]
  [3] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String)
    @ Base ./loading.jl:1864
  [4] invokelatest(::Any, ::Any, ::Vararg{Any}; kwargs::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
    @ Base ./essentials.jl:816
  [5] invokelatest(::Any, ::Any, ::Vararg{Any})
    @ Base ./essentials.jl:813
  [6] inlineeval(m::Module, code::String, code_line::Int64, code_column::Int64, file::String; softscope::Bool)
    @ VSCodeServer ~/.vscode/extensions/julialang.language-julia-1.47.2/scripts/packages/VSCodeServer/src/eval.jl:233
  [7] (::VSCodeServer.var"#66#70"{Bool, Bool, Bool, Module, String, Int64, Int64, String, VSCodeServer.ReplRunCodeRequestParams})()
    @ VSCodeServer ~/.vscode/extensions/julialang.language-julia-1.47.2/scripts/packages/VSCodeServer/src/eval.jl:157
  [8] withpath(f::VSCodeServer.var"#66#70"{Bool, Bool, Bool, Module, String, Int64, Int64, String, VSCodeServer.ReplRunCodeRequestParams}, path::String)
    @ VSCodeServer ~/.vscode/extensions/julialang.language-julia-1.47.2/scripts/packages/VSCodeServer/src/repl.jl:249
  [9] (::VSCodeServer.var"#65#69"{Bool, Bool, Bool, Module, String, Int64, Int64, String, VSCodeServer.ReplRunCodeRequestParams})()
    @ VSCodeServer ~/.vscode/extensions/julialang.language-julia-1.47.2/scripts/packages/VSCodeServer/src/eval.jl:155
 [10] hideprompt(f::VSCodeServer.var"#65#69"{Bool, Bool, Bool, Module, String, Int64, Int64, String, VSCodeServer.ReplRunCodeRequestParams})
    @ VSCodeServer ~/.vscode/extensions/julialang.language-julia-1.47.2/scripts/packages/VSCodeServer/src/repl.jl:38
 [11] (::VSCodeServer.var"#64#68"{Bool, Bool, Bool, Module, String, Int64, Int64, String, VSCodeServer.ReplRunCodeRequestParams})()
    @ VSCodeServer ~/.vscode/extensions/julialang.language-julia-1.47.2/scripts/packages/VSCodeServer/src/eval.jl:126
 [12] with_logstate(f::Function, logstate::Any)
    @ Base.CoreLogging ./logging.jl:514
 [13] with_logger
    @ ./logging.jl:626 [inlined]
 [14] (::VSCodeServer.var"#63#67"{VSCodeServer.ReplRunCodeRequestParams})()
    @ VSCodeServer ~/.vscode/extensions/julialang.language-julia-1.47.2/scripts/packages/VSCodeServer/src/eval.jl:225
 [15] #invokelatest#2
    @ ./essentials.jl:816 [inlined]
 [16] invokelatest(::Any)
    @ Base ./essentials.jl:813
 [17] macro expansion
    @ ~/.vscode/extensions/julialang.language-julia-1.47.2/scripts/packages/VSCodeServer/src/eval.jl:34 [inlined]
 [18] (::VSCodeServer.var"#61#62")()
    @ VSCodeServer ./task.jl:514
in expression starting at /Users/erlebach/src/2022/rude/giesekus/GE_rude.jl/alex_report_code_2023-03-24/julia19/optimized/macros.jl:34
"""

Because the string macro is only meant to be take literal strings. It’s a string macro.

If you want to pass a string variable, use the constructor I made.

julia> str = "x->x^3"
"x->x^3"

julia> cs = CallableString(str)
callable"x->x^3"

julia> cs(5)
125
1 Like

Thanks! So much to learn!

Why? Usually it is a bad idea to represent functions as strings. What is your application?

1 Like

Here is the use case. I am solving an equation whose right hand side is one of several functions. I’d like to plot the results with this function definition (a one-liner) in the plot title. Per page, there might be up to 16 plots. In the spirit of non-duplication, I’d like to define each function only once and have the titles generated. Thanks.

1 Like

Here is a very unsafe way to do this which will let you maintain intellisense:

julia> struct CallableString{F}
           str::String
           func::F
       end

julia> (cs::CallableString)(args...) = cs.func(args...)

julia> Base.string(cs::CallableString) = cs.str

julia> macro string_func(exp)
           line_number = __source__.line
           line = ""
           for (i, l) in enumerate(eachline(string(__source__.file)))
               if i == line_number
                   line = l
                   break
               end
           end
           body = strip(split(line, "=", limit=2)[2])

           func_name = exp.args[1].args[1]
           hidden_name = Symbol("string_func_" * string(func_name))
           exp.args[1].args[1] = hidden_name

           func = eval(exp)
           func_type = typeof(func)
           return :($(esc(func_name)) = CallableString{$func_type}($body, $func))
       end
@string_func (macro with 1 method)

julia> @string_func bob(x) = x + 1
CallableString{typeof(string_func_bob)}("x + 1", string_func_bob)

julia> bob(5)
6

julia> string(bob)
"x + 1"

The macro looks up the line number where you are defining the function and reads it in as a string. This only works on 1 line functions defined with the f(x, y) = x + y style syntax. It may not work if your function definition is wrapped inside another macro (macros can mess with the line numbers). It also won’t work in the REPL, since it won’t have line numbers to look up. But if you are just doing simple definitions in a file, it should work. It worked for me when I was using “send code to REPL”. So it sorta works in the REPL as long as you’re working from code in a file.

This macro makes me feel:
image

3 Likes