Why does `elem/2` exist?

I’ve been in the Elixir world for five years—not super long but also not super short. In this time I have seen lots of code that uses elem/2 but not once have I ever seen a situation where it made code clearer and in fact has always made code less clear. It seems its main use it to “not break pipelines.” To put it lightly: this is a non-goal. I have never run into a situation where pattern matching isn’t way way way waaaaaaaay clearer than elem/2, even if it means breaking the pipe.

IMO if we ever see an Elixir 2.0 this function should be removed from the language. But please tell me why elem/2 is indispensable to your Elixir work! I realize I’m using very dismissive language but I’m genuinely curious if there is a good use-case for it I’ve never thought of.

1 Like

Sometimes I just want to write a one-liner or pipeline and I’m too lazy to get fancy with the error handling. :man_shrugging:

  • You have tuples, a fixed size array of elements.
  • You can access them by a random index.
  • There is no syntax to access a random index of a tuple
  • Pattern matching requires you to know the length of the tuple up-front

I think it makes sense to have such a function be part of the standard library. The reasoning is quite simple: if you provide a builtin data structure, it makes sense to provide an API to read and write to it.

Wether or not it’s idiomatic to access a random element of a tuple is a whole different topic.

14 Likes

Agree 100% with doorgan.

With regards to elem/2 in pipelines, maybe a credo check for this would be useful.

2 Likes

elem/2 can be used in guards, pattern matching cannot

elem/2 is O(1) access to a vector (the only other O(1) example is binaries).

Pattern matching with large tuples is verbose and it’s easy to get the wrong element (why count wildcard matches when you can write the correct index into a function argument).

In the future, it seems we will get O(1) update of tuples in contexts where the compiler knows there are no shared references. So maybe time to incorporate :erlang.setelement/3 into Elixir somehow.

2 Likes

Pattern matching with large tuples is verbose and it’s easy to get the wrong element

If you are on control of the data: Use Records to overcome this issue

1 Like

Agreed, but it has to be noted that then – which solves the problem of not breaking pipelines – was only added in Elixir 1.12.

Use then.

4 Likes

Thanks for all the responses?

Ya, what I’m really asking is: “Does anyone have an example of an exemplary use of elem/2?” I’ve never been in a situation where I don’t know the size of a tuple. I always think of the size as being part of the type as it in other functional languages. But thanks for the thoughtful answer, all that does make sense!

I actually wasn’t aware you could use elem in guards! I still think pattern matching is clearer.

The question here is why do you have large tuples? I don’t think I’ve come across one with more than 4 before, though I could be wrong. The type of work I do is very limited to business web apps, but large tuples seem like a massive anti-pattern to me.

I’m having trouble finding it but I remember the docs recommending against put_elem and to pattern match and create a new tuple instead. Though maybe it’s gone and no longer the advice anymore? Or I’m just not looking hard enough.

This interesting as I’m aware of but never used records before.

1 Like

I particularly wouldn’t use records in elixir, I think the semantics are not particularly well ported. the only use I see for records in elixir is to interact with erlang existing records. I was even thinking to open an issue/suggest changes on that in the mailing list but didn’t had time… in this topic I showed a gist of it:
https://elixirforum.com/t/any-reason-for-the-different-semantics-with-records-in-elixir/

but I guess i’m going too off-topic here :smile:

I don’t actually have a problem with it in any apps I work on, even at work, was just generally looking for a case where pattern matching isn’t better. Though it could actually be a fun but jarring addition to recode :sweat_smile:

Everyone is always free to go off topic in any thread I create :sweat_smile:

3 Likes

It’s main use is accessing a tuple value using an index, which works no matter the size of tuple and in context, where pattern matching is not an option. Just like there’s :erlang.map_get for maps, because pattern matching is not enough.

There’s places in OTP, where you might get differently sized tuples, where leading parts of them contain the same data. You can use elem/2 to select the common fields without needing to know which specific tuple you got if the differenciation doesn’t matter for the code. It also means your code doesn’t break if OTP decides to add another version of tuple with yet another additional datapoint added to the end.

If flag timestamp, strict_monotonic_timestamp, or monotonic_timestamp is specified, the first tuple element is trace_ts instead, and the time stamp is added as an extra element last in the message tuple.

https://www.erlang.org/doc/apps/kernel/trace.html#process/4

Usually one would likely use maps for such a usecase, but sticking to tuples for performance sensitive calls like around tracing is also a good idea.

Also e.g. elixir’s record handling code wouldn’t be a lot more complex macro magic without having such functionality. Technically it doesn’t use Kernel.elem/2, but it optimizes by calling :erlang.element/2 directly, which is also what Kernel.elem/2 is using.

3 Likes

Agreed about performance. I see nothing that does not allow for multi-headed case however. I still don’t see the value of elem/2 in these cases. :thinking:

I belive it’s not a question of how often it’s used but of having handy and easy way of random access to a tuple in elixir. Also that elixir changes the indexing of tuples/list in erlang, erlang indexes starts at 1, elixir ones starts at 0… so having the counterpart of the element/2 erlang function in elixir that follows the index pattern that elixir establishes is needed IMO.

1 Like

Short short version: elem/2 exists because typing :erlang.element is too many characters.

Record also provides some examples of places where elem (or it’s Erlang equivalent) are useful, for instance is_record?:


My interpretation of the release notes about setelement optimizations is that sequences like this one (also from Record) may get a boost from the compiler and/or JIT:

Since the intermediate tuples don’t escape the function, the reduce could do all the mutation in-place.

3 Likes

An example from a project I contribute to: Lexical.Document.Lines

The key insight is that a tuple provides O(1) access to any of its elements, whereas a list is O(n). So if you need a data structure that provides fast access to any arbitrary line of a document, you reach for a tuple. (And then you use elem/2 for access, since you don’t know what its length will be.)

3 Likes

I still don’t get this, how could you not know? I mean there are several variants, right? It’s not like these variants are 1000+. What’s the problem with multi-headed case?

See here; the tuple size is however many lines the document is long. So a file with 100 lines will have a 100-element tuple. A file with 2000 lines will have a 2000-element tuple.

2 Likes

Aha, I see, reading files (and the like) into memory.

I was thinking about how e.g. Floki does stuff where you always get a recursive tri-tuple.

Thanks Zach! A real world example-usage of large tuples of unknown size is exactly what I was looking for, I just didn’t ask the question very well …and I may or may not have been a little tipsy when I opened the topic :grimacing: I couldn’t for the life of me think of an example for myself. I guess elem/2 can stay :wink:

And hey, I use that project you contribute to!

1 Like