I've recently tried writing functions that return multiple values for some calculations that chain together, and I feel that I am likely doing something incorrectly. When I attempt to chain them I can't find a way to get it work without wrapping each of the sequential function calls in a thunk. Wouldn't this incur a performance loss (not to mention a lot of extra typing)? Theres enough friction chaining them together that I feel like I have to do be doing something wrong
An example of what I mean:
(define (test-func0 x y)
(values (+ x 1) (+ y 1)))
(define (test-func1 x y)
(values (* x 2) (* y 2)))
(define (test-func2 x y)
(+ x y))
(call-with-values (lambda () (call-with-values (lambda () (test-func0 1 1)) test-func1)) test-func2)
What you did isn't wrong, but you could more concisely write:
((compose test-func2 test-func1 test-func0) 1 1)
The compiler recognizes call-with-values, so it can avoid creating unnecessary closures. (Also, Racket in general works to make patterns like this perform reasonably.)
There is some overhead for compose, because:
It is just a function, so it doesn't even know how many arguments it's being called with until runtime; and
It is extremely general, preserving the arity (including potential keywords) of the first function in the pipeline and allowing for any number of values in intermediate steps.
However, compose does put effort into optimizing common cases, which happens to include procedures of arity 2, so your example won't fall into the worst-case fallback.
The performance overhead is why compose1 exists. You could define a compose2 if it were relevant for your purposes, or you could use a macro to compose statically.
I started looking through the source code for compose and compose1 to see if I could write a static version like you had mentioned, and saw that the general case (at least from my understanding of the code) was doing a similar thing with call-with-values. Assuming that Racket will optimize the closures away I wrote this macro (very quickly might I add, this probably isn't the ideal way to deal with an arbitrary number of arguments but it works well enough for my use case) in case anyone else wanted a more succinct way to write it: