drym-org / qi

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

Better support for Racket expressions that generate multiple values #42

Open countvajhula opened 2 years ago

countvajhula commented 2 years ago

Working with multiple values in Racket is cumbersome, requiring the use of e.g. call-with-values. In Qi it's typically much more natural. So it would be great if in all such cases we could just use Qi. But in the case of a single expression that generates multiple values, Qi doesn't currently have an especially compelling way of handling it.

E.g. the time-apply utility returns many values. We'd like to be able to use its results in a convenient way.

With Racket:

(call-with-values (λ _ (time-apply + (list 1 2 3))) list)

With Qi, currently:

(~> () (esc (λ _ (time-apply + (list 1 2 3)))) list)

Preferable, something like:

(~> () (gen* (time-apply + (list 1 2 3))) list)

Could it be even nicer?

Is gen* a good name for this?

countvajhula commented 1 year ago

Actually, I'm not sure what I was thinking because the example above isn't hard in Qi, it would just be:

(~> () (time-apply + (list 1 2 3)) list)

One possible improvement could be to have a Racket-level macro that eliminates one step:

(~>* (time-apply + (list 1 2 3)) list)

This doesn't need parens around the first form since we would assume that it is a (Racket) expression that is going to generate any number of values. This form might also be preferable to ~> in all cases where we would otherwise do (~> () ...).

benknoble commented 1 year ago

I've gone back and forth between these and settled on the former because it's less to type and, to me, clearer that there are no more values as input to the initial expression. It would be nice to not have to write the () though.

(~> () (time-apply + (list 1 2 3)) list)
;; or
(~> (time-apply + (list 1 2 3)) apply list)
benknoble commented 1 year ago

And, I think ~>* could be a library macro (meaning it doesn't have to be provided by Qi for folks to start trying it out). I also admit that this case comes up fairly rarely for me, but maybe that's because so few things use multiple-valued returns :)


P.S. The apply trick in my last post doesn't work with send, either, so that's another argument in favor of using (~> () …)

countvajhula commented 1 year ago

That apply trick 😆 I had to do a double take.

re: ~>* as a library function, are there other forms that could be included in such a library or would it be a dedicated one like (require qi/thread*)?

I guess it would also include on* and switch*? Interesting.

On the other hand, if we consider multiple values as a first class citizen in Qi, then it feels natural that these forms should have an equal position in the core library to forms like ~> that support single-valued Racket outputs. Would it make sense to move even ~>, on, switch (everything except flow) into libraries? Early in the compiler project we talked about the possibility of having qi/base and qi like racket/base and racket, but I think we were mainly thinking about the core language forms. Maybe this would affect interface macros too.

benknoble commented 1 year ago

RE: libraries, I'm not really advocating one approach over another. Just that it can be done if that was desirable (say, to put it an experimental lib before committing to it publicly).

I could also see trying to extend ~> and various other interface forms (https://docs.racket-lang.org/qi/Language_Interface.html#%28part._.Using_.Qi_from_the_.Host_.Language%29) to accept expressions that generate multiple values. Example:

(~> (1 (time-apply + (list 1 2 3)) 3))
;; equivalent to (values 1 <splice all the result values of time-apply> 3)