Fl4m3Ph03n1x

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

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

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

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

LostKobrakai

That‘s not really needed. Macros would only make things more complex, as at no point AST has to be modified.

Where Next?

Popular in Questions Top

lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
mgjohns61585
Could someone help me? I’m making my first elixir program, number guessing game. I can’t figure out how to convert the user’s guess from ...
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
vac
Hi, I’m quite new in Elixir and I’m trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and I...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New

Other popular topics Top

chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
AstonJ
Please see the new poll here: Which code editor or IDE do you use? (Poll) (2022 Edition) It’s been a while since we first asked this, I...
208 31142 143
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" => #BSON.ObjectId<58eb1a7a9ad169198c3dXXXX>, "email" => ...
New

We're in Beta

About us Mission Statement