Как использовать оператор "in" в сторожевых предложениях?

Я пытаюсь написать контролер анаграммы в Elixir. Он занимает 2 слова, первый из них является ссылкой, второй должен быть проверен как возможная анаграмма первого.

Я пытаюсь записать его с помощью рекурсии и сопоставления с образцом. Я получаю ошибку об использовании оператора in в предложении охраны:

(ArgumentError) неверные аргументы для оператора in, он ожидает компиляции список времени или диапазон с правой стороны при использовании в защитных выражениях

Я не знаю, что делать, чтобы исправить это. Вот код (ошибка в четвертом определении):

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

Ответ 1

Это вызвано следующим кодом (как вы определили):

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

Вы можете найти определение макроса in в исходном коде, но я скопировал его здесь для удобства - он также содержит в документации:

гвардейской

Оператор in может использоваться в сторожевых предложениях как долго поскольку правая часть - это диапазон или список. В таких случаях эликсир будет расширять оператор до действительного защитного выражения. Например:

  when x in [1, 2, 3] 

переводит на:

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

Код, определяющий макрос:

  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

Ваш код блокирует последнюю часть в аргументе case, потому что во время компиляции не может быть гарантировано, что ваша переменная reference имеет тип list (или range.)

Вы можете увидеть, какое значение передается макросу, вызывая:

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

Здесь атом :reference передается макросу in, который не соответствует ни одному из предыдущих предложений, поэтому он попадает в предложение _ (что вызывает ошибку).

Чтобы исправить это, вам нужно объединить два последних предложения в одну функцию:

  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

Также стоит отметить, что вы, вероятно, хотите использовать "strings" вместо 'char_lists' http://elixir-lang.org/getting-started/binaries-strings-and-char-lists.html#char-lists

Другое дело, что вызов reference - head не будет работать (он поднимет ArithmeticError). Вы можете посмотреть List.delete/2, чтобы удалить элемент из списка.