Fl4m3Ph03n1x
Reusing compile time code
Background
I have to read a CSV and currently this is happening at compile time as the function runs in a module attribute:
# Imagine this csv file has 3 columns "sport, country, league"
@csv_sports_data
:my_app
|> :code.priv_dir()
|> Path.join("awesome_csv.csv")
|> File.stream!()
|> CSV.decode!(headers: false, separator: ?;)
|> Stream.map(&List.to_tuple/1)
|> Enum.uniq()
So now, because this runs at compile time (iirc) I have a variable with the data I need in tuple format. So far so good.
Problem
The problem comes when I need to do the same thing, multiple times, with small variations:
# The duplication, IT BURNS !!!
# Imagine this csv file has 3 columns "sport, country, league"
@csv_sports_data
:my_app
|> :code.priv_dir()
|> Path.join("awesome_csv.csv")
|> File.stream!()
|> CSV.decode!(headers: false, separator: ?;)
|> Stream.map(&List.to_tuple/1)
|> Enum.uniq()
@sports
:my_app
|> :code.priv_dir()
|> Path.join("awesome_csv.csv")
|> File.stream!()
|> CSV.decode!(headers: false, separator: ?;)
|> Stream.map(&List.to_tuple/1)
|> Stream.uniq()
# Always trim data from pesky users!
|> Stream.map(
fn {sport, country, league} ->
{String.trim(sport), String.trim(country), String.trim(league)}
end)
|> Stream.map(fn {sport, _country, _league} -> sport end)
#No empty sports!
|> Enum.filter(fn sport -> sport != "" end)
@countries
:my_app
|> :code.priv_dir()
|> Path.join("awesome_csv.csv")
|> File.stream!()
|> CSV.decode!(headers: false, separator: ?;)
|> Stream.map(&List.to_tuple/1)
|> Stream.uniq()
# Always trim data from pesky users!
|> Stream.map(
fn {sport, country, league} ->
# Always trim data from pesky users!
{String.trim(sport), String.trim(country), String.trim(league)}
end)
|> Stream.map(fn {_sport, country, _league} -> country end)
# We allow empty countries to make the example interesting
As you can see, I have a lot of duplicated code. At the very least I could place
:my_app
|> :code.priv_dir()
|> Path.join("awesome_csv.csv")
|> File.stream!()
|> CSV.decode!(headers: false, separator: ?;)
|> Stream.map(&List.to_tuple/1)
|> Stream.uniq()
Into a function or variable and then re-use it in @sports and countries. The trimming function is also another candidate. And then there are the little differences for @sports and @countries where I select only the values I want.
Things I tried
So, my first try was to use the @csv_sports_data inside the @sports and @countries attributes. Obviously this didn’t work, as I can’t use something that was not yet compiled into an attribute that is itself being generated at compile time.
# This wont work
@sports
@csv_sports_data
# Always trim data from pesky users!
|> Stream.map(
fn {sport, country, league} ->
{String.trim(sport), String.trim(country), String.trim(league)}
end)
|> Stream.map(fn {sport, _country, _league} -> sport end)
#No empty sports!
|> Enum.filter(fn sport -> sport != "" end)
My second try was to consider Macros. According to my understanding, I could create a Macro that reads the CSV file at compile time and then have @sports and @countries use it. However, I personally am a believer of the saying:
“The first rule about Macros - don’t use Macros”
And I feel the usage of a Macro for this specific situation would be quite overkill. So I would like to avoid it.
And then there is also the trim function:
Stream.map(
fn {sport, country, league} ->
# Always trim data from pesky users!
{String.trim(sport), String.trim(country), String.trim(league)}
end)
Which I cannot place inside a def or defp for the sake of reuse.
What now?
Surely I am missing something. Perhaps the solution I was given to work with the CSV is flawed, or perhaps I am forgetting some mechanism that would reduce the amount of duplicated code I have.
- How can I remove all the duplication?
Marked As Solved
michallepicki
@Fl4m3Ph03n1x Another issue in your code is that you can’t do
@sports
@compiled_csv_data
|> ...
you need to do
@sports
csv_data
|> ...
And turns out that in Elixir 1.5 also when declaring the @sports module attribute, to be able to use it in guards you need to do it in two steps. This works:
sports =
csv_data
|> Stream.map(fn {sport, _country, _league} -> sport end)
|> Enum.filter(fn sport -> sport != "" end)
@sports sports
def hard_sport?(sport) when sport in @sports, do: false
Also Liked
michallepicki
You can operate on values (not module attributes) in a module body, do some computations and only then assign them to attributes:
csv_sports_data = :my_app
|> :code.priv_dir()
|> Path.join("awesome_csv.csv")
|> File.stream!()
|> CSV.decode!(headers: false, separator: ?;)
|> Stream.map(&List.to_tuple/1)
|> Stream.uniq()
|> Enum.map(fn {sport, country, league} ->
{String.trim(sport), String.trim(country), String.trim(league)}
end)
@csv_sports_data csv_sports_data
And re-use the already computed value to declare other module attributes:
@sports csv_sports_data
|> Enum.map(fn {sport, _country, _league} -> sport end)
|> Enum.filter(fn sport -> sport != "" end)
@countries csv_sports_data
|> Enum.map(fn {_sport, country, _league} -> country end)
michallepicki
You can also move logic to other module that will become a dependency so it will get compiled earlier, where you can split your logic in functions however you like, for example:
defmodule SportsCsvReader do
def read_sports_data() do
:my_app
|> :code.priv_dir()
|> Path.join("awesome_csv.csv")
|> File.stream!()
|> CSV.decode!(headers: false, separator: ?;)
|> Stream.map(&List.to_tuple/1)
|> Stream.uniq()
|> Enum.map(fn {sport, country, league} ->
{String.trim(sport), String.trim(country), String.trim(league)}
end)
end
def extract_sports(sports_data) do
sports_data
|> Enum.map(fn {sport, _country, _league} -> sport end)
|> Enum.filter(fn sport -> sport != "" end)
end
def extract_countries(sports_data) do
sports_data
|> Enum.map(fn {_sport, country, _league} -> country end)
end
end
and then you’ll be able to use it directly in your other module:
csv_sports_data = SportsCsvReader.read_sports_data()
@csv_sports_data csv_sports_data
@sports SportsCsvReader.extract_sports(csv_sports_data)
@countries SportsCsvReader.extract_countries(csv_sports_data)
edit: or re-use this logic in any other module
LostKobrakai
That‘s not really needed. Macros would only make things more complex, as at no point AST has to be modified.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








