adambard / failjure Goto Github PK
View Code? Open in Web Editor NEWMonadic error utilities for general use in Clojure(script) projects
License: Eclipse Public License 1.0
Monadic error utilities for general use in Clojure(script) projects
License: Eclipse Public License 1.0
Special case of #15: map destructuring works fine now, but tuple destructuring chokes.
Correctly returns either Failure or the expected values:
(f/attempt-all [{:keys [a b]}
(if (< (rand) 0.5) (f/fail "test") {:a 1 :b 2})] [a b])
Throws UnsupportedOperationException when it doesn't return the expected values:
(f/attempt-all [[a b]
(if (< (rand) 0.5) (f/fail "test") [1 2])] [a b])
I don't see any way to handle failure without binding it to a symbol. attempt-all
is great but if we have only one binding then there's a lot of boilerplate and nesting. My proposal is to add attempt
macro which will let us handle failure without having to bind it manually.
Example with attempt-all
:
(f/attempt-all [result (f/fail "Failed")]
result
(f/when-failed [e] (handle-error e)))
Boilerplate. We don't do anything with the result if there's no error.
Example with when-let-failed?
:
(f/when-let-failed? [result (f/fail "Failed")]
(handle-error result))
Less boilerplate, but we have to customize auto-indent and our code is less linear + we still have to name result
"There are only two hard things in Computer Science: cache invalidation and naming things."
Phil Karlton
Example with attempt
:
(f/attempt (f/fail "Failed") handle-error)
0 boilerplate, elegant one-liner.
Example of real world use-case with when-let-failed?
:
(defn create-todo [input]
(f/when-let-failed? [result (f/ok->input
(validate)
(prepare)
(insert-into-db!)
(result-json))]
(create-todo-error result)))
Example of real world use-case with attempt
:
(defn create-todo [input]
(f/attempt
(f/ok-> input
(validate)
(prepare)
(insert-into-db!)
(result-json))
create-todo-error)))
Ideal name imo would be when-failed
but there's already macro with this name so attempt
seams reasonable. It's almost like attempt-all
but you attempt to get only one value and if there's only one value there's no point to bind it to anything
The current clj-kondo config shipped with the project maps failjure.core/try*
to clojure.core/try
. This causes clj-kondo to complain about the the expression Missing catch or finally in try
.
current workaround is to set .clj-kondo/config.edn
to be:
{:lint-as {failjure.core/try* clj-kondo.lint-as/def-catch-all}}
We have the following issue/exception when we're importing failjure during dev-time (repl-based workflow):
java.lang.NoClassDefFoundError: failjure/core/HasFailed
at bulk_gateway.rest.accounts$get_acccount_balance.invokeStatic(accounts.clj:68)
at bulk_gateway.rest.accounts$get_acccount_balance.invoke(accounts.clj:52)
...
(require ...)
calls after finding a list of namespaces in our integrant configuration file. These namespaces then have (ns bulk-gateway.rest.accounts (require [failjure.core]...
dependencies on failjure.(load-file "src/bulk_gateway/rest/accounts.clj")
after which the NoClassDefFoundError
exceptions go away.lein uberjar
).We've had similar issues with our own shared libraries that have (defprotocol ...
in them and solved it by adding an :out [...]
in the project.clj
of the client lib project.
I cloned your repo locally and added an :aot [failjure.core]
to failjure's project.clj
and our issue goes away after lein install
.
As far as I know this should be no-impact change for clients.
(defproject failjure "2.1.1"
:description "Simple helpers for treating failures as values"
:url "https://github.com/adambard/failjure"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies []
:repl-options {:init-ns failjure.core}
:plugins [[lein-cljsbuild "1.1.8"]
[lein-doo "0.1.10"]]
:aot [failjure.core]
:profiles
{:provided {:dependencies [[org.clojure/clojure "1.10.1"]
[org.clojure/clojurescript "1.10.764"]]}}
:cljsbuild
{:builds [{:id "test"
:source-paths ["src" "test"]
:compiler {:output-to "target/testable.js"
:main failjure.runner
:target :nodejs
:optimizations :none}}]})
Introduce the failjure version of as->
which short circuits if any of the forms returns a failjure
First of all, this is awesome! Thanks @adambard โจ ๐
As I read your introductory blog post, I had an initial reaction when I saw if-failed
:
(f/attempt-all [,,,]
,,,
(f/if-failed [e]
(log-error (f/message e))
(handle-error e)))
I saw two forms after if
and my first intuition was that they would be "then" and "else" cases. This obviously doesn't make sense here, so this didn't hang me up for long.
...But, maybe we could avoid even the chance of momentary confusion by renaming to when-failed
.
I think it would be much better, if attempt-all
would accept a plain function as a failure-handler instead of a when-failed
macro - remember the first rule of the macro-club ;-)
And it just takes a slight modification:
(defmacro attempt-all
"Used like `let`, but short-circuits in case of
a failed binding. If a `failure-handler` fn is provided, it is called to handle the failure.
Unlike `let`, only accepts a single form to execute after the bindings.
(attempt-all [x \"Ok\"
y (fail \"Fail\")]
x
(fn handle-failure [e]
(message e))) ; => \"Fail\"
"
([bindings return]
`(domonad error-m ~bindings ~return))
([bindings return failure-handler]
`(let [result# (attempt-all ~bindings ~return)]
(if (failed? result#)
(~failure-handler result#)
result#))))
What do you think?
Regards, Jan
De-structuring inside attempt-all
causes a returned java Exception or failjure Failure that would normally be short-circuited to not be short-circuited.
(f/attempt-all [m (f :a)
{:keys [a b c]} m]
3)
The above code gets around the problem. The problem would be evident if you avoided having m
, so directly de-structured. Obviously f
is returning something that f/attempt-all
ought to be catching (a java Exception or failjure Failure).
I was wondering if makes sense for change the ok?
fn to also validate a list of results?
(defn ok?
[v]
(if (coll? v)
(every? ok? v)
(not (failed? v))))
I think would be nice to add a feature like this or on the other hand if you want to have a different function with it's own semantic we can just add a different fn.
your thoughts on this one @adambard
@adambard Have you tried porting Failjure to clojurescript? Failjure uses algo.monads, which just seems to use one function name-with-attributes
, from clojure.tools.macro. Ie, it doesn't support clojurescript. But Cats does.
Anyways, I thought it would be really nice to reuse tha same Either / Error monadic semantics, on both layers of our application.
Thoughts?
Hello!
First of all, thanks for failjure
I would be glad if you write an example how to use failjure for cases, where a user needs to close opened resources in any case. E.g. Iโm doing the next:
(f/attempt-all [reader (pdf-reader source)
document (pdf-document reader)
fields (fields document)]
(do
(.close document)
(.close reader)
fields)
(f/when-fail [e]
; Here I do not have an access to reader and document
; to close them
))
I wonder if there any mechanism to close the document
and the reader
in any case, like with with-open
Think it would be handy to have a macro like f/attempt
but wraps val-or-failed
in an implicit f/try*
.
Maybe something like f/try
? or change f/attempt
?:
(f/try
#(f/fail "I failed!: %s" (f/message %))
(throw (ex-info "I throw" {})))
I think try-all-failed
and similar might also be useful.
I am trying to parse some input from user (a time Duration) and trying to be forgiving.
In my case I need to try out all options and stop when I get a non-failure.
So my algorithm is :
I think code would look like this:
(beginner with Failjure, code might not be correct)
(try-all-failed [duration (try-parse s)
duration (try-parse (str "PT" s))
duration (try-parse (str "P" s))]
duration
(f/fail "Unable to parse duration"))
Some context
https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html#parse-java.lang.CharSequence-
Examples:
"PT20.345S" -- parses as "20.345 seconds"
"PT15M" -- parses as "15 minutes" (where a minute is 60 seconds)
"PT10H" -- parses as "10 hours" (where an hour is 3600 seconds)
"P2D" -- parses as "2 days" (where a day is 24 hours or 86400 seconds)
"P2DT3H4M" -- parses as "2 days, 3 hours and 4 minutes"
"P-6H3M" -- parses as "-6 hours and +3 minutes"
"-P6H3M" -- parses as "-6 hours and -3 minutes"
"-P-6H+3M" -- parses as "+6 hours and -3 minutes"
examples use defn-spec from orchestra
This works:
(defn-spec test-failure-works (s/or :ok string? :fail ::fs/failure)
[]
(f/fail "something wrong"))
This does not work --looks like the invocation of 'empty' on Failure record from the clojure.spec code is the problem:
(defn-spec test-failure-doesnt-work (s/or :ok (s/coll-of string?) :fail ::fs/failure)
[]
(f/fail "something wrong"))
Unhandled java.lang.UnsupportedOperationException
Can't create empty: failjure.core.Failure
core.clj: 36 failjure.core.Failure/empty
core.clj: 5187 clojure.core/empty
core.clj: 5181 clojure.core/empty
alpha.clj: 1229 clojure.spec.alpha/every-impl/cfns/fn
alpha.clj: 1243 clojure.spec.alpha/every-impl/reify
alpha.clj: 1017 clojure.spec.alpha/or-spec-impl/fn
alpha.clj: 1052 clojure.spec.alpha/or-spec-impl/reify
alpha.clj: 150 clojure.spec.alpha/conform
alpha.clj: 146 clojure.spec.alpha/conform
test.clj: 97 orchestra.spec.test/spec-checking-fn/conform!
test.clj: 125 orchestra.spec.test/spec-checking-fn/fn
RestFn.java: 397 clojure.lang.RestFn/invoke
Hi, On 1.9-alpha16, just including the failjure declaration in a namespaces causes the following error
Exception in thread "main" java.lang.ExceptionInInitializerError
at clojure.main.<clinit>(main.java:20)
Caused by: clojure.lang.ExceptionInfo: Call to clojure.core/defn did not conform to spec:
In: [0] val: clojure.algo.monads/m+m-join+m fails spec: :clojure.core.specs/defn-args at: [:args :name] predicate: simple-symbol?
:clojure.spec/args (clojure.algo.monads/m+m-join+m [m-bind m-result m-zero m-plus m] (clojure.tools.macro/with-symbol-macros (m-bind m identity)))
{:clojure.spec/problems [{:path [:args :name], :pred simple-symbol?, :val clojure.algo.monads/m+m-join+m, :via [:clojure.core.specs/defn-args :clojure.core.specs/defn-args], :in [0]}], :clojure.spec/args (clojure.algo.monads/m+m-join+m [m-bind m-result m-zero m-plus m] (clojure.tools.macro/with-symbol-macros (m-bind m identity)))}, compiling:(clojure/algo/monads.clj:248:1)
at clojure.lang.Compiler.load(Compiler.java:7442)
at clojure.lang.RT.loadResourceScript(RT.java:374)
at clojure.lang.RT.loadResourceScript(RT.java:365)
at clojure.lang.RT.load(RT.java:455)
at clojure.lang.RT.load(RT.java:421)
at clojure.core$load$fn__7831.invoke(core.clj:6008)
at clojure.core$load.invokeStatic(core.clj:6007)
at clojure.core$load.doInvoke(core.clj:5991)
at clojure.lang.RestFn.invoke(RestFn.java:408)
at clojure.core$load_one.invokeStatic(core.clj:5812)
at clojure.core$load_one.invoke(core.clj:5807)
at clojure.core$load_lib$fn__7776.invoke(core.clj:5852)
at clojure.core$load_lib.invokeStatic(core.clj:5851)
at clojure.core$load_lib.doInvoke(core.clj:5832)
at clojure.lang.RestFn.applyTo(RestFn.java:142)
at clojure.core$apply.invokeStatic(core.clj:659)
at clojure.core$load_libs.invokeStatic(core.clj:5889)
at clojure.core$load_libs.doInvoke(core.clj:5873)
at clojure.lang.RestFn.applyTo(RestFn.java:137)
at clojure.core$apply.invokeStatic(core.clj:659)
at clojure.core$require.invokeStatic(core.clj:5911)
at clojure.core$require.doInvoke(core.clj:5911)
at clojure.lang.RestFn.invoke(RestFn.java:408)
at failjure.core$eval16561$loading__7717__auto____16562.invoke(core.clj:1)
at failjure.core$eval16561.invokeStatic(core.clj:1)
at failjure.core$eval16561.invoke(core.clj:1)
at clojure.lang.Compiler.eval(Compiler.java:6978)
at clojure.lang.Compiler.eval(Compiler.java:6967)
at clojure.lang.Compiler.load(Compiler.java:7430)
at clojure.lang.RT.loadResourceScript(RT.java:374)
at clojure.lang.RT.loadResourceScript(RT.java:365)
at clojure.lang.RT.load(RT.java:455)
at clojure.lang.RT.load(RT.java:421)
at clojure.core$load$fn__7831.invoke(core.clj:6008)
at clojure.core$load.invokeStatic(core.clj:6007)
at clojure.core$load.doInvoke(core.clj:5991)
at clojure.lang.RestFn.invoke(RestFn.java:408)
at clojure.core$load_one.invokeStatic(core.clj:5812)
at clojure.core$load_one.invoke(core.clj:5807)
at clojure.core$load_lib$fn__7776.invoke(core.clj:5852)
at clojure.core$load_lib.invokeStatic(core.clj:5851)
at clojure.core$load_lib.doInvoke(core.clj:5832)
at clojure.lang.RestFn.applyTo(RestFn.java:142)
at clojure.core$apply.invokeStatic(core.clj:659)
at clojure.core$load_libs.invokeStatic(core.clj:5889)
at clojure.core$load_libs.doInvoke(core.clj:5873)
at clojure.lang.RestFn.applyTo(RestFn.java:137)
at clojure.core$apply.invokeStatic(core.clj:659)
at clojure.core$require.invokeStatic(core.clj:5911)
at clojure.core$require.doInvoke(core.clj:5911)
at clojure.lang.RestFn.invoke(RestFn.java:436)
at gs_authorization.routes.services.actions$eval16555$loading__7717__auto____16556.invoke(actions.clj:1)
Using the f/ok->> macro allows short circuiting only on explicit failures, but since the regular Clojure idiom is to use nil punning, it would be useful to have a threading macro that provides the benefit of some->> as well. The modified macro below allows me to unify nils, exceptions as well explicit fails into the failjure interface. If the code below looks ok, could you please include it as part of failjure?
(defmacro check_attempt->>
([start] start)
([start form] `(domonad f/error-m [x# (->> ~start ~form)] x#))
([start form & forms]
`(let [new-start# (check_attempt->> ~start ~form)]
(cond
(f/failed? new-start#) new-start#
(nil? new-start#) (f/fail (str "got nil at " ~form))
:else (check_attempt->> new-start# ~@forms)))))
(defmacro check->>
([start & forms]
`(if (f/failed? ~start)
~start
(f/try*
(check_attempt->> ~start ~@forms)))))
There are a lot of macros in this very useful library & it'd be nice if they were indented properly w/ IDEs like CIDER.
See indent specs for CIDER.
Hi,
I'm using this library in a project and I kind of like the flow.
However I'm missing the stack traces when an a failure is reached.
Would love to have a way to capture the stack-trace on fail so to have an easier experience when debugging.
Is this feasible?
Thanks,
Euge
My code often fails in varying and spectacular ways :)
What would you think about allowing arbitrary maps to be associated with failures as well as strings?
My database might fail and my Validateur
validations might fail, and I need to be able to render those in different ways.
If I could add a map to a failure then I could put the error map from Validateur
into my failure and render that on my front end.
What do you think? Looking at your library and learning about monads has really changed the way I think about error handling, thanks!
E.g.
user> (f/ok-> (println "foo"))
foo
foo
nil
This is rather surprising. I guess the macro should put the form into a let
?
For the occasions when you only care that the form passes and do not need a value from it, I find myself using when-let-ok?
:
(f/when-let-ok? [_ (some-fn-that-returns-a-failure-or-a-value...)]
::woohoo)
I propose to add the macro when-ok
:
(defmacro when-ok [form & body]
(when-let-ok? [_# ~form]
~@body))
So we can write:
(f/when-ok (some-fn-that-returns-a-failure-or-a-value...)
::woohoo)
Happy to do a PR for this with tests if you're interested in adding it.
ps. Great library by the way, thank you for your efforts!
Hello @adambard !
You removed monads dependency from failjure lib.
I am using previous failjure version in my project and i am using it as a monad in several places.
For example, m-map over collection where projection function can return error.
Can you enlighten me why did you removed monad dependency?
Is it Occam Razor application?
Thanks!
attempt-all
can handle failures
but not exceptions
unless you wrap your function with try*
. Why not have an attempt-all*
which is similar to attempt-all
except that it short-circuits whenever an exception is thrown and hands over the control to when-failed
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.