drym-org / qi

An embeddable flow-oriented language.
58 stars 12 forks source link

Make higher-order flows implicitly thread arguments #113

Closed rvs314 closed 9 months ago

rvs314 commented 10 months ago

I find myself often writing flows that look like this:

(define-flow part-2
  (~> parse
      (>< (~> △ append))
      ▽ (split-into 3) △
      (>< (~> △ set-intersect car score))
      (<< +)))

That is, code which uses a higher-order flow like ><, which then immediately takes a ~> form. A small regex search shows that the majority of the code which uses >< in a personal project immediately takes a ~> form. Given that >< is a unary operator, it would be convenient if it and a few other operators could take an arbitrary number of arguments, then passes them to ~>. It wouldn't be a breaking change, as (>< X) would still parse as it does normally.

I think the following could be made variadic:

If others think this change would be useful, I'd be happy to send a PR which implements it.

benknoble commented 10 months ago

I've thought about this some, too. It's sometimes annoying to type, but I haven't decided if I want to give up the clarity. Sometimes it just means I should extract some sub-flows (not always).

I also haven't thought about what else the variadic notation could be used for, and it might not be worth committing to a shorthand for threading... Given the multi-values nature of Qi computations, I would really love to know that we've ruled out other possible uses of this notation.

countvajhula commented 9 months ago

I don't know whether it'd be preferable to have an implicit ~> (I think I'd personally incline towards an explicit ~> and/or decomposing here, as @benknoble mentioned), but I can see the benefits you've pointed out. For now you could, if you like, implement the functionality you are looking for here as a set of Qi macros.

To Ben's point about multiple values, @NoahStoryM has written some prototypes in the past that leverage function arity to distribute input values. That may be related, e.g. see #64 and #69

One thing in your code, I believe (<< +) is equivalent to just + since + is variadic.

If you want to go the macros route, you could do something like this:

(define-qi-syntax-rule (><> f ...)
  (>< (~> f ...)))

(~> (1 2 3) (><> sqr add1) +)

Once the compiler branch is merged, you could even override the built-in forms if you wanted to, something like:

(require (except-in qi
                    ><)
         (only-in qi
                  [>< q:><]))

(define-qi-syntax-rule (>< f ...)
  (q:>< (~> f ...)))

(~> (1 2 3) (>< sqr add1) +)

And the set of these forms that you mentioned could be provided as a library, say implicit-threading, which you could use as (require qi/implicit-threading).

That way, you could use this behavior and even provide it to others without needing it to be part of Qi itself, and it doesn't rule out including it in Qi itself in the future. If you decide to go down this road, please see Creating Libraries for Qi.

rvs314 commented 9 months ago

It seems like this is a design question that goes beyond me, so I'll close this for now.