What's the return type of .__struct__?

Question is straightforward.

date = DateTime.utc_now()
IO.inspect date.__struct__
date = NaiveDateTime.utc_now()
IO.inspect date.__struct__

Result of above codes is;

DateTime
NaiveDateTime

, instead of

"DateTime"
"NaiveDateTime"

or

%DateTime
%NaiveDateTime

Moreover, it can be directly used in codes like;

date = DateTime.utc_now()
IO.inspect date |> date.__struct__.to_string

, which returns

"2018-04-20 03:21:48.186032Z"

Here’s my question; what’s the return type of __struct__/0 and what’s the reason of built instead of String type?

At first I thought it’s weird, then I thought it’s so convenient in code syntax, and now I’m worried about its ambiguity as well as insecurity similar to String.to_atom/1 from external values.

Like any other module name in elixir it’s an atom. For erlang modules it’s quite apparent (:erlang, :os, …), but for elixir modules it’s not as obvious:
https://hexdocs.pm/elixir/syntax-reference.html#aliases

1 Like

I would recommend never relying on any field that has the form __something__. This “convenience” is incidental and there are much better ways of ensuring something can be printed (through protocols).

As @LostKobrakai said, the value of that field is a module (atom) simply referring to the module where the struct is defined.

Why would you want it to be a string, by the way? Unless you’re going to be deconstructing it and manipulating it an atom is always better, as it takes less space and is much faster for comparisons. (Some people would point out that atoms take space in the atom table, but in this case it’s already in the atom table because it’s a defined module as well, so we’re not taking extra space there, plus: It’s a far too common consideration for people. You have to be massively careless or have telecoms-level uptime for this to be an issue.)

1 Like

A little more detail:

Module names in Elixir are ‘simply’ atoms, but they have a slight bit of syntactic sugar:

iex> :"Elixir.Foo"
Foo

Note the :"Elixir."-part in here. The reason module names are prefixed by this, is to prevent name clashes with (pre-existing) Erlang module names. side-note: Yes, defining a module with a simple atom name like :foo is indeed possible, and is sometimes done to make it more natural to call Elixir code from within Erlang

So, %SomeStructName{}.__struct__ returns the name of the module it was defined in. This is used to perform pattern matching on struct type, and for protocol dispatching (which under the hood does exactly that). Do note that __struct__ is an implementation detail, and in general your code should not expect the field to be there; in future versions of Elixir, the way struct types are identified might change.


More generally, if you want to find out more information of what it is that you have in a variable or field, you can use the i helper in IEx:

i Date.utc_today.__struct__

07:27:08.040 [warn]  Non-unicode filename <<32, 234, 251, 82, 185, 85>> ignored

Term
  Date
Data type
  Atom
Module bytecode
  .asdf/installs/elixir/1.6.1/bin/../lib/elixir/ebin/Elixir.Date.beam
Source
  /home/ubuntu/bob/tmp/ae83a1578040bae08aad3293e21be16d/elixir/lib/elixir/lib/calendar/date.ex
Version
  [279493073830564593805028590010583389176]
Compile options
  [:debug_info]
Description
  Use h(Date) to access its documentation.
  Call Date.module_info() to access metadata.
Raw representation
  :"Elixir.Date"
Reference modules
  Module, Atom
Implemented protocols
  IEx.Info, String.Chars, Inspect, List.Chars

Oh, and you don’t have to be scared about the atom table being filled in this way: Since the name is the same atom as the one that was used for defining the module, it is already defined before.

1 Like

There is no such thing as date.__struct__.to_string, perhaps you mean date.__struct__ |> to_string?

But that should return "Elixir.DateTime" on your input.


Also, the __struct__ in foo.__struct__ is not a function, its a field in a map.


The reason it is an atom, is easy and simple, it always points at the module, that has the corresponding defstruct. You need to have this reference to be able to resolve the structs field at runtime.

It is not a string, because converting a string back to an atom is dangerous, as it can overflow your atom space.

But having an atom there directly is considered safe, as under normal circumstances atoms in that field will always reference a module that is in your codebase anyway, and every module loaded gets its place in the table of atoms. So by using it in __struct__ will not use any more space or entries in that table.


Last but not least, a little word of warning.

Even though its use is documented and a well known fact, I consider the __struct__ field an implementation detail and try to not access it directly.

In fact, there is no reason to do anymore, since we can use variables and underscore when matching and creating structs in recent versions of elixir.

%foo{bar: bar} = %Foo{bar: "bar"} # => foo will be Foo and bar be "bar"
%_{narf: _} = %Foo{narf: "narf"} # => will match any struct that has a field narf
2 Likes

Right, I agree it’s incidental and I’m worried about probable vulnerability.

And, I wrote a function which receives any kind of date types(Date, DateTime, NaiveDateTime), then change it to String, then slice first some characters.

I do care about atom table size, but that’s not an issue for this subject. Reason I described to_existing_atom/1 is, it’s an important supplement of to_atom/1 to protect from unexpected inputs. I couldn’t find like this for __struct__.

Fortunately my function is simply advantaging string interpolation as follows;

def dt_to_dhm(dt), do: String.slice("#{dt}", 0, 16)

What vulnerability would that be?

What was it you couldn’t find for __struct__?

Oh typo, thanks.

Actual code is,

date = DateTime.utc_now()
IO.inspect date.__struct__.to_string(date)
IO.inspect date |> date.__struct__.to_string()

Of course, both produces the almost-same result.

I don’t care nor worry about the atom type of module name since it seems efficient.

Thanks for your tip. Matching and creating struct looks useful in a certain cases.

Why are you doing string conversion this funny and complicated?

Why not just use Kernel.to_string/1?

Thanks. I think I was confused it’s vulnerable since I thought __struct__ is a function. But it doesn’t seem to have a vulnerability because __struct__ is a value.

At first I recognized the return type of __struct__ doesn’t look like any expected kind of common types; atom, map, struct, string. Instead they’re a CamelCase strings without quotations. Moreover they can be used in the codes itself.

I have still question that these presentations and use of them may lead misunderstanding or confusion; they’re atom that doesn’t start with :.

One of my early code was like this (maybe not exact);

def dtm_to_dh(date) do
  case date.__struct__ do
    Date          -> "sth.."
    DateTime      -> "sth.."
    NaiveDateTime -> "sth.."
    _             -> "sth.."
  end
end

If you’re that worried about that call them aliases, like they’re named in the syntax docs I linked above.

Also this is the more safe way to match for specific structs (meaning without relying on implementation details)

def dtm_to_dh(date) do
  case date do
    %Date{}          -> "sth.."
    %DateTime{}      -> "sth.."
    %NaiveDateTime{} -> "sth.."
    _             -> "sth.."
  end
end
2 Likes

Great, thanks for the improvement. More explicit syntax for me.