Thank you Overmind, that's a great summary!
Honestly, I am a bit puzzle because this code:
happy_path do
{:ok, b} = a
{:ok, d} = b
c(d)
end
Is exactly the same as:
{:ok, b} = a
{:ok, d} = b
c(d)
i.e. Elixir developers already code the happy_path, so why wrap it in a do
block?
I also don't like it changes the semantics of = based on the existence of
else. In particular, with "with", I can explicitly choose in the same
construct if I want the conditional (<-) or the usual matching semantics
(=).
I do agree a common way of sharing the error handling code could be welcome
but the current syntax is a bit misleading (as handle_conn(conn) will be
called with 2 arguments and not 1).
Further thoughts? :D
*José Valim*
www.plataformatec.com.br
Skype: jv.ptec
Founder and Director of R&D
On Mon, Jun 20, 2016 at 7:29 PM, OvermindDL1 <[email protected]> wrote:
> Yes, sorry, thought the documentation helped at the link but it could use
> more examples. Three sections then. :-)
>
> *General Use*
> First of all happy_path uses matching syntax instead of <- syntax, so it
> flows far more naturally:
> ```elixir
>
> happy_path do
> {:ok, b} = a
> {:ok, d} = b
> c(d)end
>
> ```
> The above happy_path macro translates the given code into:
> ```elixir
>
> case(a) do
> {:ok, b} ->
> case (b) do
> {:ok, d} -> c(d)
> endend
>
> ```
> And like the `with` in Elixir 1.3 it also has an else:
> ```elixir
>
> happy_path do
> {:ok, b} = a
> c(b)else
> {:error, x} -> xend
>
> ```
> The else is matched to anything that does not match the `happy_path`, if
> no match is there then a normal `MatchError` is thrown by the `case`'s
> themselves if `happy_path!` was used, else the unmatched value is returned
> (I usually use `happy_path!`, but a few cases of `happy_path` are useful).
> I know the 'with' syntax of `<-` will not be changed because of backwards
> compatibility, but the above functionality could be added to the `do`
> section, would be nice to get a returning/throwing version distinction too
> like `happy_path`/`happy_path!`.
>
> The `happy_path` also supports guards:
> ```elixir
>
> happy_path do
> x when not is_nil(x) = some(foo)
> x + 1end
>
> ```
>
>
> *Default Handler*
>
> From my own code an example (mentally replace `happy_path` with `with` for
> comparison):
>
> ```elixir
>
> def index(conn, _params) do
> happy_path! do # This is normally a hairy set of embedded case's or a
> dozen function calls
> {:ok, {uid, _obj}} = verify_uid(uid)
> true = conn |> can?(index(%Perms.Auth.Permission{uid: uid, perm_name:
> :_}))
> query = from p in Permission,
> where: p.uid == ^uid
> conn
> |> render(:index,
> uid: uid,
> permissions: Repo.all(query),
> newable_permissions: MyServer.PermsLoader.get_all_perms(),
> )
> else
> {:error, :not_a_number} -> bad_request(conn)
> {:error, :invalid_uid} -> unprocessable_entity(conn)
> {:error, :atom_invalid} -> bad_request(conn)
> {:error, :struct_invalid} -> unprocessable_entity(conn)
> {:error, err} -> internal_server_error(conn)
> false -> unauthorized(conn)
> endend
>
> ```
>
> Lot of error cases, I ended up having a lot more so I branched it off into
> a separate function:
>
> ```elixir
>
> def index(conn, _params) do
> happy_path! do
> {:ok, {uid, _obj}} = verify_uid(uid)
> true = conn |> can?(index(%Perms.Auth.Permission{uid: uid, perm_name:
> :_}))
> query = from p in Permission,
> where: p.uid == ^uid
> conn
> |> render(:index,
> uid: uid,
> permissions: Repo.all(query),
> newable_permissions: MyServer.PermsLoader.get_all_perms(),
> ) else # Maybe some other cases first before falling to the
> generic one for this controller err -> handle_errors(err, conn)
> endend
> defp handle_error(%{valid?: false}, conn) ,do: bad_request(conn)
> defp handle_error({:error, :not_a_number}, conn) ,do: bad_request(conn)
> defp handle_error({:error, :invalid_uid}, conn) ,do: not_found(conn)
> defp handle_error({:error, :invalid_pidx}, conn) ,do: not_found(conn)
> defp handle_error({:error, :no_item_at_pidx}, conn) ,do: not_found(conn)#
> Snip a few more that eventually falls down to a catch-all that returns a 500
> error
>
> ```
> Which works and would with Elixir 1.3's with it looks like as well,
> however having that else at the bottom makes it easy to forget adding the
> default handler (over 30 of this style so far here, I've forgotten more
> than one a few times as I copy the happy_path header part around...).
> Happy recently added a method to allow setting a default handler that is
> called if there are no matches in the else branch:
>
> ```elixir
>
> def index(conn, _params) do
> happy_path(else: handle_errors(conn)) do
> {:ok, {uid, _obj}} = verify_uid(uid)
> true = conn |> can?(index(%Perms.Auth.Permission{uid: uid, perm_name:
> :_}))
> query = from p in Permission,
> where: p.uid == ^uid
> conn
> |> render(:index,
> uid: uid,
> permissions: Repo.all(query),
> newable_permissions: MyServer.PermsLoader.get_all_perms(),
> ) # else # # Maybe some other cases first before falling to
> the generic one for this controller
> # # In most cases this else block does not exist though, only for
> special cases, the
> # # default handles most. endend
>
> defp handle_error(%{valid?: false}, conn) ,do: bad_request(conn)
> defp handle_error({:error, :not_a_number}, conn) ,do: bad_request(conn)
> defp handle_error({:error, :invalid_uid}, conn) ,do: not_found(conn)
> defp handle_error({:error, :invalid_pidx}, conn) ,do: not_found(conn)
> defp handle_error({:error, :no_item_at_pidx}, conn) ,do: not_found(conn)#
> Snip a lot more that eventually falls down to a catch-all that returns a 500
> error
>
> ```
> Any error that happens that is not already handled by the else will be
> piped into the else default handler, so the `handle_errors(conn)` is
> effectively turned into a last `else` case of: err -> err
> |> handle_errors(conn)
>
>
> *Tags*
> This is a nice-to-have but not really something necessary, or perhaps even
> wanted (I would not put it in the base language as-it-is, maybe only after
> some syntax modification if at all, probably not)
>
> From the docs:
> ```elixir
>
> happy_path do # using the `foo` tag
> @foo {:ok, x} = y
> # is exactly the same as
> {:foo, {:ok, x}} = {:foo, y}else
> {:foo, {:error, e}} -> "Foo error"end
>
> ```
>
> And lastly an example from production code of how `happy_path` has saved
> my sanity from case-hell and/or function call explosion:
> ```elixir
> def delete(conn, %{"uid" => param_uid, "name" => param_name, "pidx" =>
> param_pidx} = params) do happy_path! do {:ok, {uid, _obj}} =
> verify_uid(param_uid) {:ok, {perm_struct, struct_name, name}} =
> verify_perm_exists(param_name) true = conn |> can?(delete(%Perms.Auth.
> Permission{uid: uid, perm_name: name})) {:ok, permission} =
> verify_single_record(Repo.get_by(Permission, uid: uid, perm_name: name)) {
> :ok, idx, _perm} = verify_pidx(permission.perms, param_pidx) {:ok,
> redirect_to} = case Map.get(params, "redirect", nil) do "show" -> {:ok,
> permissions_path(conn, :show, uid, name)} _ -> {:ok,
> permissions_path(conn, :index, uid)} end case List.delete_at(permission.perms,
> idx) do [] -> Repo.delete!(permission) conn |> put_flash(:info, gettext
> "Permission
> deleted successfully.") |> redirect(to: permissions_path(conn, :index,
> uid)) perms -> Permission.changeset(permission, %{perms: perms}) |>
> Repo.update!
> conn |> put_flash(:info, gettext "Permission deleted successfully.") |>
> redirect(to: redirect_to) end else err -> handle_error(conn, err) end end
> ```
>
> Consequently if anyone has suggestions or improvements on my code and/or
> coding style I would be much appreciative. :-)
>
> On Monday, June 20, 2016 at 10:20:17 AM UTC-6, José Valim wrote:
>>
>> Hey OvermindDL1!
>>
>> Everyone is welcome to propose new features to Elixir but it is your
>> responsibility to explain why you think it is a good addition. :) So please
>> do elaborate on the examples where you have used it and why you think they
>> are better than with. With more code samples and details, it is going to be
>> much more productive discussion.
>>
>> Thank you.
>>
>>
>>
>> *José Valim*
>> www.plataformatec.com.br
>> Skype: jv.ptec
>> Founder and Director of R&D
>>
>> On Mon, Jun 20, 2016 at 6:12 PM, OvermindDL1 <[email protected]> wrote:
>>
>>> The happy_path library ( https://github.com/vic/happy ) seems like it
>>> is `with` for Elixir 1.3, but with more features and runs on older Elixir
>>> versions; has it been thought about to add those features that it has over
>>> `with` to `with`? Specifically it handles tagging and setting a default
>>> error_handler for the unhandled error path are two that I find quite useful
>>> (especially in phoenix). I know it is likely too late for 1.3, but perhaps
>>> it would be a useful addition for 1.4? The `vic` repo in general seems to
>>> have a few useful little life-helper libraries for elixir that would be
>>> good to take a look at the design of to see if there is anything worth
>>> making available to the wider Elixir audience...
>>>
>>> --
>>> You received this message because you are subscribed to the Google
>>> Groups "elixir-lang-talk" group.
>>> To unsubscribe from this group and stop receiving emails from it, send
>>> an email to [email protected].
>>> To view this discussion on the web visit
>>> https://groups.google.com/d/msgid/elixir-lang-talk/3804944c-86eb-46a7-a581-7d3869ac6144%40googlegroups.com
>>> <https://groups.google.com/d/msgid/elixir-lang-talk/3804944c-86eb-46a7-a581-7d3869ac6144%40googlegroups.com?utm_medium=email&utm_source=footer>
>>> .
>>> For more options, visit https://groups.google.com/d/optout.
>>>
>>
>>
--
You received this message because you are subscribed to the Google Groups
"elixir-lang-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/elixir-lang-talk/CAGnRm4%2BE70%2BNmM0qCMHfVTmDSPhoBWoszXV3rURGL6CJJ9%2B%2BrA%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.