Is there a better way to handle :ok tuples?

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

I have that ugly code multiple functions return {:ok, what_i_need } tuple. That is why i use anonymous functions is there better way handle :ok tuples?

What’s wrong with

{:ok, time} = Time.new(3, 2, 00)
{:ok, naive_date_time} = NaiveDateTime.new(Date.utc_today(), time)

?

EDIT: alternatively, you could also use a with expression:

with {:ok, time} <- Time.new(3, 2, 00),
  {:ok, naive_date_time} <- NaiveDateTime.new(Date.utc_today(), time)
do
  # use naive_date_time here
else
  {:error, err} -> # handle any error
end
1 Like

Problem is i need use in for loop

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

                 date_time
               end).()
        })

So i cant use this

{:ok, time} = Time.new(3, 2, 00)
{:ok, naive_date_time} = NaiveDateTime.new(Date.utc_today(), time)

But with looks good thanks

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
3 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}

6 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