qi icon indicating copy to clipboard operation
qi copied to clipboard

[Feature Request] Converting `rest-args` from `list` to `values` first.

Open NoahStoryM opened this issue 2 years ago • 5 comments

See previous discussion here: https://github.com/countvajhula/qi/pull/59

@NoahStoryM : I noticed that when switch-lambda and flow-lambda use rest-args, rest-args is treated as input to flow directly. Can we consider converting rest-args from list to values first? I think it is more in line with the point-free style.

@benknoble : I would like to point out, though, that when looking at (flow-lambda args …), I can tell that the flow expects 0 or more input values, and by analogy with regular Racket I expect the values to be in a list. It is not difficult to use sep as the first part of the flow to split the rest-args. Breaking the analogy to regular Racket would be odd, and might also cause confusion in the way inputs to the flow are handled.

NoahStoryM avatar Jul 27 '22 00:07 NoahStoryM

I can tell that the flow expects 0 or more input values

It seems that this change doesn't affect it?

and by analogy with regular Racket I expect the values to be in a list.

I think the reason why regular Racket stores the rest args into a list is that it can't handle values directly. This problem does not exist in Qi.

Breaking the analogy to regular Racket would be odd, and might also cause confusion in the way inputs to the flow are handled.

Well, in my understanding, the input accepted by flow should be values. It's odd for me to convert part of the input to list.

NoahStoryM avatar Jul 27 '22 01:07 NoahStoryM

I can tell that the flow expects 0 or more input values

It seems that this change doesn't affect it?

Yes, that comment was a hold-over from a draft where my example involved more arguments (like (flow-lambda (a b . rest) …)), which isn't currently supported in Qi anyway. But your PR #59 adds that. In that case, I can immediately say the flow expects 2 input values, plus an arbitrary many more; by analogy with Racket, I still expect the rest to be in a list.

and by analogy with regular Racket I expect the values to be in a list.

I think the reason why regular Racket stores the rest args into a list is that it can't handle values directly. This problem does not exist in Qi.

This is true: I question how much Qi wants to break with Racket.

Breaking the analogy to regular Racket would be odd, and might also cause confusion in the way inputs to the flow are handled.

Well, in my understanding, the input accepted by flow should be values. It's odd for me to convert part of the input to list.

Again, my argument is that this is what Racket does naturally, and it is odd for me to break that analogy. OTOH, I can see some value in separating the list and staying in values land.

Newly occurring to me, there is an efficiency question: Racket is going to collect the input into a list for us (assuming a translation like (flow-lambda (a b . rest) body) => (lambda (a b . rest) …)).

  • The current Qi behavior (to do nothing) avoids doing any work. If I want rest as values instead of list, I am explicit about the work of seping it.
  • The proposed Qi behavior (to automatically sep the list) makes Qi do some invisible work. Then, if I want the opposite case (rest as a list instead of values), I have to ask Racket to do more (and duplicate) work to collect it back.

It may be that (a) values to lists and back is a cheap conversion, in which case this may not matter, or (b) that in fact the semantics of automatically seping the rest arguments are by far the common case, so that we are ok with any performance cost to undo the automatic conversion. I don't know whether either is true.

benknoble avatar Jul 27 '22 14:07 benknoble

I will point out that my original comment about "breaking change" still holds:

((flow-lambda rest (~> sep count)) 1 2 3)

is valid in current Qi but would break with the proposed change.

benknoble avatar Jul 27 '22 14:07 benknoble

I find that passing flow-lambda's rest-args as a list directly to flo will cause some consistency issues:

(define (t a #:b b . x) `(a b ,(apply + x)))
(~> (1 2 3 4 #:b -1 5 6) t)                             ; '(a b 20)
((flow-lambda (a #:b b . x) (~> t)) 1 2 3 4 #:b -1 5 6) ; ERROR -- `x` that `t` receives should be number values, but flow-lambda passes `t` a list.

Because t's arguments contain a keyword, we cannot split the input. And I think the key of the issues is that the arguments of flow-lambda should be consistent with the arguments accepted by flo.

NoahStoryM avatar Jul 29 '22 00:07 NoahStoryM

Interesting example. I suppose you could write (~> (== (gen a) (gen #:b b) (~> (gen x) sep)) t) if keywords became acceptable flows, but that would require changes everywhere (I'm not actually sure if the gens are necessary). Frankly it seems that passing keywords along is a bit difficult, because suddenly you want to be able to manipulate them. Another version without this issue might be (~> (== (gen a) (~> (gen x) sep)) (t #:b b)). But neither is as "pretty" as ((flow-lambda (a #:b b . x) t) 1 2 3 4 #:b -1 5 6). I will note for future readers that this is clearly a toy example, as in this case it would be easier to just write (t …)—the example does demonstrate that composing flows with rest arguments is painful.

benknoble avatar Jul 29 '22 16:07 benknoble