LoginSignup
3
0

More than 1 year has passed since last update.

[Elixir] ※ Compileのタイミングを考慮しないとProtocolをImplement出来ない時がある

Last updated at Posted at 2022-01-10

REnumというRubyやRailsのEnumerableMethodを使えるようにするライブラリを作っています。

このライブラリはメタプログラミングによりElixirのEnum全てのFunctionを移譲しております。

これらがうまく機能していることを担保する為、Elixi本体から各moduleの該当unittestをコピーしてきました。
ところがどっこい。test module内で定義されたstructでprotocolが上手く実装されてず、test自体も失敗してしまうという事象が発生しました。

** (Protocol.UndefinedError) protocol Enumerable not implemented for %REnum.StreamTest.HaltAcc{acc: 1..3} of type REnum.StreamTest.HaltAcc (a struct). This protocol is implemented for the following type(s): Date.Range, File.Stream, Function, GenEvent.Stream, HashDict, HashSet, IO.Stream, List, Map, MapSet, Range, Stream

動かないコード

# test/r_enum/stream_test.ecs
defmodule REnum.StreamTest do
  use ExUnit.Case, async: true

  doctest REnum.Stream.Native

  defmodule HaltAcc do # test module内でstructの定義
    defstruct [:acc]

    defimpl Enumerable do
      def count(_lazy), do: {:error, __MODULE__}

      def member?(_lazy, _value), do: {:error, __MODULE__}

      def slice(_lazy), do: {:error, __MODULE__}

      def reduce(lazy, _acc, _fun) do
        {:halted, Enum.to_list(lazy.acc)}
      end
    end
  end

  test "zip_with/2" do
    zip_fun = &List.to_tuple/1
    stream = %HaltAcc{acc: 1..3}
    # ここのtestで落ちる
    assert REnum.Stream.zip_with([1..3, stream], zip_fun) |> Enum.to_list() == [
             {1, 1},
             {2, 2},
             {3, 3}
           ]
  end
end

unit test内でstructを入れ子にして定義しています。
elixirのリポジトリ内でEarthlyを用いてテストを動かした場合は成功するのですが、コピーしてきたリポジトリ内でmix testを行っても上手く行きません。
※ Elixirのコード内でのtestの動かし方に関しては、また別の機会に調査してみようと思います。きっとmix testのコマンドではないのかもしれないです。

動く実装

iexで試したり、testの他ファイルやlib以下でtestに使うstructを定義してみたところ、lib以下に定義した場合に、正しい成功の挙動を確認することが出来ました。
それらを少し整理した実装が以下コードです。

# mix.exs
  def project do
    [
      ...
      elixirc_paths: elixirc_paths(Mix.env()) # Compile対象をMIX_ENVによって変更する
    ]
  end
  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]
# test/support/halt_acc.ex
defmodule HaltAcc do # elixirc_pathsで定義したpathに含まれる箇所でstruct定義を行う
  defstruct [:acc]

  defimpl Enumerable do
    def count(_lazy), do: {:error, __MODULE__}

    def member?(_lazy, _value), do: {:error, __MODULE__}

    def slice(_lazy), do: {:error, __MODULE__}

    def reduce(lazy, _acc, _fun) do
      {:halted, Enum.to_list(lazy.acc)}
    end
  end
end
# test/r_enum/stream_test.ecs
defmodule REnum.StreamTest do
  use ExUnit.Case, async: true

  doctest REnum.Stream.Native

  test "zip_with/2" do
    zip_fun = &List.to_tuple/1
    stream = %HaltAcc{acc: 1..3}
    # 成功!!
    assert REnum.Stream.zip_with([1..3, stream], zip_fun) |> Enum.to_list() == [
             {1, 1},
             {2, 2},
             {3, 3}
           ]
  end
end
  • test/support内に該当のstructを定義する
  • compile対象のコードを、MIX_ENV == testの場合のみ["lib", "test/support"]とする

※ elixirc_pathsの対象はdefaultだと"lib"以下のみです。(多分きっとelixir_compile_pathの略だと思ってます)

まとめ

こちらの記事でもありましたが、Elixirではちょいちょいcompileのタイミングを考慮する必要があるのかもしれません。

ここの辺りをもう少し調査していけば、Elixirの内部実装に関しても、もっと詳しくなれそうです。
(ライブラリの作成が終わったら是非チャレンジしてみたいと思います。)
もし、こちらの事象と解決策に関して、ズバリ何が起きているのか、ご存知の方がおりましたら、コメントか何かでご教授頂けますと幸いです。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0