Chaining together functions with multiple return values

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)

Am I missing something?

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:

  1. It is just a function, so it doesn't even know how many arguments it's being called with until runtime; and
  2. 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.

compose works, as @LiberalArtist points out.

(let*-values
    ([(s t) (test-func0 1 1)]
     [(s t) (test-func1 s t)]
     [(s) (test-func2 s t)])
  s)

is my preferred way of writing such expressions, unless I really want function composition in the mathematical sense.

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:

(define-syntax (call-with-values-chained stx)
  (syntax-parse stx
    [(_ last-elem)
     #'last-elem]
    [(_ first-elem (~seq rest-lst ...))
     #'(call-with-values (lambda () (call-with-values-chained rest-lst ...)) first-elem)]))
1 Like

This is also a perfect opportunity to show Qi syntax (from (require qi) after installing the package).

(~> (1 1) test-func0 test-func1 test-func2)
2 Likes