Store a function in Ecto

Hi

I am trying to set up a Quiz Server based on the book Designing Elixir Systems with OTP. (A Quiz Game), where I want to be able to choose a template, create a quiz with it, and then follow as the user goes through the questions one by one.

The design works well in iex, and I am now trying to set up an interface in Phoenix.

Currently, there are two inputs that are automatically generated using functions, the validator (to test if the answer provided is correct) and the input generator (that basically generates the list of inputs to answer).

For instance a multiplication template would accept a list of inputs {left: [7}, right: 1…10} and generate out of it all the combinations so the kid can test the full table. In this case the validator just tests that the input is left * right. But the validator could test the distance in the case of the capital of a country to see if it is “close enough”, and say it is correct.

My problem is that now I want to store the functions in Ecto. I have thought of serializing the function but I don’t know if there is an easy way of doing that and I’ve read this thread (How to serialize anonymous function in disk - #6 by OvermindDL1) that seems to be against it.

Is there a good way of storing the functions in Ecto?

I’ve realized that you can use

:erlang.term_to_binary

and
:erlang.binary_to_term

To convert a function into a string and back. I wonder if there is any security consideration to be taken into account.

I don’t know about security concerns but the problem I think is that functions can be state if the code that generated them is updated and no longer the same. I guess such a function would not work because erlang will not be able to find the corresponding compiled code.

I would rather store a tuple {module, function, arguments}.

1 Like

Thanks, I didn’t think that the code would not work with a new version.
So a function like:

a = fn i -> 1..i |> Enum.to_list end

Could you ellaborate further how you would store it? I don’t know what would be in module and arguments and how to recover it

You just have to define this function as a regular function.

def range_to(i) do
  Enum.to_list(1..i)
end

And now you can store {MyQuizModule, :range_to, [123]}

1 Like

I am confused as to why would you want to store them in DB?

You can:

  1. Have a collection of already written functions in a module QuizServer.Functions.
  2. Reference them by their string name in DB so you can just have e.g. “multiply”.
  3. Find them inside the QuizServer.Functions by the name fetched from the DB.

Very easy to do. So why store actual Erlang bytecode in a DB?

In my definition the code was an anonymous function you use to modify the template and create a quiz.
So I can reuse my template

The code is in the quizserver.

I thought that with functions being a first class data type, I could use this schema to create new templates so I can change the input functions and use them to generate new quizzes easily (just give an input to the generator and you are done, and the generator itself can randomize the questions, etc). Instead of having a map of functions and storing their name and publishing a new version of the application for each new inputs or validations, use the anonymous function to store them, and be able to create new versions just creating a new template with the updated function. (for instance, if I want to uprade my code to compare string trimming them)

It seems that this is not a good strategy, but I thought it would be a nice way of making the code flexible, using those functions as parameters. If I need to have them in the code, and store the name, then I need to publish a new version of the code to update the data, and those two functions are no longer a parameter, and more a fixed strategy

But how would you update your stored functions without updating the code?

Currently you can create a new Template, and provide one of the parameters as a function. (the example code under Multiplication uses a function in the module, but you could create a Template from scratch and provide an anonymous function where is needed, I don’t need to create a new Template every time I want to build a new quiz).

To provide this new anonymous function you have to edit code. You can as well define a new module and a named function inside, or add this function to an existing module ; and provide this module+function+args to your template. I don’t see what an anonymous function can do here that this method cannot, really.

1 Like

To be more explicit, conceptually a fun and a mfa tuple are the same in your use case. They both hold predefined data (closures in the case of funs, and args list for mfa-tuple), and can both accept new arguments (call arguments for funs and items added to the args list, generally on the beginning of the list, for mfa tuples).

But it is easy to store a function name and a list of arguments because it is just data.

defmodule Template do
  def create_fun_range() do
    fn i -> 1..i |> Enum.to_list() end
    # which is the same as
    # &range_to/1
  end

  def create_name_range() do
    :range_to
  end

  def create_mfa_range(preset_args \\ []) do
    {__MODULE__, :range_to, preset_args}
  end

  def range_to(i) do
    1..i |> Enum.to_list()
  end
end

a = Template.create_fun_range()

apply(a, [3])
|> IO.inspect(label: "with fun")

b = Template.create_name_range()

apply(_default_module = Template, b, [3])
|> IO.inspect(label: "with name")

c = {mod, fun, args} = Template.create_mfa_range()

apply(mod, fun, [3 | args])
|> IO.inspect(label: "with mfa")

The three ways can be executed with apply and yield the same results:

with fun: [1, 2, 3]
with name: [1, 2, 3]
with mfa: [1, 2, 3]

You can just write that function in your template module:

import Kernel, except: [apply: 2, apply: 3]

  # ...

  def apply(fun, args) when is_function(fun, length(args)) do
    Kernel.apply(fun, args)
  end

  def apply(name, args) when is_atom(name) do
    Kernel.apply(__MODULE__, name, args)
  end

  def apply({mod, fun, preset_args}, args) do
    Kernel.apply(mod, fun, args ++ preset_args)
  end

And now your module can execute whatever functional parameter for your quiz.


Template.apply(a, [3])
|> IO.inspect(label: "with fun")

Template.apply(b, [3])
|> IO.inspect(label: "with name")

Template.apply(c, [3])
|> IO.inspect(label: "with mfa")

1 Like

Thanks,

After your response I have started to work with a similar approach to see how it works. Storing the Module and name and arity and then calling Function.capture to create a function that I can call with any parameters.

 def cast(%{module: module, function: function, arity: arity} = func) when is_map(func) do
    moduleName = String.to_existing_atom("Elixir.#{module}")
    f = String.to_atom(function)
    {:ok, Function.capture(moduleName, f, arity)}
  end

I was using apply but then I can’t easily dump and load them from the database. With this I can store the function and the parameters separated, and apply them when I need the results (or ask for customer input for the parameters and apply them to the function)

To answer your other question. One of the reasons to use the anonymous function is that I can connect to my database and create new templates without code, just providing the function to the template. Updating the logic of my application without having to update the code.

I still don’t like this structure. (And now i can store the name of the function in the database).

With the anonymous function, I can create new templates and use them in quizzes with no code. This this version, I need to have all the code in a function anyway, so instead of using a database, I can basically implement a behaviour and store the name of the module in the quiz, instead of having a template in the database that requires code outside of the database.

I will continue investigating

Well if you really want to go this way, you can store quoted expressions in your database generated from other code without running the main app, and then eval’ them in the app :smiley:

But honestly what is so bad about deploying a new version when you need new code?

2 Likes

This is best and most optimal IF you will never ever change the module, function, and argument count of the function you are referencing. If you can, or you want to deserialize it onto a system without that module or when that module might not have that function then dynamically compiling the function is the only reliable way (and of course anything that compiled function calls will need to exist too).

Remember, these actually do have a name, and it’s a new, potentially random, and unique name every time the app is compiled (it’s actually based on a few, very changeable, aspects of the environment). Don’t use it across compilation versions.

Yep, this is the dynamic compilation version. ^.^

3 Likes

Actually, I ended up using EEx (that was used to compile the questions), to compile the solutions too. No other solution was working fully.

Thanks both for helping out

2 Likes