Is there a better way to handle :ok tuples?

In any case - what exactly is stopping you from putting the “ugly code” into a stand-alone, clearly-named function?

5 Likes

There is no loop, it’s a list comprehension :slight_smile:

4 Likes

If You use named function You can use & &1 shortcut.

1 Like

I actually want to create a macro for all :ok tuples but my background is not enough to build something like that

I don’t follow …

defmodule Demo  do
  def create_time() do
    {:ok, time} = Time.new(3, 2, 00);
    {:ok, datetime} = NaiveDateTime.new(Date.utc_today(), time)
    datetime
  end
end

IO.inspect(Demo.create_time())
IO.inspect(Time.new(25, 2, 00))

Now clearly the tagged tuple is an irritant to you - but it serves a purpose

y$ elixir demo.exs
~N[2018-05-12 03:02:00]
{:error, :invalid_time}

It essentially communicates that the function cannot produce valid results over it’s entire input domain.

1 Like

You can do this stuff as part of the comprehension:

{:ok, time} = Time.new(3, 2, 00)
for x <- 1..30, 
    date = Date.utc_today(),
    {:ok, date_time} = NaiveDateTime.new(date, time) do
  MyApp.create_something(%{date_time: date_time})
end

EDIT:
Assuming the operations you’re performing with dates aren’t the same ones here. Otherwise, you could just define your datetime outside the comprehension.

1 Like

Am i the only one who doesn’t like :ok tuples? I prefer that when a function success just returns result without tuple, if fails traditional way {:error, reason}. Idk maybe there is a good reason for :ok tuples.

You risk promoting people doing:

result = foo()

If foo() succeeds then great, result has the result. If foo() fails however and returns an error then you have no idea.

3 Likes

It would require you to handle the error case first in much of your code or create awkward pattern matches.

case do_something() do
  {:error, reason} ->
    # Handle error
  value ->
    # Success case
end

case do_something() do
  value when is_integer(value) ->
    # Success case
  {:error, reason} ->
    # Handle error
end
2 Likes

{:ok, _} can be easily unwrapped in a with statement. I also prefer this form in function signature, but that is personal.

Sometime ago I also had that impression, " why I need to match a tuple when I just want to return the value" the problem is that what really happens when you do res = f() it’s actually doing a pattern matching, not the usual assignment, in that case you are accepting anything, an value or error, requiring you to do the pattern matching, which is more work. Now if you want the result or the process can blew up, some functions have a bang ! version which will raise if failed, like file = File.read!("foo.txt"), you can look up to that too

1 Like

In my mind that is “sloppy typing” - always returning a two element tuple where the consistently typed value of the first element is an indication of the type of the second value is much cleaner - and I suspect much better for pattern matching.

The :ok/:error tuple is a poor man’s implementation of Either. When used correctly it makes it easier to compose functions without having to specify explicit conditionals to deal with the errors - i.e. it enables railway-oriented programming (ROP).

defmodule Demo  do

  def f1({hour, minute, second}),
    do: Time.new(hour, minute, second)

  def f2({:ok, time}),
    do: NaiveDateTime.new(Date.utc_today(), time)
  def f2(other),
    do: other

  def f3(input),
    do: input
        |> f1()
        |> f2()

end

IO.inspect(Demo.f3({3,2,0}))
IO.inspect(Demo.f3({25,2,0}))
$ elixir demo.exs
{:ok, ~N[2018-05-12 03:02:00]}
{:error, :invalid_time}

4 Likes

But what if {:error, _} is a legit return value? How would you distinguish this from a real error?

Consider an Elixir Term Parser: Terms.parse("{:error, :badarg}").

If we were returning plain values on success but {:error, :badarg} on non-string input, we had a problem here.

But since it is common to return wrapped values in success cases, we can clearly and unambiguisly distinguish between {:ok, {:error, :badarg}} and {:error, :badarg}.


I do not understand your reasoning, seems to work:

iex(1)> for x <- 1..10 do
...(1)>   {:ok, time} = Time.new(3, 2, 0) 
...(1)>   {:ok, naive} = NaiveDateTime.new(Date.utc_today(), time)
...(1)>   %{date_time: naive}
...(1)> end
[
  %{date_time: ~N[2018-05-12 03:02:00]},
  %{date_time: ~N[2018-05-12 03:02:00]},
  %{date_time: ~N[2018-05-12 03:02:00]},
  %{date_time: ~N[2018-05-12 03:02:00]},
  %{date_time: ~N[2018-05-12 03:02:00]},
  %{date_time: ~N[2018-05-12 03:02:00]},
  %{date_time: ~N[2018-05-12 03:02:00]},
  %{date_time: ~N[2018-05-12 03:02:00]},
  %{date_time: ~N[2018-05-12 03:02:00]},
  %{date_time: ~N[2018-05-12 03:02:00]}
]
5 Likes

Because i was trying that after date_time:. Now i understand why it wasnt working.

You should look at expede’s “Exceptional” library on hex.pm then. :slight_smile:

Yeah, I am really liking how Rust is implementing it actually.

That is when {:ok, ...} should always be used as the success value then.

You could always have done this if you really wanted it ‘inline’ without using a function call:

# This:
(fn ->
  {:ok, date_time} =
    NaiveDateTime.new(
      Date.utc_today(),
      (fn ->
        {:ok, time} = Time.new(3, 2, 00)
        time
      end).()
    )
  date_time
end).()

# Could be done as this (not really readable):
({:ok, dt} = NaiveDateTime.new(Date.utc_today(), ({:ok, t} = Time.new(3, 2, 00); t)); dt)

# Or as (if you *know* your inputs are valid as shown above):
elem(NaiveDateTime.new(Date.utc_today(), elem(Time.new(3, 2, 00), 1), 1)

# Or standalone bindings are always nicely readable:
( {:ok, time} = Time.new(3, 2, 00)
  {:ok, date_time} = NaiveDateTime.new(Date.utc_today(), time)
  date_time

# Although what really should be done is putting it into a function that you then just call:
get_offset_date_time()  #  :-)
3 Likes

Bit late joining this discussion.

I’ve adopted the {:ok, value} or {:error, error} (where error will always be an Exception) style completely for my libraries and many of my functions are now just with/else/end statements.

I’ve found its better to adopt the style (almost) 100% - switching between the two is just too confusing as you never remember whether the result will be :ok/:error style or the “bare” value.

To support the style though I’ve had to write supporting functions, especially when Enum.map and the functions returns :ok/:error.

Depending on your taste the with/else/end may look awfully verbose or quite clean. After working with the style for a while now, I find the code looks quite “clean”: here for example is a module in one of my libraries.

2 Likes

That’s similar to the direction I’m heading, too.

Having:

  1. A clear signal something has gone right or wrong
  2. A rich error that can be matched against

is just too nice. You get to program the happy path and then handle the errors as broadly or as narrowly as you wish.

1 Like

Exactly the same here. I’ve ended up with {:ok, value} and {:error, %SomeException{}}, though I run everything through a normalize function that fixes things up well. This really is the Maybe/Either type in static typed languages and we’ve ended up just doing monad transformations on them, that’s essentially what with is, just a monadic do but a bit more verbose. Language support for it all would be awesome. :slight_smile:

2 Likes

Shhh, don’t tell anyone! Monads are scary! :wink:

We use this pattern quite a lot as well. Unfortunately, these values do not work so great in function pipelines. Usually its not a problem - often pipelines don’t need to explicitly handle failure, the consumers can match on the result, or they have that baked in to the data structure you are piping like with an Ecto.Multi. I have a utility function I employ though when I need maybe just a single non-result handler (a function that takes an untagged value) in the middle of a pipeline. Its just fmap (but don’t tell anyone, Functors are scary!):

  @type result :: {:ok | :error, any}

  @spec map_ok(result, (any -> any)) :: result
  def map_ok({:ok, value}, fun), do: {:ok, fun.(value)}
  def map_ok({:error, _} = error, _fun), do: error

You can employ it like so:

starting_value
|> result_producer()
|> map_ok(&non_result_consumer/1)
2 Likes

There are a variety of libraries that give you something like ~> instead of |> to do a ‘monadic pipe’ though, I use the exceptional library for example for that (as it handles a variety of such things). ^.^;

starting_value
|> result_producer()
~> non_result_consumer()

Or you can use exceptional’s ~> at all points regardless:

starting_value
~> result_producer()
~> non_result_consumer()

It has an option to override |> directly, which should be ‘generically’ safe except macro work and so forth (plus overriding built-in things is eh anyway), so it doesn’t do it by default (and suggests against it).

1 Like