elt547

elt547

I have a struct representing a session request with only two fields, email and bytes. Basically the struct should either be passed around as immutable or created with a default value as a function return value. I have a function random_bytes which I want to be the default value of bytes.

Since you can’t declare it like this:
defstruct [:email, bytes: random_bytes()]

Someone suggested making a “new” function:

def new(email) do
  %__MODULE__{email: email, bytes: random_bytes()}
end

But to me this feels very non-idiomatic. It feels like shoehorning an object oriented pattern into a functional language.

What is the best way to build a strict with a complex default value in elixir?

Showing Posts 20 to 11

sodapopcan

sodapopcan

I personally don’t think this would be nice as it would mean coming across a wild %Foo{} means there could be side-effects. I’m perfectly happy with status quo where %Foo{} means “static thing I’m allow to manipulate” and Foo.new() means “I’m special, please let me construct myself!”.

cjbottaro

cjbottaro

I was trying to say “this is an example of what OP was asking for”. It doesn’t work, but it would be nice if it did.

ycherniavskyi

ycherniavskyi

Your example does not compile :man_shrugging:t2: (I have fixed some compile errors already):

Erlang/OTP 25 [erts-13.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace]

Interactive Elixir (1.13.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> defmodule Foo do
...(1)>   defstruct [:email, bytes: &random_bytes/0]
...(1)>
...(1)>   defp random_bytes do
...(1)>     :rand.uniform(100)
...(1)>   end
...(1)> end
** (CompileError) iex:2: undefined function random_bytes/0 (there is no such import)
    (elixir 1.13.4) expanding macro: Kernel.defstruct/1
iex(1)>
cjbottaro

cjbottaro

I think the post is really about “struct default values that are determined at runtime”…

defmodule Foo
  defstruct [:email, bytes: &random_bytes/0]

  defp random_bytes do
    :erlang.rand_bytes(16)
  end
end

Which admittedly would be kinda nice… :slight_smile:

al2o3cr

al2o3cr

Quick note: 99% of the time when you write struct you really mean struct! because

  • struct! will raise if required keys from @enforce_keys are omitted
  • struct! will raise if given keys that aren’t allowed in the struct

Another pattern that can catch bad keys at compile-time: use a struct literal combined with a function to apply defaults. Something like:

defmodule SessionRequest do
  defstruct [:email, :random_bytes]

  def setup(%__MODULE__{} = session) do
    Map.put_new_lazy(session, :bytes, fn -> random_bytes() end)
  end
end

# at the callsite:
session_request =
  %SessionRequest{
    email: some_email
  }
  |> SessionRequest.setup()

This is a little over-abstracted for the case where SessionRequest only has one field supplied by the user, but would make more sense if there were a dozen:

  • the compiler will catch invalid keys in the literal
  • if you’ve declared types for the specific keys, Dialyzer may catch errors like assigning a literal string to a key declared to be integer(). It will NOT catch misconfigured use of struct/struct!
cjbottaro

cjbottaro

I felt this way too until I divorced the word new from “object instantiation”.

I think the official Elixir documentation even says…

Just use data directly:

%__MODULE__{email: email, bytes: random_bytes()}

If that’s not good enough, make a function:

def new(email) do
  %__MODULE__{email: email, bytes: random_bytes()}
end

And finally… metaprogramming (no example… :stuck_out_tongue:).

See the “In other words:” part of this section:

tomkonidas

tomkonidas

We can even mix and match both solutions:

def new(attrs) do
  __MODULE__
  |> struct(attrs)
  |> maybe_with_random_bytes()
end

defp maybe_with_random_bytes(%{bytes: nil} = struct) do
  Map.put(struct, :bytes, random_bytes())
end

defp maybe_with_random_bytes(struct), do: struct
tomkonidas

tomkonidas

The thing is attrs is an Enumerable.t(), so it can be a map or a keyword. So that is why i do it after i create the struct.

But yea very valid if you want to have the two functions as you showed

msimonborg

msimonborg

Another option would be to just build the default opts with Keyword.put_new_lazy/3. Then you create the struct once without needing to update it, and you still only run the random_bytes() generator when necessary. I don’t know how this would compare performance-wise with the with statement plus the update.

def new(attrs) when is_list(attrs) do
  attrs = Keyword.put_new_lazy(attrs, :bytes, &random_bytes/0)
  struct(__MODULE__, attrs)
end

# optionally if you want to accept both list and map arguments
def new(attrs) when is_map(attrs), do: new(Enum.to_list(attrs))

I always use __MODULE__ just in case I want to change the module name down the line, I don’t have to update anything else in that file.

tomkonidas

tomkonidas

It can also be done as a case if you are more comfortable with that.

def new(attrs) do
  case struct(__MODULE__, attrs) do
    %{bytes: nil} = struct ->
      %{struct | bytes: random_bytes()}

    struct ->
      struct
  end
end

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews