3

In a nutshell, I have a script that reads a .yaml file to get some configuration information at runtime such as what URL to contact, what shared secret to use, whether to use debug mode, etc.

The module that uses that config has a start function which later calls a loop and also calls a logdebug function that writes diagnostics but only if debug mode is set. What is irritating me, though is that I have to pass my configuration around to each of these functions every time I call them. It would be much easier if I could call the start function and have it set some variables that are available to all the other functions in the module. Can that be done? I can't seem to find anything about how to do that.

Is there a preferred way of setting runtime configuration like what I'm doing here? Maybe I'm overcomplicating things?

EDIT: Little more detail, I'm distributing this as an executable created with Escript.Build and I don't want to make the end users edit a file and then rebuild the file. That's why I want the end user (who might not be super technical) to just be able to edit a .yaml file.

1
  • I'm not sure if this would solve your problem Kevin but you may want to take a look at conform Commented Oct 31, 2015 at 21:32

4 Answers 4

5

Disclaimer: I'm interpreting your 'runtime config' more as arguments; if that's not the case, this answer may not be very useful.

A Module is similar to a Class.

Unfortunately, not similar enough for this common O-O approach to work; Elixir/Erlang modules have no "life" in them, and are just flat logic. What you are effectively trying to do is store state in the Module itself; in a functional language, state must be kept in variables, because the Module is shared across all callers from all processes- another process might need to store different state!

However, this is a common programming problem, and there is an idiomatic way to solve it in Elixir: a GenServer.

If you aren't familiar with OTP, you owe it to yourself to learn it: it'll change the way you think about programming, it'll help you write better (read: more reliable) software, and it'll make you happy. Really.

I would store the config in the GenServer's state; If you create an internal struct to represent it, you can pass it around easily and set defaults; all the things we want in a pleasing API.

A sample implementation:

defmodule WebRequestor do
  use GenServer

  ###  API  ###
  # these functions execute in the CALLER's process
  def start_link() do
    GenServer.start_link(__MODULE__, [], [name: __MODULE__])
  end

  def start do
    # could use .call if you need synch, but then you'd want to look at 
    # delayed reply genserver calls, which is an advanced usage
    GenServer.cast(__MODULE__, :start)
  end

  #could add other methods for enabling debug, setting secrets, etc.      
  def update_url(new_url) do
    GenServer.call(__MODULE__, {:update_url, new_url})
  end

  defmodule State do
    @doc false
    defstruct [
      url: "http://api.my.default",
      secret: "mybadpassword",
      debug: false,
      status: :ready, # or whatever else you might need
    ]
  end

  ###  GenServer Callbacks  ###
  # These functions execute in the SERVER's process

  def init([]) do
    config = read_my_config_file
    {:ok, config}
  end

  def handle_cast(:start, %{status: :ready} = state) do
    if config.debug, do: IO.puts "Starting"
    make_request(state.url)
    {:noreply, %{state|status :running}}
  end
  def handle_cast(:state, state) do
    #already running, so don't start again.
    {:noreply, state}
  end  

  def handle_call({:update_url, new_url}, _from, state) do
    {:reply, :ok, %{state|url: new_url}}
  end

  ###  Internal Calls  ###

  defp make_request(config) do
    # whatever you do here...
  end
  defp read_my_config_file do
    # config reading...
    %State{}
  end
end
Sign up to request clarification or add additional context in comments.

2 Comments

Wow that is a really detailed answer, and I suspect there is a lot of good stuff in there. I'm going to have to take some time really thinking my way through this code sample. Thank you!
Great answer! In this case, since he just wants to keep state somewhere, he could use an Agent and avoid many of the boilerplate in a GenServer.
3

The answer depends on your constraints. Do you need to use .yaml? We discourage the use of YAML unless you really have non-Elixir programmers needing to touch those. If they are all being touched by programmers, then you can just use Elixir configuration:

# config/config.exs
config :my_app,
  url: "...",
  this: "...",
  that: "..."

This will allow you to access and change the configuration by using functions like Application.get_env(:my_app, :url) and Application.put_env(:my_app, :foo, :bar). In the future, if you want to build releases (shipping the whole application with the VM in a single directory), provide upgrades and so on, using Elixir configuration will prove the optimal workflow.

2 Comments

I went with a combination of :yamerl and Application.put_env. Reason being: the yaml file is meant for end users that may not know how to rebuild the file that Escript.Build is creating. Do you think that my use case is a place where it makes sense to use yaml or do you still think I should look to a different way of configuring the app at runtime?
Why do you have end-users modifying yaml or any other configuration? Give them a nice UI to make whatever configuration changes need to be made. End-users shouldn't have to know anything about your underlying implementation mechanism. In fact if you have to move away from YAML for some other reason you're making it needlessly harder for your end-users.
1

Can you use an exs file instead ? Possibly load the yaml file there if necessary? Here's a blog post that seems quite similar: http://www.schmitty.me/taking-advantage-of-mix-config/

Comments

0

I ended up using yamerl in my main module to read the .yaml file and then I used Application.put_env/2 to put the values into a place where they were available for all my modules.

[ config | _ ] = :yamerl_constr.file("config.yaml") Application.put_env(:osq_simulator, :base_url, :proplists.get_value('base_url', config))

Although based on some other feedback I got, it looks like maybe the answer from Chris Meyer is the "right" way to do what I'm trying to do.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.