almeynman
almeynman

Reputation: 7418

elixir initialise struct with another struct

What if I have the following struct:

%Registration{email: "[email protected]", first_name: "John", last_name: "Doe"}

And I want to create two different structs from it:

%Account{email: "[email protected]", password: nil, ...}
%Profile{first_name: "John", last_name: "Doe", age: nil, ...}

I have already taken a look at this question, but if I do

registration = %Registration{email: "[email protected]", first_name: "John", last_name: "Doe"}
struct(Account, registration)

I receive protocol Enumerable not implemented for %Registration{...

So I have to use Map.from_struct

registration = %Registration{email: "[email protected]", first_name: "John", last_name: "Doe"}
struct(Account, Map.from_struct(registration))

But I think it is not very clean. So I was just thinking is there a better way to do this? This should be quite a common problem, right?

Upvotes: 1

Views: 1376

Answers (2)

Alex Bubnov
Alex Bubnov

Reputation: 123

You can do it easily, but in a bit hacky way - just delegate protocol functions to Enumerable.Map:


  defmodule Contact do
    defstruct [:name, group: "", version: "", status: :latent]

    defimpl Enumerable, for: Contact do
      defdelegate([count(e), member?(e, m), reduce(e, a, f)], 
                  to: Enumerable.Map)
    end
  end

or in a bit more proper way like


  defmodule Contact do
    defstruct [:name, group: "", version: "", status: :latent]

    defimpl Enumerable do
      alias Enumerable, as: E
      def count(c), do: Map.from_struct(c) |> E.count()
      def member?(c, m), do: Map.from_struct(c) |> E.member?(m)
      def reduce(c, a, f), do: Map.from_struct(c) |> E.reduce(a, f)
    end
  end

Enumerable.count implementation can be generated with macro to return struct members count which is already known at compile time. Enumerable.member? can also be generated with macro or written by hand or delegated again to Enumerable.Map.member? to remove runtime penalty of Map.from_struct.

All the code above is generic so it can be placed to use-able module like


defmodule StructEnumerable do
  defmacro __using__(opts \\ []) do
    quote do
      defimpl Enumerable do
        alias Enumerable, as: E
        def count(c), do: Map.from_struct(c) |> E.count()
        def member?(c, m), do: Map.from_struct(c) |> E.member?(m)
        def reduce(c, a, f), do: Map.from_struct(c) |> E.reduce(a, f)
      end

      # you can also add following code
      # to provide Fetch behaviour implementation
      # for your structs
      defdelegate([
        fetch(t, k),
        get_and_update(t, k, l)
      ], to: Map)
    end
  end
end

and embed it like


  defmodule Contact do
    defstruct [:name, group: "", version: "", status: :latent]
    use StructEnumerable
  end

Upvotes: 1

Sasha Fonseca
Sasha Fonseca

Reputation: 2313

As per the documentation:

Converts a struct to map. It accepts the struct module or a struct itself and simply removes the struct field from the struct.

The function exists for a reason. I don't see the way you did it as being a problem really. If it is a common problem I haven't seen it before. If you want to have a more expressive or cleaner code you may try:

registration = %Registration{email: "[email protected]", first_name: "John", last_name: "Doe"} |> Map.from_struct
struct(Account, registration)

or other equivalent code by checking the docs here.

Upvotes: 2

Related Questions