Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use the 'in' operator in guard clauses?

I am trying to write an anagram checker in Elixir. It takes 2 words, the first one is a reference, the second is to be tested as a possible anagram of the first.

I am trying to write it with recursion and pattern matching. I get an error about using the inoperator in a guard clause:

(ArgumentError) invalid args for operator in, it expects a compile time list or range on the right side when used in guard expressions

I don't know what to do to fix it. Here is the code (error is in 4th definition):

defmodule MyAnagram do
  def anagram?([], []), do: true

  def anagram?([], word) do
    IO.puts 'Not an anagram, the reference word does not contain enough letters'
    false
  end

  def anagram?(reference, []) do
    IO.puts 'Not an anagram, some letters remain in the reference word'
    false
  end

  def anagram?(reference, [head | tail]) when head in reference do
    anagram?(reference - head, tail)
  end

  def anagram?(_, [head | _]) do
    IO.puts 'Not an anagram, #{head} is not in the reference word.'
    false
  end
end
like image 214
svarlet Avatar asked Jul 02 '15 12:07

svarlet


1 Answers

This is caused by the following code (as you identified):

def anagram?(reference, [head | tail]) when head in reference do
  anagram?(reference - head, tail)
end

You can find the definition of the in macro in the source code, but I have copied it here for convenience - it also contains the following in the documentation:

Guards

The in operator can be used in guard clauses as long as the right-hand side is a range or a list. In such cases, Elixir will expand the operator to a valid guard expression. For example:

  when x in [1, 2, 3] 

translates to:

  when x === 1 or x === 2 or x === 3

The code that defines the macro:

  defmacro left in right do
    in_module? = (__CALLER__.context == nil)

    right = case bootstraped?(Macro) and not in_module? do
      true  -> Macro.expand(right, __CALLER__)
      false -> right
    end

    case right do
      _ when in_module? ->
        quote do: Elixir.Enum.member?(unquote(right), unquote(left))
      [] ->
        false
      [h|t] ->
        :lists.foldr(fn x, acc ->
          quote do
            unquote(comp(left, x)) or unquote(acc)
          end
        end, comp(left, h), t)
      {:%{}, [], [__struct__: Elixir.Range, first: first, last: last]} ->
        in_range(left, Macro.expand(first, __CALLER__), Macro.expand(last, __CALLER__))
      _ ->
        raise ArgumentError, <<"invalid args for operator in, it expects a compile time list ",
                                        "or range on the right side when used in guard expressions, got: ",
                                        Macro.to_string(right) :: binary>>
    end
  end

Your code block is hitting the last part in the case statement because it can't be guaranteed at compile time that your variable reference is of type list (or range.)

You can see what value is being passed to the macro by calling:

iex(2)> quote do: head in reference                                       
{:in, [context: Elixir, import: Kernel],
 [{:head, [], Elixir}, {:reference, [], Elixir}]}

Here, the atom :reference is being passed to the in macro, which doesn't match any of the previous clauses so it falls through to the _ clause (which raises the error.)

To fix this, you will need to combine your last two clauses into one function:

  def anagram?(reference, [head | tail]) do
    case head in reference do
      false ->
        IO.puts 'Not an anagram, #{head} is not in the reference word.'
        false
      true ->
        anagram?(reference - head, tail)
    end
  end

It is also worth noting that you probably want to use "strings" instead of 'char_lists' http://elixir-lang.org/getting-started/binaries-strings-and-char-lists.html#char-lists

The other thing is that calling reference - head won't work (it will raise an ArithmeticError). You might want to look at List.delete/2 to remove an item from a list.

like image 78
Gazler Avatar answered Sep 20 '22 13:09

Gazler