1
votes

I'm trying to learn elixir(but mostly functional programming)

I am implementing a very simple GenServer which basically wraps a list of entries. The maximum count of entries and the maximum size (in bytes) of each param is restricted(config file)

defmodule List do

    def init(_) do 
        {:ok, []}
    end

    def handle_call({:insert, param1, param2, param3}, from, list) do

        import Application

        param1_max_size = get_env(:app, ....)
        param2_max_size = get_env(:app, ....)
        param2_max_size = get_env(:app, ....)

        max_items_count = get_env(:app, ....)

        ## should be {:reply, {:error, :your_list_is_full}, list}  if list is full 

        ## should be {:reply, {:error, {:check_this_args_please, wrong_params_list}, list} if any param is wrong. wrong_params_list contains the offending params 

        ## should be {:reply, {:ok}, [{param1, param2, param3} | list ]} otherwise

    end

end

I know it seems easy but basically i'm trying to find an elegant functional way to do that. My head is procedural and i always end up in a "nested-if-else-hell" using = operator like if it were the C one.

thx

1

1 Answers

3
votes

The cleanest way I know for validating multiple parameters is Ecto with its changesets. You might want to implement something very similar.

  1. Create a datastructure that holds values and validation errors
  2. Make all validation functions take that datastructure as first argument and return this datastructure to make it easy for piping. I am going to call it changeset the same way Ecto does.

The code could be something like this:

changeset = %{data: {param1, param2, param3}, errors: []}

def validate_param(changeset, number) do
  max_size = get_env(:app, :"param#{number}"
  if elem(changeset.data, number-1) < max_size do
    changeset
  else
    errors = [{:"param#{number}", elem(changeset.data, number-1)} | changeset.errors
    %{ changeset | errors: errors}
  end
end

validated_changeset = 
  changeset
  |> validate_param(1)
  |> validate_param(2)
  |> validate_param(3)

case validated_changeset.errors do
  [] -> {:reply, :ok, [changeset.data | list]}
  errors -> {:reply, errors, list}
end

In Elixir you can define an atom using quotes to contain special characters. If there are no special characters it is the same as without quotes:

iex(1)> :"a b"
:"a b"
iex(10)> :"ab"
:ab

Inside quotes you can use string interpolation so you can create generic validate_param using atom :"param#{number}".

elem function is zero indexed, so we need to subtract 1 from number: elem(changeset.data, number-1).

If there are no errors we are returning the changeset without any modifications.

Errors will have structure {field_name, field_value}. It should be easy to add list to the changeset and create one more validation function that checks list length and appends correct error.

You can also remove duplication in calling validate_param 3 times using List.foldl:

List.foldl([1, 2, 3], changeset, fn(number, changeset) -> validate_number(changeset, number) end)

or even shorter:

List.foldl([1, 2, 3], changeset, &validate_number(&2, &1))

The most important thing is the pattern of functions returning the same type as they take as first argument. It makes it easy to pipe things avoiding many nested if or case statements.