clj-commons / manifold

A compatibility layer for event-driven abstractions
1.02k stars 106 forks source link

if-let and when-let on let-flow #218

Closed cosineblast closed 1 year ago

cosineblast commented 1 year ago

It would be nice to have constructs like clojure's if-let and when-let but made for let-flow instead.

It's simple enough to just implement this once needed, but nice to have by default.

KingMob commented 1 year ago

What's the use case you had in mind?

let-flow's value is in handling dependencies between deferred's automatically, but both if-let and when-let take only a single binding, so there's no dependencies to resolve.

With only a single binding, let-flow would be effectively the same as writing:

(when-let [foo @some-deferred]
  (do-something foo)

A multi-binding let-flow is possible, but it's not clear if the termination criteria should be all non-nil or any non-nil.

On top of that, unlike single-threaded code, independent let-flow bindings can run in separate threads, making termination tricky/impossible. It would have to be completely serialized

cosineblast commented 1 year ago

Well, perhaps this is not the intended usage of the macro, but I find let-flow very useful to compose deferred values into complex code, instead of recurring to chain and lambdas. (Similar to using do notation in haskell instead of >>=).

  (let-flow [value some-deferred]
    (foo value)
    (bar value))

  ;; although in this case we could have just gone with

  (chain some-deferred
         #((foo %) (bar %)))

Because of this, I often end up with situations in which let-flow and if are being used exactly like let and if in the if-let macro:

(let-flow [value some-deferred-tuple]
    (if value
      (let [[x y z] value]  z)
      (foo)
      ))

So I wonder if it is worth introducing an if-let-flow construct for those cases.

  (if-let-flow [[x y z] some-deferred-tuple]
    z
    (foo))

Something like this:

(defmacro if-let-flow [bindings then else]
  `(let-flow [value# ~(second bindings)]
     (if value#
       (let [~(first bindings) value#]
         ~then
         )
       ~else
       )
     ))

(defmacro when-let-flow [bindings & body]
  `(let-flow [value# ~(second bindings)]
     (when value#
       (let [~(first bindings) value#]
         ~@body
         )
       )
     ))
KingMob commented 1 year ago

So, you're not interested in a multi-binding version of if-let and when-let?

In that case, you really don't need new macros. It's probably simpler to use core if-let/when-let and deref the sole deferred. E.g., instead of if-let-flow, do:

(if-let [foo @some-deferred]
  (do-something-with foo)
  (log/error "foo is false or nil"))

It'll effectively be the same thing.

I guess you don't get async in that case, so if you need to ensure it runs in the background and/or returns a deferred, it would be:

(future
  (if-let [foo @some-deferred]
    (do-something-with foo)
    (log/error "foo is false or nil")))

Still, I'm not quite sure it's worth adding to the manifold API. Like I said, the let-flow value is in figuring out the dependencies.