Coder Social home page Coder Social logo

crowdhailer / ok Goto Github PK

View Code? Open in Web Editor NEW
600.0 10.0 20.0 122 KB

Elegant error/exception handling in Elixir, with result monads.

Home Page: http://insights.workshop14.io/2015/10/18/handling-errors-in-elixir-no-one-say-monad.html

License: Apache License 2.0

Elixir 100.00%
pipeline monad macros elixir elixir-pipelines

ok's Introduction

OK

Elegant error/exception handling in Elixir, with result monads.

Hex pm Build Status License

Result tuples

The OK module works with result tuples by treating them as a result monad.

{:ok, value} | {:error, reason}

See Handling Errors in Elixir for a more detailed explanation.

See FAQ at end of README for a few common question.

OK.for

OK.for/1 combines several functions that may fail.

  • Use the <- operator to match & extract a value for an :ok tuple.
  • Use the = operator as you normally would for pattern matching an untagged result.
require OK

OK.for do
  user <- fetch_user(1)             # `<-` operator means func returns {:ok, user}
  cart <- fetch_cart(1)             # `<-` again, {:ok, cart}
  order = checkout(cart, user)      # `=` allows pattern matching on non-tagged funcs
  saved_order <- save_order(order)
after
  saved_order                       # Value will be wrapped if not already a result tuple
end

OK.for/1 guarantees that it's return value is also in the structure of a result tuple.

OK.try

OK.try/1 combines several functions that may fail, and handles errors.

This is useful when writing code that has it's own representation of errors. e.g. HTTP Responses.

For example when using raxx to build responses the following code will always return a response.

require OK
import Raxx

OK.try do
  user <- fetch_user(1)             # `<-` operator means func returns {:ok, user}
  cart <- fetch_cart(1)             # `<-` again, {:ok, cart}
  order = checkout(cart, user)      # `=` allows pattern matching on non-tagged funcs
  saved_order <- save_order(order)
after
  response(:created)                # Value will be returned unwrapped
rescue
  :user_not_found ->
    response(:not_found)
  :could_not_save ->
    response(:internal_server_error)
end

OK Pipe

The pipe (~>>) is equivalent to bind/flat_map. The pipe (~>) is equivalent to map.

These macros allows pipelining result tuples through multiple functions for an extremely concise happy path.

use OK.Pipe

def get_employee_data(file, name) do
  {:ok, file}
  ~>> File.read
  ~> String.upcase
end

Use ~>> for File.read because it returns a result tuple. Use ~> for String.upcase because it returns a bare value that should be wrapped in an ok tuple.

Semantic matches

OK provides macros for matching on success and failure cases. This allows for code to check if a result returned from a function was a success or failure while hiding implementation details about how that result is structured.

import OK, only: [success: 1, failure: 1]

case fetch_user(id) do
  success(user) ->
    user
  failure(:not_found) ->
    create_guest_user()
end

FAQ

Why does OK not catch raised errors?

For the main rational behind this decision see the article Errors are not exceptional

Two other reasons:

  • Exceptional input and errors are not the same thing, OK leaves raising exceptions as a way to handle errors that should never happen.
  • Calls inside try/1 are not tail recursive since the VM needs to keep the stacktrace in case an exception happens. see source.

What about other shapes of error and success?

  • Accepting any extra forms is a slippery slope, and they are not always unambiguous. If a library is not returning errors as you like it is very easy to wrap in a custom function.

    def fetch_foo(map) do
      case Map.fetch(map, :foo) do
        {:ok, foo} -> {:ok, foo}
        :error -> {:error, :no_foo}
      end
    end

What changed in version 2.0

  • OK.with was deprecated.
  • use OK.Pipe was added.
  • OK.bind was renamed OK.flat_map.

Additional External Links and Resources

ok's People

Contributors

andersonmcook avatar anonyfox avatar crowdhailer avatar ecly avatar istefo avatar jaminthorns avatar jpittis avatar nurges avatar seymores avatar thomasnaudin avatar vic avatar wraiford avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ok's Issues

use OK.Pipes

handle importing the pipeline operators.
user OK.Pipes or use OK.Pipeline

Raise case clause error with correct line reference

Inside a block if a value that is not understood(any value that is not a result tuple) is passed to a bind operator, the error that is raised references the start of the block and not the line of the bind operation.

Dialyzer complains on `~>>`

Hello,

The following snipplet

defmodule Test do
  import OK, only: ["~>>": 2]

  defp check1(value) do
    cond do
      value > 5 -> {:ok, value}
      true -> {:error, "abc"}
    end
  end

  defp check2(value) do
    cond do
      value > 7 -> {:ok, value}
      true -> {:error, "def"}
    end
  end

  @spec hello(integer) :: {:ok, integer} | {:error, String.t}
  def hello(v) do
    {:ok, v}
    ~>> check1
    ~>> check2
  end
end

Complains with

lib/test.ex:21: The pattern {'error', _@2} can never match the type {'ok',_}

any ideas how to manage that. The code looks correct.

options for with

complete - handle every error case
pure - return values don't have to be result tuple

How to return values from a result block

Take the following example, which is available in the test suite

OK.try do
  a <- safe_div(8, 2)
  b <- safe_div(a, 2)
  _ <- {:ok, a + b}
end

Wrapping the result in an ok tuple and matching it to a throw away variable is noise, that I would like to remove.

Currently replacing the last line with the following alternative will error.

OK.try do
  ...
  a + b
end

This is probably the correct behaviour, if we allow the above we have to assume the last line is a success. One alternative is to assume the last line always returns a result tuple. Then the last line can be simplified to

OK.try do
  ...
  success(a + b)
  failure(:reason)
end

A more advanced option would be to add a yield block.

OK.try do
  ...
  yield
  a + b
end

The problem with this is that yield is not a keyword like else to try/yield/end will not look the same as if/else/end.
Could use else as the start of the yield block, messy if we ever decide to use else for some error handling.
Could see if there is a way to add yield or another keyword to behave the same way as else

Suggestion: Allow Only Pipes in OK.With

I really like the organizational structure of the

OK.with do
   stuff <- stuff
else
  match -> error
end

For grouping error handling all in one place. However, I find the stuff <- stuff syntax has me creating temp variables that just exist to pass into the next step.

It'd be nice to have a block without any <- segments.

It would turn a method like this:

def update(%{id: id, decision: attrs}, _resolution) do
  OK.with do
    decision <- do_get(id)
    Structure.update_decision(decision, attrs)
  else
    "does not exist" = message -> OK.failure(message)
    %{} = changeset -> OK.failure(changeset.errors)
  end
end

into this

def update(%{id: id, decision: attrs}, _resolution) do
  OK.with do
    {:ok, id} 
    ~>> do_get()
    ~>> Structure.update_decision(attrs)
  else
    "does not exist" = message -> OK.failure(message)
    %{} = changeset -> OK.failure(changeset.errors)
  end
end

Perhaps this could be called something like OK.with_pipe

OK.with and ~>>, OK.pipe macro?

Wow. I'm using the lib again this morning and I've been noticing that I am using ~>> operator in tandem with OK.with a lot. This is super sweet. Like I've said, I use with extensively, and as such, the |> becomes much less useful for me. But ~>> gives me piping back within with statements, and this makes for a powerfully readable happy path. Take the following code block using native with:

with(
  # We will query off of the current identity
  {:ok, current_identity} <- IbGib.Expression.Supervisor.start_expression(Enum.at(identity_ib_gibs, 0)),

  # Our search for the latest version must be using the credentials of
  # **that** ibgib's identities, i.e. in that timeline.
  {:ok, src} <-
    IbGib.Expression.Supervisor.start_expression(src_ib_gib),
  {:ok, src_ib_gib_info} <- src |> get_info(),
  {:ok, src_ib_gib_identity_ib_gibs} <-
    get_rel8ns(src_ib_gib_info, "identity", [error_on_not_found: true]),

  # Build the query options
  {:ok, query_opts} <-
    build_query_opts_latest(src_ib_gib_identity_ib_gibs, src_ib_gib),

  # Execute the query itself and extract its single result ib^gib
  {:ok, query_result} <-
    current_identity |> query(identity_ib_gibs, query_opts),
  {:ok, query_result_info} <- query_result |> get_info(),
  {:ok, result_ib_gib} <-
    extract_result_ib_gib(query_result_info, src_ib_gib)
) do
  {:ok, result_ib_gib}
else
  error -> default_handle_error(error)
end

I write comments sometimes when I feel the code is not self-documenting enough, and when there are subtleties, e.g. here it is not obvious why I have to use the src's identity (working with branching timelines is tough to reason about!)

This is where the ~>> really comes in handy. Here is the refactored code:

OK.with do
  # We will query off of the current identity (1st, arbitrarily)
  current_identity <-
    identity_ib_gibs 
    |> Enum.at(0) 
    |> IbGib.Expression.Supervisor.start_expression()

  # Our search for the latest version must be using the credentials of
  # **that** ibgib's identities, i.e. in that timeline.
  src_ib_gib_identity_ib_gibs <- 
    {:ok, src_ib_gib}
    ~>> IbGib.Expression.Supervisor.start_expression()
    ~>> get_info()
    ~>> get_rel8ns("identity", [error_on_not_found: true])

  # Build the query options
  query_opts <-
    build_query_opts_latest(src_ib_gib_identity_ib_gibs, src_ib_gib)

  # Execute the query itself and pull the result_ib_gib out.
  result_ib_gib <-
    {:ok, current_identity} 
    ~>> query(identity_ib_gibs, query_opts)
    ~>> get_info()
    ~>> extract_result_ib_gib(src_ib_gib)

  OK.success result_ib_gib 
else
  reason -> OK.failure handle_ok_error(reason, log: true)
end

This is waaaay cleaner, since piping is so powerfully concise and readable, and it's used in combination with the power of the result monad and OK.with.

I'm creating this issue because I think it would be useful to have an optional macro for starting the pipe, since I personally think it's good to keep the tuples out of OK.with. For example, instead of

result_ib_gib <-
  {:ok, current_identity} 
  ~>> query(identity_ib_gibs, query_opts)
  ~>> get_info()
  ~>> extract_result_ib_gib(src_ib_gib)

it could be...

result_ib_gib <-
  OK.pipe current_identity
  ~>> query(identity_ib_gibs, query_opts)
  ~>> get_info()
  ~>> extract_result_ib_gib(src_ib_gib)

This may be macro overkill though, but I wanted to discuss the possibility.

Regardless about the macro, I think the combination of OK.with and ~>> is a powerful feature of the lib that you should consider adding to the documentation (or I could if you like the idea). ๐Ÿ‘

Implement a map

use ~>. This should map the value inside an ok, but not expect a result tuple to be returned

{:ok, 5}
~>  add1()
# => {:ok, 6}

{:error, :some_error}
~>  add1()
# {:error, :some_error}

Note. this should make use of OK.map(tuple, wrapped_anon_fn)

Add mapM / sequence to OK.

OK's ~>> operator has been really useful for error handling. However, it causes me frustration when mapping over a list with a function that can cause errors.

def parse(input) do
  parse_into_list(input)
  ~>> validate_length(10)
  ~>> Enum.map(&parse_item/1)
end

parse returns a list of {:ok, parsed_item} and {:error, msg} but what we really want is either a list of parsed_item or a single {:error, msg}. In Haskell, this is often what we use sequence for.

def sequence(li) do
  result = Enum.reduce_while li, [], fn
    {:ok, val}, acc -> {:cont, [val | acc]}
    {:error, _} = err, _acc -> {:halt, err}
  end
  if is_list(result) do Enum.reverse(result) else result end
end

sequence has type [m a] -> m [a] or in OK's case [Result a] -> Result [a].

We can now take our function from before and use sequence on it.

~>> Enum.map(&parse_item/1) |> sequence

This will either return an {:ok, [parsed_item1, parsed_item2, ...]} or the first error the map encountered.

We would rarely use sequence directly, we'd instead use a function called mapM which does the combined map and sequence in one go!

def mapM(li, fun) do
  Enum.map(li, fun) |> sequence
end

I use mapM in chains of error handling when I want to pass the resulting map on to another function with ~>>.

def parse(input) do
  parse_into_list(input)
  ~>> validate_length(10)
  ~>> mapM(&parse_item/1)
  ~>> do_more_stuff_with_a_list_of_parsed_items
end

I want to add mapM to the OK library.

  • I don't know if the implementation I've written above is the one we'd want.
  • I don't know if we want to call the function mapM and sequence rather than less scary names.
  • I would be pleased to open a PR and go through the review process.

Handle different shapes/styles of success & failure tuples

It would be great if we could capture different types of successes from various third party libraries as well as the failures that can occurs.
Not everything is shaped {:ok, value} and it would be awesome to get to see that normalised to common ones as well as capturing exceptions and treating them as errors.

Edit:

From the looks of it, I'd assume it would be adding multiple version of bind for different 'shapes'

 def bind({:success, value}, func) when is_function(func, 1), do: func.(value)
 def bind(failure = {:failure, _reason}, _func), do: failure

 def bind(value, func) when is_number(value), is_function(func, 1), do: func.(value)
 def bind(failure = {:failure}, _func), do: failure

Just shooting from the hip.

can I match the return text from the touple {:error, text} in case of error

Hi, would be possible write something like this

def validate_text(text) when is_bitstring(text), do: {:ok,text}
def validate_text(_text), do: {:error, "not a valid text to translate"}

#and then if I put

#OK block here
some_text <- validate_text("ok I pass")
wrong_text<- validate_text(3)
rescue #match the error touple and extract the text in the block
{:error,text} -> IO.puts(text)  #handle the error

so basically I can return my validation text???...hope you can understand the idea...thanks!!!

OK.with else pattern matching

I am just now starting to look at incorporating the OK.with block, and I really like the reduction of noise it gives, but I am unsure how to handle the else block. In my usage, 95% of the time I use a default error handler like so:

with(
  1..
  2..
) do
  {:ok, result}
else
  error -> default_handle_error(error)
end

I try to make all of my errors return in the form of {:error, reason}, but ya never know right. Sometimes I need to pattern match on other errors.

Are you working on something parallel to elixir's else pattern matching in with statements, or is this functionality already implemented?

Warnings on new elixir 1.4.0 rc1

I'm checking out the new elixir rc1 (forum thread, GitHub link), and I am getting compiler warnings:

warning: this clause cannot match because of different types/sizes

Code:

# function declaration
def plan(identity_ib_gibs, src, opts) do

...

# OK Usage
{:ok, identity_ib_gibs}
~>> PB.plan("[src]", opts)         # <<< warning on this line
~>> PB.add_fork("fork1", dest_ib)  # <<< not on this line
~>> PB.yo

The code is executing fine, as I have a bunch of tests, plus this code is already in production. This release has a lot of compiler warnings added, so it looks like it's possibly an assumption that they're doing with the compiler warning and your macro I think?

I'm posting this issue because I wanted to see if you also get these warnings or if it's something specific to me.

Version 2.0

Some breaking changes

  • Drop OK.with
  • rename bind to flat_map
  • raise exception for bad return from flat_map and ~>> Not doing this as dialyzer will catch if return is incorrect

Regular pipe after multiple OK pipes trigger multiple function calls

When an error occures in an "OK pipe" that ends with a regular elixir pipe the errored function is called multiple times. The expected behaviour is that each function in the pipeline will be called only once.

Here's an example of the problem:

defmodule MyModule do

  import OK, only: ["~>>": 2]

  def demo_bug do
    {:ok, :inital_value}
    ~>> failing_function()
    ~>> pass_through()
    |> summarize()
  end

  def failing_function(_whatever) do
    IO.puts "Calling failing_function"
    {:error, "failing_function always fail"}
  end

  def pass_through(value) do
    value
  end

  def summarize({:ok, _result}), do: "All good"
  def summarize({:error, error}), do: "Failed due to #{inspect(error)}"

end

I want the regular pipe by the end to pattern match for both errors and success conditions. If you do not intend to support this use case just say so and ignore the rest of the ticket. Otherwise, when I run the demo_bug function the failing_function is called multiple times.

iex> MyModule.demo_bug()
Calling failing_function
Calling failing_function
"Failed due to \"failing_function always fail\""

The number of times the failing_function is called is 2 in the power of the number of "OK piped" functions that follow. If we change the code to:

  def demo_bug do
    {:ok, :inital_value}
    ~>> failing_function()
    ~>> pass_through()
    ~>> pass_through()  # This line is new
    ~>> pass_through()  # This line is new
    |> summarize()
  end

The failing_function will be called 2 ^ 3 = 8 times!

iex> MyModule.demo_bug()
Calling failing_function
Calling failing_function
Calling failing_function
Calling failing_function
Calling failing_function
Calling failing_function
Calling failing_function
Calling failing_function
"Failed due to \"failing_function always fail\""

Compiler warning for catchall else block

I'm creating this issue per our previous discussion here.

If there is a catchall in the else block in OK.with, then it creates a compiler warning, e.g.

OK.with do
  a <- safe_div(8, 2)
  b <- safe_div(a, 2)
  OK.success a + b
else
  # This creates a compiler warning.
  _reason -> {:error, "catchall fail"}
end

This stems from the unconditional addition of a catchall in OK.with/2:

defmacro with(do: {:__block__, _, normal}, else: exceptional) do
  exceptional_clauses = exceptional ++ (quote do
        reason ->
          {:error, reason}
  end)
...

Bind error for `~>>`

The following examples should raise bind errors for bad values

5 ~>> IO.inspect

This will currently through a case clause error

{:ok, 5} ~>> (fn _ -> :no_result end).()

This currently throws no error but makes no sense when considering result types

Does OK.with handle raised errors?

https://github.com/CrowdHailer/OK#oktry

OK.try do
  user <- fetch_user(1)             # `<-` operator means func returns {:ok, user}
  cart <- fetch_cart(1)             # `<-` again, {:ok, cart}
  order = checkout(cart, user)      # `=` allows pattern matching on non-tagged funcs
  saved_order <- save_order(order)
after
  response(:created)                # Value will be returned unwrapped
rescue
  :user_not_found ->
    response(:not_found)
  :could_not_save ->
    response(:internal_server_error)
end

Does that mean the cases under rescue are raised errors, or errors in {:error, reason} ?

Condense import/require for using both `OK.with` and `~>>`

Currently, to use both OK.with and ~>>, you must do two lines:

import OK, only: ["~>>": 2]
require OK

The require OK is easy to remember, but I personally have to go looking for another file that has the correct ~>> import statement. I was thinking that this may be a barrier to entry for new users.

So two questions:

  1. Is there a way, possibly with a use/__using__ implementation, that could accomplish this?
  2. Do you even think it's a good idea to do so or are separate lines better?

Add a from optional function

{:ok, value} = OK.optional(value)
{:error, :none} = OK.optional(nil)

perhaps provide reason

{:error, reason} = OK.optional(nil, reason: :needs_value)

port = OK.optional(Map.get(config, "port"), reason)

Allowing bare :ok and :error values

Context

I recently realised more and more code I'm writing in my day job is half-hearted error handling for which OK-style ~result monad would be the better solution.

At the same time, the main reason I don't introduce it to the codebase is that many of the functions in the system that are used just for their side-effects have a return value of :ok | {:error, reason}. Or, similarly, we're using standard library functions like Map.fetch(map(), key()) :: {:ok, value()} | :error.

As much as I personally like OK, I can't really make an argument that all functions that don't return correctly formed 2-tuple results should be re-written or wrapped, just so that we can start using the library.

I believe the lack of ability to handle bare :ok and :error values significantly impedes the widespread adoption of OK.

Semantics

I believe wherever an {:ok, term} | {:error, term} is expected, an :ok or :error could be interpreted as {:ok | :error, nil}. I can't think of a case where it would be really problematic.

I imagine however you might not like the semantics change, I remember you being pretty committed to keeping it strict and there's a note about it in the readme. One could potentially make an argument that there might exist users for whom it is important that OK enforces the 2-tuple shape of the function returns.

Depending on the implementation we can keep or not keep the backwards compatibility

Implementation options

Note, I haven't carefully read and thought about the current implementation, some of these might be harder to execute on than I imagine.

1. Just change the semantics, keep the interface as it is.

I think I might be up for that, but I wouldn't be surprised @CrowdHailer and others weren't happy with that.

Would probably mean releasing OK 3.0

2. Create lenient versions of all macros / functions in OK module

I'm not sure what the right names for the new functions would be. Also, transitioning from strict to lenient OK in any project would be an ugly change.

3. Create a lenient version of the OK module

The module would basically rewrite whatever OK does with the new semantics, with as much code re-use as possible. The implementation would probably end up a bit ugly, but using it and transitioning from OK to "lenient OK" would probably be painless.

I think something like this would work:

require OK.LenientOK, as: OK

# the rest of the code unchanged

4. Some clever option with use

I'm not sure how that would look like in its best version. Also, from my limited understanding of Elixir macros, I feel like having use in many modules could have a bad impact on compilation time, as it would be independently executed for each of the modules. It's just my assumption though, probably worth confirming.

Also, personally I like to not use use if I can help it, it introduces unnecessary magic that might be hard to debug.

5. Something else

There's probably other options, I can't think of any good ones right now, but I'm open to suggestions.

Closing notes

I think I like (1), if that's not an option, I think (3) could give a good end result.

As usual, I'd be down for doing the implementation if we decide on going ahead with any of the options and it looks viable.

Clarify semantics of OK.try rescue clause

I think it's worth it to make it clearer in the docs what happens in the "unhappy path" in OK.try - the wrapped errors get unwrapped and "rescued", but non-tagged errors cause a runtime error.

For example the first test case will succeed, but the second one will fail:

  test "OK.try with wrapped error" do
    res = OK.try do
      foo <- {:error, :foo}
    after
      :unwrapped_success
    rescue
      :foo -> :got_foo
    end

    assert res == :got_foo
  end

  test "OK.try with unwrapped error" do
    bar = fn -> :bar end

    res = OK.try do
      bar <- bar.() # will cause runtime error!
    after
      :unwrapped_success
    rescue
      :bar -> :got_bar
    end

    assert res == :got_bar
  end

For more clarity it might be worth adding that safe_div/2 used in all examples returns {:ok, integer()} | {:error, atom()}, so in the division by zero case it will return {:error, :zero_division}, not :zero_division

README improvement to OK.success and OK.failure

I wish the result of OK.success and OK.failure is clarified in the doc as I spent some time trying to understand what the result of these function calls are.

And a simple one line doc explaining the result in the code sample should suffice.

require OK

OK.with do
  user <- fetch_user(1)
  cart <- fetch_cart(1)
  order = checkout(cart, user)
  saved <- save_order(order)
  OK.success saved   # returns {:ok, saved}
else
  :user_not_found ->
    OK.failure :unauthorized   # returns {:error, :unauthorized}
end

Suggestion/Question regarding OK.for after block

With the OK.with do I really liked how I could have the return block be implicit. Since that this is deprecated now, I've gone over to OK.for do, where I now have to make an explicit after block every time. Is there any way around this as it means I end up having to store my value such that I can return it, eg:

def champions(id, region) do
  OK.for do 
    champions = 
      get_champions(id, region)
      ~>> Enum.take(3)
      |>  Enum.map(fn map -> Map.get(map,"championId") end)
      |>  Enum.map(fn id -> name_from_id(id) end)
  after 
    champions
  end
end

Before I could simply end my pipeline with |> OK.success and skip the else or after block, which I thought was neat.

Add when clauses to bind expression

handle when cases in bind

OK.try do
  a when is_integer(a) <- safe_div(6, 2)
  b = a + 1
  c <- safe_div(b, 2)
  {:ok, a + c}
end

However:

  • don't want to start throwing match errors in otherwise safe functions.
  • can't use when in match expressions and want the bind operation to match the behaviour of a match.

warning: this clause cannot match because of different types/sizes

When adding some doctests for the OK lib this morning, I ran mix test and got a bunch of warnings:

warning: this clause cannot match because of different types/sizes

I checked, and my code base also had these warnings. It happens anytime that you return a literal or an OK.success expression as the last line.

I was going to just post this issue, but then I started looking into it and I think I've actually addressed it! I personally dislike PRs without a corresponding issue, so I'm creating this issue first. (If you prefer just the PR, then in the future I'll just do that.)

The issue stems from the following clause of nest/1 (I added :

  defp nest([normal | []]) do
    quote do
      case unquote(normal) do
        {:ok, value} ->
          {:ok, value}
        {:error, reason} ->
          {:error, reason}
      end
    end
  end

The warning stems from the case statement not having a match on the {:error, reason} when the line is already an {:ok, value} statement (or the corresponding OK.success equivalent). So in the PR I'm about to submit, I added a couple clauses that specifically pattern match out these two cases (ok literal and OK.success).

Link to why try catch is a bad idea

catching exceptions has issues will tail recursion. discussed somewhere can't find link. Need to add it to an FAQ on why by default OK does not work with exceptions.

Result Type

I feel like the library is missing a result(t, e) type for use in typespecs:

@type result(t, e) :: {:ok, t} | {:error, e}

It could also be a good idea to use this type as the input parameter for many of the functions instead of term()?

Proposal: add `ok?` and `error?` methods

Could be cool to have some other helpers to check if result tuple is in error or ok.

The use case in my mind is filter a collection of result tuples looking only for ok results.

Thank you in advance

Alternative syntax or some sugar

Are there any constraints that would make this impossible

Original (This is a modified version from the README.md

OK.with do
  user <- fetch_user(1)
  cart <- fetch_cart(user)
  order = checkout(cart)
  save_order(order)
else
  :user_not_found ->           # Match on untagged reason
    {:error, :unauthorized}    # Return a literal error tuple
end

Compared to some alternative syntax

OK.with do
    1 
      |> fetch_user
      |> fetch_cart
      |> checkout
      |> save_order
else
  :user_not_found ->           # Match on untagged reason
    {:error, :unauthorized}    # Return a literal error tuple
end

Maybe not using |> but something that allows the original piping from Elixir but with happy path until the end.

To be honest, this is what Elixir should have done from day 1 :-)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.