4 ms·
Elixir has a nice take on this with the `with` keyword/macro with {:ok, file_handle} <- File.open(filename), {:ok, contents} <- IO.read(file_hand
by el_oni 3y ago
Elixir has a nice take on this with the `with` keyword/macro
with {:ok, file_handle} <- File.open(filename),
{:ok, contents} <- IO.read(file_handle),
{:ok, parsed} <- MyModule.parse(contents)
do
{:ok, parsed}
end
what this does is run the functions in order top to bottom, and if the return value from each function doesn't match with what is on the left, it returns early with the thing that didn't match, otherwise it continues.
This means you don't need to write each function to take a tuple of {:ok, value} and another clause to take {:error, reason}, you can just write your functions to take the value they care about and let pattern matching in the with block to take care of error propagation.
so if File.open returns {:error, reason} then IO.read never executes and the result of the with is {:error, reason}
It essentially means you can program the happy path and let the caller match on the sad paths (if they want to)
- bjourne 3y agoWhat is the advantage of that over exceptions? And what if IO.read fails? Who closes the file handle?
- ahoka 3y agoUnless you are writing oldschool Java, then exceptions are not typechecked.
- el_oni 3y agoThe file handle is the PID of the process that opened the file, it monitors the process that asked for the file to be opened. and if that process goes down the file will be closed. You can also add an else in the with so it looks like with {:ok, fh} <- File.open(filename), {:ok, contents} <- IO.read(fh) do contents else {:error, reason} -> File.close(fh) {:error, reason} # this will return after the file has been closed end or maybe you would prefer to open the file, and pass that into the with and either way close the file. I just used IO as an example because they return nice {:ok, x} or {:error, reason} tuples, but this works with any pattern.
- dahfizz 3y ago> The benefits over an exception is as the caller i can pattern match on the result of this. so i could have this is in a function in a case Your example looks just like exceptions to me, just with different keywords try: return with_example(fname) except RecoverableError: recover(fname) except Exception as e: raise e
- el_oni 3y agoIn the examples yes, because it's just a simple binary ok or error case but if you returned a list you can pattern match on an empty list, a single element list or a list that's longer. which you wouldn't do with exceptions in another language. that's the nice part about `with` that you can stop once you stop matching the pattern and return whatever you currently have, which in the list example may be a perfectly valid thing to return. with [value] <- Module.some_list_function(arg), # can return an empty list too [head | tail] = list <- Module.another_func(value), # can return a single element list longer_list <- Module.takes_multi_element_list(list) do longer_list end There are loads of other examples for uses of this, or you can just write in an additional clause for your function that handles the other case and pipe it down the line. It's about what makes sense for your domain. I may not be explaining this well so apologies for any confusion
- bedobi 3y agoException based error handling is so bad and unsafe that adopting functional error handling with Either, Try etc as implemented by functional addon libraries for many languages, while not yet common, in time it will become the new default even in OO languages. (just like it's been the default in functional languages for decades) Functional error handling types are much simpler, safer and more powerful. Simpler because they don't rely on dedicated syntax- they're just regular objects no different to any other object. Safer because unlike exceptions, they force callers to handle all potential outcomes, but no more. (no risk of ignoring errors and no risk of catching a higher level of error than desired, ubiquitous bugs in exception based error handling) Powerful because they support map, flatmap, applicative etc, making it easy to eg chain multiple computations together in desired ways, which is unwieldy and bug prone when using exceptions. > What is wrong about dedicated syntax It adds complexity to the language! It could be that, when learning Java, Kotlin and any other language, we learn that methods return what they say they do... and that's that. No weird dedicated syntax and magic, special treatment for returning anything other than the happy path, and the HUGE complexity that comes with it, eg the dedicated syntax itself and how it behaves, differences between checked and unchecked exceptions, hierarchies of exceptions etc etc. > Exceptions are easier But that's the point, they're not. Exceptions based error handling is unnecessary, hugely complex, doesn't compose at all, obfuscates or straight up hides what can go wrong with any given call, so leads to countless trivially preventable bugs... I could go on. And after decades of use, there's still no consensus about what exceptions should be or how they should be used. Exceptions are a failed experiment and I have no doubt that in ten years, Java, Kotlin and many other languages will acknowledge as much and move away from it the same way Joda Time outcompeted and replaced the horrible Java date and time library.
- jaen 3y agoOption and Result types, as implemented today in mainstream languages (ie. mostly anemically), are not the answer to exceptions being a mess. Exceptions have a lot of additional functionality in larger ecosystems such as: - Backtraces ie. showing the exact path of the error from its source to whereever it was handled, in a zero-cost way. This is by far the most important aspect of exceptions, as it enables automatically analysing and aggregating them in large systems, to eg. attribute blame from changes in error metrics to individual commits. - Nested exceptions ie. converting from one error system to another without losing information. Extensible with arbitrary metadata. - An open and extensible error type hierarchy. Again, necessary in large scale systems to differentiate between eg. the cause (caller fault, callee fault aka HTTP 400/500 divide), retryable or permanently fatal, loggable etc. exceptions while also maintaining API/ABI backward/forward compatibility. (for some of these, eg. Rust has crates for a Result-y equivalent, but a community consensus does not exist, yet...) General-purpose exceptions simply are complicated, and any system trying to "re-invent" them will eventually run into the same problems. Over-simplifying error handling just results in less maintainable, debuggable and reliable systems.
- el_oni 3y agoThe file handle is the PID of the process that opened the file, it monitors the process that asked for the file to be opened. and if that process goes down the file will be closed. You can also add an else in the with so it looks like with {:ok, fh} <- File.open(filename), {:ok, contents} <- IO.read(fh) do contents else {:error, reason} -> File.close(fh) {:error, reason} # this will return after the file has been closed end or maybe you would prefer to open the file, and pass that into the with and either way close the file. I just used IO as an example because they return nice {:ok, x} or {:error, reason} tuples, but this works with any pattern. The benefits over an exception is as the caller i can pattern match on the result of this. so i could have this is in a function in a case result = case with_example(filename) do {:ok, result} -> result {:error, :some_reason} -> # this is recoverable, do something else #log the issue recover!(filename) # bangs in functions indicate they can fail and raise an error {:error, :another_reason} -> # this is unrecoverable # log the issue raise "unrecoverable error" _ -> # any other case we don't know about raise "unexpected issue" or i might not care and want it to crash if it doesn't match {:ok, parsed} = with_example(filename) # will raise a match error if {:error, reason is returned
- deleted 3y ago[deleted]
- sodapopcan 3y agoExceptions in Elixir are reserved for situations that are truly exceptional. If IO might fail as a business concern, then we use File.read and pattern match on the return type where we can explicitly handle the error case. Otherwise, if we know a file will always be there and IO is failing for reasons out of our control, that is truly exceptional so we can use File.read! which will throw an exception on failure. In Elixir we generally don't handle this, we just crash and a supervisor brings the process back up.
- kazinator 3y ago> returns early with the thing that didn't match What if the things are all of a different type? This is dynamically typed? You have to look at the type of the thing to find out where it went wrong, and guess which failing expression it came from? It's like halfway to reinventing exception handling. with pat1 expr1, pat2, expr2 .... do happy case // all matched catch // various unhappy patterns matched against mismatching expr pat1 do ... end pat2 do ... end ... end Just call it "else" or something instead of "catch" and then it doesn't look like exception handling.
- el_oni 3y agoWell, this isn't the only way to do things. There are exceptions in elixir and you can catch them if you want. But most functions that can fail have two versions File.read() returns {:OK, contents} or {:error, reason} So you can pattern match on the result. File.read!() returns contents or raises an exception. The first one allows you to use errors as values and handle the problem at the source. The latter assumes its going to be successful and either makes you catch the exception or, more likely for elixir, let the process crash. The with statement is a good fit for certain domains where otherwise you might have a bunch of nested cases. If you are worried about it returning a different type you can wrap it in the else block with {:ok, bar} <- func(foo) do bar else value -> {:failed, value} end So you can make your failed cases more homogeneous to pattern match on. You can even pattern match on the different failed cases if you want to ensure more homogeneity. If using with doesn't make sense for the domain there are other constructs in the language that will
- ljm 3y agoThis is influenced by Haskell (where) and lisp (let) AFAIK. You’re setting the preconditions for the function, and with elixir you get an extra ‘else’ block to help.