Is there a function that takes another function as the argument and then returns the definition of the function passed?

For example, take the following (somewhat silly) piece of code here:

iex(1)> sqr = elem(Code.eval_string("sqrelems = &(Enum.each(&1, fn x -> IO.puts(x*x) end))"), 1)[:sqrelems]
#Function<42.125776118/1 in :erl_eval.expr/6>
iex(2)> sqr.([1, 2, 3, 4, 5])
1
4
9
16
25
:ok

I use Code.eval_string to get from the string representation of a function definition to an actual function and then I match this to the variable sqr.

I would like to then put sqr into a function and get the string representation back out.

Is this possible?

Maybe the following snippet meets your needs:

fun_str = "sqrelems = &(Enum.each(&1, fn x -> IO.puts(x*x) end))"
sqr = elem(Code.eval_string(fun_str), 1)[:sqrelems]      
sqr.([1, 2, 3, 4, 5])
my_map = Map.put(%{}, sqr, fun_str)
Map.get(my_map, sqr)
1 Like

This does output the string representation, but unfortunately you had to first store this representation. I was hoping that a sort of reverse eval was available that would work on a func that had only been defined using standard syntax.

That is:

func = fn x ->
 -my code-
end

Then in iex

iex> c "file-containing-func.exs"
iex>  f = Mod.func
iex> def_to_string f

"func = fn x ->
 -my code-
end" 

#with the necessary escapes

Even better would be if it worked with named functions as well.

I do not know of any function that does what you are looking for. Can you elaborate on your use case?

For using AI parsing to automate the review functions that are part of third party modules or libraries, then modify them in accordance with set specifications and output the old def, the new def, functionality changes and justifications for such changes as the pertain to initially inputted criteria.

For third party modules and libraries you can already access the source code in your deps folder, presumably that’s what you’d want to send to the AI?

1 Like

Can this procedure be done at runtime, and would the information be parsable if obtained at run time. How would I do this?

Also I would especially like to do this for functions that are prepassed as arguments in other built-in functions.

You can read the files at runtime File.read!("deps/some_dep/lib/whatever.ex"). But there’s clearly an aspect of this I’m not getting. Basically, I’m saying that if you want AI to manipulate / weigh in on Elixir source code you need to operate on that source code, not try to turn data back into source code.

2 Likes

This looks rather close to what I wanted, but how would I directly access the function source code, it seems like this would give me the entire module?

Would I just have to use a regex?

If you want to extract a specific function from the module you could use Code.string_to_quoted and then find the function definition AST within that AST. From there you could use Macro.to_string to turn that specific function AST back into a string.

However if that function calls other functions you wouldn’t have a complete picture of stuff. Whether that is OK or not for your purposes I can’t say.

2 Likes

That’s really helpful. Thanks.

Modified, as ! does not need to be escaped

This is a very very rough but functioning utility version of what I wanted to implement.

I wanted to quickly build this and continue working through the books, but I thought this may come in handy for other people learning or people who wanted to quickly access insight into the code-bases with which they are working in the iex. This serves as a quick code snippet review.

I’ve not yet covered elixir best practices for documentation. This is just to provide a quick solution to anyone who had a similar question to mine.

God bless.

defmodule GetMod do

  #Determines source of module using data from IEx.Info.info (this form of info outputs a value that can be handled, unlike the i helpfer function).
  #Module bytcode and Source are keys in the map that IEx.Info.info outputs
  #These can be used to determine the source of compiled and built-in modules.
  #However compiled modules can be located using IEx.Info.info "Source" by itself
  def source_of(module) do

    info = &IEx.Info.info/1

    get_tuple = &(Enum.filter(&1, fn x -> elem(x, 0) === &2 end)|> Enum.at(0))

    get_sub_paths = &(get_tuple.(&1, &2) |> elem(1))

    runner? = &{Regex.match?(~r{[/|\runner.*]}, &1), &1}

    remove_runner = fn
      {x, y} when x === false-> y

      {x, y}  when x === true ->
        Regex.replace(~r[^/.*?(elixir/){2}], y, "")
    end

    old_prefix = get_sub_paths.(info.(module), "Module bytecode")

    new_prefix = (old_prefix !== "[]" && Regex.replace(~r{bin.*}, old_prefix, "")) || ""

    input_source = get_sub_paths.(info.(module),"Source")

    suffix =  input_source |> runner?.() |> remove_runner.()

    new_prefix <> suffix

  end

  def get_file_contents(source) do
    File.read(source) |> elem(1)
  end

  def get(module) do

    source = source_of module

    get_file_contents(source)

  end

  #Check if the function is a boolean or potentially error yielding, and return the special character
  def despec_string(str) do

    Regex.match?(~r/\?/, str)
      && "?"
        || Regex.match?(~r/\!/, str)
          && "!"

  end
  #f_name = spec === "?" ... replaces the special character in the function name with an escaped version
  #reg_run = ~r... matches reg_run with regular expression for capturing multi-line string that begins with the function signature
  #reg_rep = ~r...matches reg_rep with the regular expression for capturing any string that starts with a function signature other than that of the target function
  def get_func(module, f_name) do

    spec = despec_string(f_name)

    f_name = spec === "?"
              && Regex.replace(~r/.{0}(?=\?)/, f_name, "\\?")
                || f_name

    contents = get module

    reg_run = ~r/\n?\s?def(macro|struct|p)?\s(#{f_name}(\(|\s)|#{f_name}(\(|,))+.*/s #if you need the regex explained just let me know in the thread.
    reg_rep = ~r/\n?\s?def(macro|struct|p)?\s(?!(#{f_name}(\(|\s)|#{f_name}(\(|,)))+.*|(@doc).*/s
    funcs = Regex.run(reg_run, contents)  #string of module contents starting at first instance of f_name, this string has been *checked* for f_name
    funcs !== :nil &&
    eval_contents({"", funcs, {reg_run, reg_rep}}) || "No function named #{f_name} in module" #starts from first instance of f_name with empty string
    #Regex.replace(reg_rep, Enum.at(funcs, 0), "#\nEnd of string; ostensibly other functions defined beyond this point.") || "No function named #{f_name} in module"

  end

  def process_string(string, r) do
    {reg_run, reg_rep} = r
    funcs = Regex.run(reg_run, string)
    Regex.replace(reg_rep, Enum.at(funcs, 0), "")
  end

  def eval_contents({string, checked, _,}) when checked === nil  do
     string <> "End of string; ostensibly other functions defined beyond this point."
  end

  def eval_contents({string, checked, r}) when checked !== nil do
    {regm, regr} = {_reg_run, _reg_rep} = r
    checked_string = checked |> Enum.at(0)
    str = string <> "... \n" <> process_string(checked_string, r)
    c_str = Regex.run(regm, Regex.run(regr, checked_string) |> Enum.at(0))
    eval_contents({str, c_str, r}) # check beyond current uninterrupted fnc def for matches
  end

end

Update Example:

#consider
defmodule GetMod do

  def get(module) do

    source = source_of module

    get_file_contents(source)

  end

  ...other functions

  defmacro get(g, i, o) do
  end

  defp get(g,h) do
  end

  def get do
  end
end

iex(35)> IO.puts get_func GetMod, "get"
...
 def get(module) do

    source = source_of module

    get_file_contents(source)

  end

  #Check if the function is a boolean or potentially error yielding, and return the special character
 ...
 defmacro get(g, i, o) do
  end

  defp get(g,h) do
  end

  def get do
  end

 End of string; ostensibly other functions defined beyond this point.
:ok

Example:

iex(254)> IO.puts get_func GetMod, "get_func" 
def get_func(module, f_name) do

    spec = despec_string(f_name)

    f_name = spec === "?"
              && Regex.replace(~r/.{0}(?=\?)/, f_name, "\\?")
                || |f_name

    reg_run = ~r/\n?\s?def(macro|struct|p)?\s(#{f_name}(\(|\s)|#{f_name}(\(|,))+.*/s #if you need the regex explained just let me know in the thread.

    reg_rep = ~r/\n?\s?def(macro|struct|p)?\s(?!(#{f_name}(\(|\s)|#{f_name}(\(|,)))+.*|(#
End of string; ostensibly other functions defined beyond this point.
:ok
iex(234)> IO.puts get_func Kernel, "round" #built-in
 def round(number) do
    :erlang.round(number)
  end

  #
End of string; ostensibly other functions defined beyond this point.
:ok
iex(235)> IO.puts get_func Kernel, "pop_in" #built-in

 def pop_in(data, keys)

  def pop_in(nil, [key | _]) do
    raise ArgumentError, "could not pop key #{inspect(key)} on a nil value"
  end

  def pop_in(data, [_ | _] = keys) do
    pop_in_data(data, keys)
  end

 #
End of string; ostensibly other functions defined beyond this point.
:ok
iex(236)> IO.puts get_func Kernel, "match?" #built-in
 defmacro match?(pattern, expr) do
    success =
      quote do
        unquote(pattern) -> true
      end

    failure =
      quote generated: true do
        _ -> false
      end

    {:case, [], [expr, [do: success ++ failure]]}
  end

  #
End of string; ostensibly other functions defined beyond this point.
:ok

This should work on most windows systems, as the file system on linux/mac devices are different it may not work on those.

1 Like

This won’t work with Erlang deps. You can try using Tria for this. Just call

iex> Tria.Language.Beam.inspect_fn MapSet, :new
fn -> %{__struct__: MapSet, map: %{}} end
fn
  %{__struct__: MapSet} = map_set_36 ->
    map_set_36

  enumerable_68 ->
    set_100 =
      :sets.from_list(Enum.to_list(enumerable_68),
        version: 2
      )

    %{__struct__: MapSet, map: set_100}
end
fn enumerable_132, transform_164 when :erlang.is_function(transform_164, 1) ->
  set_196 =
    :sets.from_list(
      Enum.map(
        enumerable_132,
        transform_164
      ),
      version: 2
    )

  %{__struct__: MapSet, map: set_196}
end
:ok

It even works with erlang functions

Thanks for that. I’ll take a look at the syntax now.

Edit: Ah I see I need to install from git. Will look at the details for that.

Does Tria work with Elixir modules or is this just for work with Erlang deps?

Another thing about my implementation is that it doesn’t catch if someone has scattered variants of the same function throughout their module (this seems unlikely); I know how I need to write the code to catch those occurrences but it’s superfluous for now, as I want to finish the books.

Yes, it works with both Elixir and Erlang. Installation from git is easy, just {:tria, github: "hissssst/tria"} and that’s it

1 Like

This seems great! I’ll look to start experimenting with it.

Edit for the sake of completeness I added the feature

catch if someone has scattered variants of the same function throughout their module…

Updated in solution post

I wanted to add this fix…:

  def eval_contents({string, checked, r}) when not is_nil(checked) do
    {regm, regr} = {_reg_run, _reg_rep} = r
    checked_string = checked |> Enum.at(0)
    str = string <> "... \n" <> process_string(checked_string, r)
    other_funcs = Regex.run(regr, checked_string)
    c_str = other_funcs !== nil && Regex.run(regm, other_funcs  |> Enum.at(0)) || nil
    eval_contents({str, c_str, r})
end

But it seems I’ve used my edit credits.

other_funcs = Regex.run(regr, checked_string)
c_str = other_funcs !== nil && Regex.run(regm, other_funcs |> Enum.at(0)) || nil

Resolves the error that occurs if the function is the last function of its module. Arises with the Enum and reduce module-function pair.

There is an alternative with

when not is_nil(checked)

You can ping a mod to do it for You :slight_smile:

2 Likes

There is an alternative with…

Thanks for that tip! Will update as it is clearer

You can ping a mod to do it for You :slight_smile:

Thanks, I’ll use the module for a few days first to make sure it needs no more edits.

Haven’t run into any issues using this version over the last several days, so I’m posting as the solution or Mod edit request for the solution answer.

God bless.

defmodule GetMod do

  #Determines source of module using data from IEx.Info.info (this form of info outputs a value that can be handled, unlike the i helpfer function).
  #Module bytcode and Source are keys in the map that IEx.Info.info outputs
  #These can be used to determine the source of compiled and built-in modules.
  #However compiled modules can be located using IEx.Info.info "Source" by itself
  def source_of(module) do

    info = &IEx.Info.info/1

    get_tuple = &(Enum.filter(&1, fn x -> elem(x, 0) === &2 end)|> Enum.at(0))

    get_sub_paths = &(get_tuple.(&1, &2) |> elem(1))

    runner? = &{Regex.match?(~r{[/|\runner.*]}, &1), &1}

    remove_runner = fn
      {x, y} when x === false-> y

      {x, y}  when x === true ->
        Regex.replace(~r[^/.*?(elixir/){2}], y, "")
    end

    old_prefix = get_sub_paths.(info.(module), "Module bytecode")

    new_prefix = (old_prefix !== "[]" && Regex.replace(~r{bin.*}, old_prefix, "")) || ""

    input_source = get_sub_paths.(info.(module),"Source")

    suffix =  input_source |> runner?.() |> remove_runner.()

    new_prefix <> suffix

  end

  def get_file_contents(source) do
    File.read(source) |> elem(1)
  end

  def get(module) do

    source = source_of module

    get_file_contents(source)

  end

  #Check if the function is a boolean or potentially error yielding, and return the special character
  def despec_string(str) do

    Regex.match?(~r/\?/, str)
      && "?"
        || Regex.match?(~r/\!/, str)
          && "!"

  end
  #f_name = spec === "?" ... replaces the special character in the function name with an escaped version
  #reg_run = ~r... matches reg_run with regular expression for capturing multi-line string that begins with the function signature
  #reg_rep = ~r...matches reg_rep with the regular expression for capturing any string that starts with a function signature other than that of the target function
  def get_func(module, f_name) do

    spec = despec_string(f_name)

    f_name = spec === "?"
              && Regex.replace(~r/.{0}(?=\?)/, f_name, "\\?")
                || f_name

    contents = get module

    reg_run = ~r/\n?\s?def(macro|struct|p)?\s(#{f_name}(\(|\s)|#{f_name}(\(|,))+.*/s #if you need the regex explained just let me know in the thread.
    reg_rep = ~r/\n?\s?def(macro|struct|p)?\s(?!(#{f_name}(\(|\s)|#{f_name}(\(|,)))+.*|(@doc).*/s
    funcs = Regex.run(reg_run, contents)  #string of module contents starting at first instance of f_name, this string has been *checked* for f_name

    funcs !== :nil &&
    eval_contents({"", funcs, {reg_run, reg_rep}}) || "No function named #{f_name} in module" #starts from first instance of f_name with empty string
    #Regex.replace(reg_rep, Enum.at(funcs, 0), "#\nEnd of string; ostensibly other functions defined beyond this point.") || "No function named #{f_name} in module"

  end

  def process_string(string, r) do
    {reg_run, reg_rep} = r
    funcs = Regex.run(reg_run, string)
    Regex.replace(reg_rep, Enum.at(funcs, 0), "")
  end

  # defmacro get(g, i, o) do
  # end

  # defp get(g,h) do
  # end

  # def get do
  # end

  def eval_contents({string, checked, _,}) when is_nil(checked)  do
     string <> "End of string; ostensibly other functions defined beyond this point."
  end

  def eval_contents({string, checked, r}) when not is_nil(checked) do
    {regm, regr} = {_reg_run, _reg_rep} = r
    checked_string = checked |> Enum.at(0)
    str = string <> "... \n" <> process_string(checked_string, r)
    other_funcs = Regex.run(regr, checked_string) #check this to determine if func is last func in module, other_funcs === nil if it is.
    c_str = other_funcs !== nil && Regex.run(regm, other_funcs |> Enum.at(0)) || nil
    eval_contents({str, c_str, r}) # check beyond current uninterrupted fnc def for matches
  end

end

Example

iex(48)> IO.puts get_func IO, "stream"
...
 def stream, do: stream(:stdio, :line)

  ...
 def stream(device \\ :stdio, line_or_codepoints)
      when line_or_codepoints == :line
      when is_integer(line_or_codepoints) and line_or_codepoints > 0 do
    IO.Stream.__build__(map_dev(device), false, line_or_codepoints)
  end

  End of string; ostensibly other functions defined beyond this point.
:ok
1 Like