Aduril

Aduril

Hello there,
Whenever I setup a new project, there is a small function I always add: reply/1. What does it do?
In a LiveView mount, handle_event or handle_info
instead of writing
{:ok, stream(socket, :posts, Blog.list_posts())}
or
{:noreply, stream_insert(socket, :posts, post)}

one should be able to write

socket 
|> stream(:posts, Blog.list_posts())
|> reply(:ok)

or

socket
|> stream_insert(:posts, post)
|> reply(:noreply)

Therefore, a function called reply/1 could be added into Phoenix. It could be implemented somewhat like that:
def reply(socket, reply) when is_atom(reply), do: {reply, socket}

This could increase the readability (it’s faster to “scan” the code of your LiveView).
You can put that little function in your project, but I would suggest to make it a part of LiveView itself.

I am eager to hear your opinion on that :slight_smile:

Showing Posts 19 to 10

sodapopcan

sodapopcan

That monad thing is neat, though I personally don’t actually care that Elixir doesn’t have codified monads and kind of like that it doesn’t. Elixir hits this really nice mix of elegant and scrappy for me that not only makes it productive but also less gatekeep-y.

lkuty

lkuty

Yes, IMO this is the right way to see it in the context of a LiveView pipeline. We’re transforming the socket except in the last step when we’re returing a tuple. And as such we should not hide this in a opaque pipe step. But I like the shortcut |> noreply() too much because it is nice visually :blush:
More generally, a pipeline step transforms A into B which could be of a different type from A. Since the use of |> noreply() is the last step of the pipeline, it is more forgivable, even if it breaks the LiveView convention.
I just wanted to discuss the matter a little bit but I do not agree with incorporating that idea in Phoenix given the reasons you exposed.
As a side note, I’ve also used basic monadic code based on the idea of Railway Oriented Programming by Scott Wlaschin and a post on Medium. Stuff like:

defmacro left >>> right do
  quote do
    (fn ->
      case unquote(left) do
        {:ok, x}           -> x |> unquote(right)
        {:error, _} = expr -> expr
      end
    end).()
  end
end
sodapopcan

sodapopcan

I believe this is the wrong way to think about it. We’re certainly transforming a socket, but what we’re returning is a different value altogether that contains the socket but also additional information that is used for branching. If we really wanted to make it about piping a socket all the way through, that additional info would need to be part of the socket, ie, have a socket.reply key or something like that. But of course I don’t think it really belongs there, so we need to return a wrapper value. If Elixir had built-in monads then we could do something closer to what you’re talking about where we shove the wrapped monadic value all the way through from the get-go, but introducing a pipeable function is just masking what is going on and not actually solving what you’re talking about. All it does is give us a visual pipeline.

lkuty

lkuty

I agree and did something similar to make the code easier to read IMO. Some might have objections because it adds special conventions to the code base but it works well for me. Especially the noreply/1 which is super simple.

def ok(value, extra \\ nil)
def ok(value, nil), do: {:ok, value}
def ok(value, extra), do: {:ok, value, extra}
def error(reason), do: {:error, reason}
def reply(state, res), do: {:reply, res, state}
def noreply(state), do: {:noreply, state}
derek-zhou

derek-zhou

I love chaining pipeline myself; however, I try to limit the chaining to functions that return a value in the same shape. socket and {:noreply, socket} are not the same shape. My idiom for handle_event is:

def handle_event("something", _, %Socket{assigns: %{...}} = socket) do
  {:noreply, do_something(socket, ...)}
end

defp do_something(socket, ...) do
    socket
    |> assign(...)
    |> push_event(...)
end

I only use the handle_event to destructure the socket, and all operations on socket is done in do_something call, which ofen contains long chain of piping function calls.

Disclaimer: I don’t think my idiom is the one true idiom that everyone else should follow.

sodapopcan

sodapopcan

Just to add one more thing I really disliked like when I used this pattern was that any one-liner error cases (useful for small form that don’t need additional messaging than just the form error) suddenly became three lines, giving them far more visual weight than necessary.

What was:

  {:error, changeset} ->
    {:noreply, assign(socket, :form, to_form(changeset)}

became:

  {:error, changeset} ->
    socket
    |> assign(:form, to_form(changeset))
    |> noreply()

We could make it a one-liner like this:

  {:error, changeset} ->
    noreply(assign(socket, :form, to_form(changeset)))

which illustrates just how little value is gained here.

thiagomajesk

thiagomajesk

I was going to reply the same thing, and I think there’s a huge learning opportunity here about premature abstractions… In my experience, It’s rarely the case where your system’s data shares so many properties that you can just pipe it to infinity, but I think the main point is that you are essentially just trading one idiom for another, which is less expressive, more limited and only hides away something pretty easy to type.

To me at least, when I look at functions like ok(), reply(), or noreply() it looks like a leaky abstraction because it only hides the implementation details (if any) and it doesn’t remove enough cognitive load to justify its usage. For instance, if you are used to working with GenServers you know you can return a lot more on a :noreply result, so how useful is it really to abstract away a tuple by parametrizing its values?

All in all, I think the bigger picture is that you either get a lot of value from an abstraction that justifies its usage or you end up having an unnecessary one.

greven

greven

Agree with the sentiment of it’s up to personal preference, myself I stick to the tuple as I try to not deviate too much from the standards as it will be harder for someone new joining the project to grok all the “in-house rules”.

What I do personally is to have a shortcut in VSCode. So “nr” + tab becomes the no reply tuple. :slight_smile:

LostKobrakai

LostKobrakai

I think this is where the mismatch comes from. Indeed in the case of {:noreply, state} or {:ok, state} it feels like you’re only transforming state. But there are in many places other options as well.

  • mount/3 can also return {:ok, state, keyword}
  • handle_call/3 can also return {:reply, term, state}
  • handle_event/3 can also return {:reply, map, state}

Those callbacks are not just transformations of state, but transformations of state is just one of potentially many things they do and return information about. Sometimes those other things are even the only thing happening with no changes to state.

E.g. for me most simple callbacks look like this:

def handle_event("something", _, socket) do
  socket = 
    socket
    |> assign(a: :something)
    |> update(:b, fn x -> x + 1 end)

  {:noreply, socket}
end

The state transformation is neatly contained in a pipeline, but the return of that state transformation is separate to the transformation itself. It doesn’t belong in the pipeline. This becomes apparent if the code changes and you need to return a reply:

def handle_event("something", _, socket) do
  socket = 
    socket
    |> assign(a: :something)
    |> update(:b, fn x -> x + 1 end)

  {:reply, %{b: socket.assigns.b}, socket}
end

:noreply is literally telling the caller of the callback “there’s no reply to send for this one”. That’s not a state transformation.

Aduril

Aduril OP

I’m not against abstraction, but poor abstractions create a level of indirection that makes things harder to follow, so keep that in mind.

So far I think we are on the same side :wink:

I think this is mainly because a lot of people who first learn Elixir get mesmerized by how beautiful pipelines are and try to shoehorn everything in there (yeah, it’s cool I know, been there done that). But sometimes I find that this obsession tends to create an extra cost of overly abstracting things at the expense of readability.

I think, I see what you mean, but here the shoehorning is entirely not the case, isn’t it?
The paradigm of LiveView encourages exactly that pattern with a socket being transformed into another socket. This pattern breaks only at the end.
I would necessarily say that the reply function would be a big win regarding this, but for me it feels like a small imperfection within the framework to have a ceremony at the end that serves no real benefit for most of the basic use cases*. Do you see my point there?

Edit:
* given that there are of course use cases, where it serves a purpose. Though relevant, I personally so far encountered just a handful of those cases.

Where Next? Top

Trending in Proposals: Ideas Top

woylie
We are seeing a lot of warning logs like this: navigate event to "https://someurl" failed because you are redirecting across live_sessio...
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