EEP-79 native records and structs

I was reading the EEP-79, and thinking about the poor record support in Elixir(i’ve tried to discuss about that in the forum before). I see 2 clear advantages of native-records over structs, mainly it being a enforced natively by the runtime and the fact that it avoids creating modules just to define new data structures.

  1. on being enforced by the runtime:
    currently there is a misguiding property on structs that guarding against a struct %MyStruct{} is actually just a validation on %{__struct__: MyStruct}. while with native-records it gonna be enforced that it have all fields. this makes native-records more reliable, so you don’t accidentally use Map.drop/2 on a struct and passes a bad structured data.
  2. on avoiding creating modules just to hold a data structure:
    native-records are defined by #module.record{}, this allows to create multiple records in a single module, making it easy to handle and providing a better named scope for the data structure. this avoids the overuse of modules just to define a new struct.

I didn’t spot any mention to how native-records would work on distributed erlang and what would be the expected behavior on conflicting definitions when you send data from one node to another(that being one of my problems with relying too much on structs). but just those 2 points I think is a reason to prefer native-records.

I’m not sure if the core team is already discussing how to deal with native-records, but i’d really like it to be well supported by elixir(I still think named anonymous functions could be supported by elixir, for example, and I know I missed that train). So if the discussion is not supposed to be on closed doors, I’d like to start it here.

I personally see 3 possible ways to how it gonna unfold:

  1. no support or half-baked support just as it happens with current records.
  2. new semantics for records/native-records meaning probably new syntax around it.
  3. converge structs and native-records to be a single thing.

I really hope that 1 doesn’t happen, and 3 is too big of a heavy lift(even heavier if we consider the efforts on the type system) to be done right away. the more reasonable/sane approach would seem to be 2, maybe with converging both in a far future 2.0 elixir version.

but I really would like to know if there are any plans from the core team on that subject.

3 Likes

Native records does not check all fields, it only checks the fields you have listed + the module:record name.

From EEP-79:

When a native-record value is matched, its captured native-record definition is consulted.

So it behaves very similarly to how Structs except you cannot use the :maps API to manipulate it. This ties into how native records work with multiple versions, that is you can access old native records of the same module:record as long as you are only reading/changing fields that exist in it.

I don’t know which way that Elixir will take with native-records as Elixir has a similar problem as Erlang’s tuple records in this regard, that is the implementation of structs has leaked through into user code and thus it is hard/impossible to do a switch without the user deciding that it is safe to switch.

2 Likes

Slightly related, Structs allow to match on the module name, but it is not yet possible with native records - there is an issue already https://github.com/erlang/otp/issues/10861

Given that native records will be experimental in Erlang/OTP 29 (and maybe also in Erlang/OTP 30), it seems like this is the time to provide feedback like this :slight_smile:

the more reasonable/sane approach would seem to be 2 (new semantics for records/native-records meaning probably new syntax around it), maybe with converging both in a far future 2.0 elixir version.

I hope converging both, and maybe it’s a good reason for 2.0 to not be that far in the future but as soon as native records are stable, in one or two years? If structs could be implemented as native records, it would improve interoperability with Erlang libraries (and maybe Gleam could adopt them as well?).

1 Like

Unless I understood the EEP wrongly I guess it’s very different from structs given this:

When a native-record value is created, it “captures” key information from the current native-record definition, namely:

  • The fully qualified name of the native-record (module name and native-record name)

  • Field names

  • Whether it is exported (through -export_record)

so the moment you modify it to have a different shape, you gonna need to create a new data structure(being converting the native-record to map/proplist, or moving to a different native-record). structs even if you modify them, they keep being the same data structure(maps) and pass guards validation even if malformed data is given. I understand that elixir syntax sugars around structs try to guardrail against those malformed data, but just by being a new data structure native records have more guardrails around it then structs.

I agree, that’s why I think it would be good to discuss having it supported now and maybe in a very far future of elixir 2.0 have it converge. That’s my point to start the discussion. first highlighting how different it is from structs and why it’s good to have a first class support to it in Elixir even if we have structs.

Yep. for now I’d like to know what are the thoughts of Elixir core team on adding support to it, maybe making anonymous dot notation to work on it. and possibly access behavior implementation for it. probably around erlang 31 when the thing gets more solid.

i wouldn’t rush it. initially i just want it to not being ignored :slight_smile:

Citation needed for half-baked tuple record support. Record module exists for interoperability and I’m sure it could be improved. Have you tried submitting PRs for bugs or missing features?

2 Likes

2 years ago I asked in this forum if there was a reason for elixir records to not behave the same way they behave on erlang. i didn’t open a bug for this bc if the way it is right now is desired, there is no reason to fix it(and I assume is desired bc it’s a pretty obvious issue on implementation side).

I’m not saying it’s half-baked as an offense to maintainers, but as a description of a thing that works kinda the same but with it’s own oddities to be functionally usable but without being fully part of the language(and I understand that its a design choice to make stuff simpler, more syntax would be conflicting with struct and map dot notation for example).

If I need to define more than one struct in a module, I define them in bare modules that have nothing but defstruct and put all functions in the main module. The main module and the struct defining modules can still live in the same file, which is all that matters to me.
.

2 Likes

the problem is not creating files, and having a way to bypass creating a file doesn’t make it less of a problem.it reduces the friction, but doesn’t eliminate the problem that is: module name conflicting on being a function namespace or a data type identifier.

the two points I made was related to native records being a compelling feature that worth the effort of being fully brought to elixir(even if in a slow pace). i’m not trying to solve structs problems.

Doesn’t reducing the friction make it less of a problem? Unless your definition of “problem” is not what I have. Do you have an example to show why “module name conflicting on being a function namespace or a data type identifier” has made it hard to express what you want to express in Elixir?

module name is a an atom, and in elixir it’s atom prefixed by Elixir., atoms have a hard length limit on 255 characters. if you’re using module names not only as function namespace but also as a data type identifier, you have a tendency to add more stuff to that namespace, making it easier to offend the hard limit on 255 characters. and it can easily offended when people try to make structs describe deeply nested data.

keeping module name as just a namespace, and making the record have it’s own name inside that namespace, stops the tendency to deeply nest one namespace in the other. you can describe the nested data in the same module with each nested level being it’s own independent record and composing on them(as it already happens with xmerl for example).

I am struggling to understand what problem is OP trying to solve here.

I can agree structs being butchered by Map.take and Map.drop might be a problem, SOMETIMES, very rarely, but beyond that – what is the issue? (Not to mention that we can and should add a Credo rule to forbid structs ever being fed to these functions.)

I wouldn’t want to type 255-character long struct / record names anyway.

I agree records could give a bit more strictness but objectively, how big of a problem is that today? Genuine question. Good teams understand the limits of their tools and don’t allow those limits be tripped with lints.

Something that could be valid but was never relevant to me – distributed Erlang indeed, as OP mentioned. But I have zero visibility on this. What would the benefits be?

1 Like

I’m not trying to solve a problem. I’m describing the limitations of struct, to explain why I see native-records as a compelling new feature to the runtime.

please, read the full thread and the EEP.

what i’m not doing:

  • i’m not trying to solve a problem I have with structs.
  • I’m not trying to make structs better or advocate to people stop using structs.
  • I’m not saying the current records can substitute structs.

what I’m discussing:

  • a new feature/data-structure on the beam has converging usage as structs and compelling reasons to have a proper first-class citizen support in Elixir.
  • I understand that initially they can’t used interchangeably and that converging structs to native-records would be a a very heavy lift given all the efforts the core team are implement right now.
  • I think that this new feature/data-structure is worth the effort of being properly design and integrated to Elixir, and that it gonna take time from the core team to be done(more probably, new syntax and common features like dot notation and access behavior support).
  • to avoid being a second class citizen feature/data-structure(like current records) and drive adoption I think the conversations around it could be started now as the feature is introduced in Erlang.
2 Likes

I appreciate the initiative to try and find a really good use before the ecosystem takes an unexpected turn. I suppose I am always too focused on concrete problems to solve right away. Nasty bias from a busy job.