@enforce_keys and Kernel.struct

First of all

Happy New Year everyone

Disclaimer I did not find an issue on Github or a discussion here on this topic, if I missed it, my appologies

one of my resolutions have been to work on EarmarkParser, … again :blush: and during my refactorings I stumbled about a puzzling behavior

defmodule LP2.Parser.ListInfo do                                                                                                                                                                      
  use LP2.Types                                                                                                                                                                                       
                                                                                                                                                                                                      
  @enforce_keys [:bullet, :list_indent, :loose]                                                                                                                                                       
  defstruct bullet: nil, list_indent: nil, loose: false                                                                                                                                               
                                                                                                                                                                                                      
  @type t :: %__MODULE__{bullet: binary(), list_indent: non_neg_integer(), loose: boolean()}                                                                                                                                                                                                                                                   
end                                                       

please note the @enforce_keys, I verified that that works and then found, quite puzzled, that it does not :confused:

iex(1)> alias LP2.Parser.ListInfo
LP2.Parser.ListInfo
iex(2)> struct(ListInfo)
%LP2.Parser.ListInfo{bullet: nil, list_indent: nil, loose: false}

then I tried this

 iex(3)> %ListInfo{}
** (ArgumentError) the following keys must also be given when building struct LP2.Parser.ListInfo: [:bullet, :list_indent, :loose]
    (lp2 0.1.0) expanding struct: LP2.Parser.ListInfo.__struct__/1
    iex:3: (file)

which is good, of course, now my questions are

  • is the usage of struct discouraged?
  • if not, should the behavior of ignoring @enforce_keys not be considered as a bug?

KR
Robert

Seems there is an exclamation point version that does check @enforce_keys: https://hexdocs.pm/elixir/Kernel.html#struct!/2

2 Likes

oops, great than, sorry for the noise :blush:

“Discouraged” is probably too strong, but I’d generally suggest a different approach if you’re passing a compile-time constant for the struct name - generic libraries like Ecto have to use a function like struct, but it makes it harder for the compiler to catch errors.

For instance:

defmodule Demo do
  @enforce_keys [:required_thing]
  defstruct [:name, :required_thing]
end

# compile-time error
%Demo{naem: "oops"}

# run-time error
struct!(Demo, %{naem: "oops"})

# no error
struct(Demo, %{naem: "oops"})

Similarly, AFAIK Dialyzer can’t “see” through struct calls, so it’s possible to produce code that breaks the @type defined in the struct.

Also beware that “enforced keys” means precisely that - the key is present in the input:

iex(1)> defmodule Demo do
...(1)>   @enforce_keys [:name]
...(1)>   defstruct [:name]
...(1)> end
{:module, Demo,
 <<70, 79, 82, 49, 0, 0, 9, 152, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 1, 29,
   0, 0, 0, 28, 11, 69, 108, 105, 120, 105, 114, 46, 68, 101, 109, 111, 8, 95,
   95, 105, 110, 102, 111, 95, 95, 10, 97, ...>>, %Demo{name: nil}}
iex(2)> struct!(Demo, %{name: nil})
%Demo{name: nil}
5 Likes

Exactly this. struct is vital in certain complex macros where it might not be always possible to compile the struct-module before the macro-module.
In normal code, however, we really do want the struct module to be compiled first, exactly because (a) then it is possible to perform an @enforce_keys-check and (b) preventing cyclic dependencies in general is a good idea for maintainability.

1 Like

yes very well put, I changed all my occurrences of struct to struct! after the first reply.

I wonder if I should make a PR for the doc of Kernel.struct underlining the dangers of struct a little bit better?

Do you feel that the current documentation transpires the points you have made?

which can be found here