In Ruby we might have something like this:
module Specialties
def self.specialties
@specialties ||= ["Specialty One", "Specialty Two"]
end
endAnd we could call this from anywhere in the application:
def valid_specialty?(specialty)
specialty.in?(Specialties.specialties)
endIn Elixir, we can have a Specialties module, but we can't memoize data:
defmodule Specialties do
def specialties
["Specialty One", "Specialty Two"]
end
endThis is a problem if the data comes from some expensive operation (like loading and parsing a file, for example)
We can use something like an Agent to hold this state, and we could definitely start the Agent in a supervisor, but this feels like a lot of overhead just to get simple memoization. From the perspective of the "consuming" code, it just wants to call Specialties.specialties, and from the perspective of the Specialties module, it really only cares about how to get the actual specialties, not how to memoize them in an Agent.
We could make a Specialties module that knows how to memoize the data in an Agent lazily (i.e. without a supervisor starting it up in advance) like this:
defmodule Specialties do
defp load_specialties do
["Specialty One", "Specialty Two"]
end
defp start_link do
Agent.start_link(fn -> nil end, name: :specialties)
end
defp get_and_set_specialties do
data = load_specialties()
Agent.update(:specialties, fn _ -> data end)
data
end
def specialties do
start_link()
Agent.get(:specialties, fn state -> state end) || get_and_set_specialties()
end
endHowever, only the first function is actually interesting --- the last 3 functions are just boilerplate to deal with the Agent stuff and memoization. We could extract this boilerplate into a macro like this:
defmodule Memoizer do
defmacro memoize(description, do: content) do
quote do
def load do
unquote(content)
end
defp start_link do
Agent.start_link(fn -> nil end, name: __MODULE__)
end
defp get_and_set do
data = load()
Agent.update(__MODULE__, fn _ -> data end)
data
end
def(unquote(description)) do
start_link()
Agent.get(__MODULE__, fn state -> state end) || get_and_set()
end
end
end
endAnd then use it like this:
defmodule SpecialtiesViaMacro do
import Memoizer
memoize specialties() do
["Specialty Three", "Specialty Four"]
end
endOf course, in a multi-process environment, two processes could call Specialties.specialties at the same time, and they both might end up running the expensive calculation (which is true in the Ruby version too), but for quick and dirty memoization, especially for single-process Elixir code, this seems a lot simpler than always building an Agent and supervising it.
As a newbie to Elixir, I'm probably missing something here, so I probably won't actually use this approach, but I wanted to at least document the concept.