Strategy for building complicated struct - passing through functions / building once?

Hi All,

I didn’t find any guides / recommendations for Elixir about the correct architectural design / strategy for building complicated structs. Could you please advise?

I’m working on an app that shows current status of a system. It is supposed to produce a single struct for each system. Let’s call it %System{}.

Possible ways to achieve that:

a) Create empty %System{} (most of it’s fields will be nil) and then pass it through a number of functions that will add or modify values. Each function will return the %System{} with more values. Added / modified values can be simple (String, Integer, Boolean) or more complicated (another structs).

b) Call all functions in a main function and get all values for all fields into a number of variables. Then assemble and return %System{a: a, b: b, ...}.

Thank you.

2 Likes

I’d probably go with a compositional style

%System{
  a: fetch_a(),
  b: fetch_b(),
  c: fetch_c()
}

So plan b). Thank you. But it’ll be slightly more complicated. Probably something like this:

with {:ok, a} <- fetch_a(y, z),
     {:ok, b} <- fetch_b(x, a.something, z),
     {:ok, c} <- fetch_c(x, b.soemthing) do
  {:ok, %{
    a: a
    b: b
    c: c
  }}
else
  {:error, _} = error -> error
end

ah yep, if you’re getting ok tuples back you’ll need to do it the long way

How you compose the fetching of data with actually building the struct based on it depends on how you want callers to interact with it (in both the success and error cases). E.g. see Plug.Conn, which is partly filled by the webserver integration and partly by plugs in the pipeline. There are considerations like which data is available at certain steps or what needs to be user triggered because it’s performance intensive (e.g. reading the request body).

Generally you should try to work out how much flexibility callers need and then you can decide how to best allow them to fetch the system structs according to that flexibility.

Plug.conn is an excellent example. Thank you.

I do want to give callers flexibility to fetch additional details (like fetch_cookies(t(), Keyword.t()) :: t()) but I also want to introduce some basic set of values.

At this moment I’m thinking building the whole thing as plan A (passing %System{} through fetch_cookies-like functions) and then when it’s finished and after I decided on the minimum set of values, changing the construction to plan B %System{a: a, b: b, ...}.

P.S.
Looking at the documentation and the typespec source code, Plug.conn has interesting way of handling unfetched via Unfetched.t(). I was thinking just using nil but I’ll consider to use :unfetched atom instead.

Unfetched sounds a good idea as nil can be ambiguous in many cases. What I have done in cases where I’ve felt I’d need to modify a map/struct too many times before producing the final map is to gather intermediate keys and values into a keyword list and then use Map.new/1 which uses the :maps.from_list/1 BIF underneath to convert into a map in one go (which should be more efficient than continuously updating an immutable map). Of course this approach also has a major drawback: no compile-time support for valid struct keys, so I’d suggest only go down this way in cases of ‘too many’ updates.

1 Like