Kevin Thompson
Kevin Thompson

Reputation: 2506

Elixir: setting variables with module-wide scope

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.

Upvotes: 3

Views: 3043

Answers (4)

Kevin Thompson
Kevin Thompson

Reputation: 2506

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.

Upvotes: 0

Chris Meyer
Chris Meyer

Reputation: 1631

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

Upvotes: 5

José Valim
José Valim

Reputation: 51439

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.

Upvotes: 3

user210309
user210309

Reputation: 136

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/

Upvotes: 1

Related Questions