Unexpected match failure against a struct

I’m having a weird matching problem. I’m handling complex numbers using the ComplexNum library. This library defines two types of complex number, ComplexNum.Cartesian and ComplexNum.Polar, but I only use the Cartesian variety. So I have following set up (showing only the relevant bits):

defmodule TopLevel
  alias ComplexNum.Cartesian, as: Complex

  defmodule SubModule

    defp convert(v) do
      Logger.debug(inspect v)
      case v do
        s when is_binary(s) -> {:ok, s}
        i when is_integer(i) -> {:ok, i}
        f when is_float(f) -> {:ok, f}
        c = %Complex{} -> {:ok, c}
        ms = %MyStruct{} -> convert_my_struct(ms)
        # some other clauses omitted
        %{} -> convert_map(v)
        # some other clauses omitted
        _ -> {:error, :not_implemented}
      end
    end

    defp convert_map(v) do
      # Use reduce_while to bail if errors inside the map
      Enum.reduce_while(v, acc, fn {key, value}, acc ->
        # details omitted
      end)
      # details omitted
    end

  end
end

When convert() is called with a Complex during testing, the match clause against the Complex alias fails, and the more generalised match against a map is triggered, leading to an error in convert_map():

10:57:28.797 [debug] #ComplexNum (Cartesian) <2.0·𝑖>


  1) test test_name (SomeTest)
     test/some_test.exs:9124
     ** (Protocol.UndefinedError) protocol Enumerable not implemented for #ComplexNum (Cartesian) <2.0·𝑖> of type ComplexNum (a struct). This protocol is implemented for the following type(s): MapSet, HashSet, Range, Function, HashDict, File.Stream, GenEvent.Stream, List, Stream, IO.Stream, Date.Range, Map
     code: Enum.each(test_cases, fn tcase ->
     stacktrace:
       (elixir) lib/enum.ex:1: Enumerable.impl_for!/1
       (elixir) lib/enum.ex:141: Enumerable.reduce/3
       (elixir) lib/enum.ex:1998: Enum.reduce_while/3

Note that when a MyStruct is passed, the correct match clause is triggered and convert_my_struct() called.

I’ve used the alias in other contexts without problems, so I don’t think it’s that. Can anyone point out what is causing the match clause against %Complex{} to fail? Thanks.

Hello and welcome,

The error suggests something else… maybe You tried to use Enum on the struct.

Please show your failing test (some_test.exs)

1 Like

What Koko said, but also: Complex is aliased to ComplexNum.Cartesian and the error is talking about a ComplexNum type.

Looks like the reduce_while in convert_map isn’t getting a list but just a single ComplexNum struct.

3 Likes

Well, after reading the error, it’s the reduce while on the struct…

This is the same error when trying to Enum.map a struct.

iex> Enum.map %Core.Event{}, fn {k, v} -> {k, v} end
** (Protocol.UndefinedError) protocol Enumerable not implemented for
...

# but it works on a map, of course
iex> Enum.map %{}, fn {k, v} -> {k, v} end          
[]

You could…

Enum.reduce_while(Map.from_struct(v), acc, fn {key, value}, acc ->
  # details omitted
end)

Do not forget structs have special keys, like

:__struct__, :__meta__
3 Likes

Thanks for the welcome and response. The test won’t be useful to show (this error occurs in nested code not immediately obvious from the code in the test).However, the point I’m making is that convert_map() should never have been called on a ComplexNum because there is an earlier clause c = %Complex{} -> {:ok, c} matching specifically that struct, so the Enum.reduce_while() shouldn’t even be reached when a Complex is passed to convert(), because convert_map() shouldn’t be called in that case.

It should work, but it’s quite complicate to check why it’s not…

You might try some simplification, and check if it is working.

Like not using alias, or using alias without as. This is simple to test for You.

But not for us. You might also provide some sample code to test (not the one in this post)

1 Like

Change this line to:

Logger.debug(inspect(v, structs: false)

Then we can get a look at the data you actually have without the inspect protocol implementation hiding things.

2 Likes

This is the issue. Your match is not matching because you’re matching a different struct. The struct you have is ComplexNum, not ComplexNum.Cartesian.

3 Likes

Indeed! Author of the ComplexNum-package here: There is only a single ComplexNum struct, whose mode-field indicates whether it is in cartesian or polar notation.

To be honest, I do not remember why I made the decision back in 2017 to have a single struct rather than two separate ones. Maybe to make pattern-matching on ‘any complex number’ easier? Maybe to allow for sharing of the same protocol implementations?
If I were to re-write the library today I would probably have two separate structs and use a tiny bit of metaprogramming to reduce code repetition between them.
But that is neither here nor there.

The problem in your code is that you have:

  • aliased ComplexNum.Cartesian to Complex
  • Then pattern match on a %Complex{}, AKA a %ComplexNum.Cartesian{}-struct which does not exist.
    Instead, pattern match on %ComplexNum{} or %ComplexNum{mode: ComplexNum.Cartesian} if you need to restrict the input to allow only complex numbers in cartesian notation.
7 Likes

Indeed, that was it! Thanks to you all for your help. I’m just dipping my toe in the water here, and I love that this community is so helpful and welcoming!

2 Likes