Out-parameters in Racket

Hi, Racket Discourse.

Long time no see. I have C# coming out my ears, lately, so I thought to make a bit of a racket to clear the air... :drum:.

I really enjoy using out-parameters in C#, and I think they make some nice patterns more ergonomic.

For those of you who are unfamiliar with the concept, an out-parameter is a parameter which allows a method (or procedure, really), to return multiple values, by exposing the assignment of reference values among the formal parameters. These out-parameters can then be assigned to in the body of the method-definition, and read from at the call-site, like so:

var isPresent = dict.TryGetValue(key, out var value);
    ^^^^^^^^^                         ^^^^^^^^^^^^^^
   |                                  the out-parameter, which is either null or the value of the key, if it exists
   |
    a boolean return value, depending on whether the dictionary contains the value

if (isPresent)
{
    doSomething(value);
}

In C#, for example, these out-parameters carry additional constraints, like the requirement of assigning a value to one if not nullable before a method body exits.

The out-parameters can also be passed down into other out-parameter methods, like so:

public bool Strategy<TFind>
(
    PathSearchPoolUpdate<TPath>                          update,
    [NotNullWhen(true)] [MaybeNullWhen(false)] out TFind target
)
    where TFind : ApiPath<TPath>
{
    IEnumerable<IPathSearch<TPath>> pool = [this];
    while (pool.Any())
    {
        var next = pool.First();
        if (next.On(out target)) return true;
                    ^^^^^^^^^^
                    we pass the target out-parameter down into the `next.On` method
        else pool = update(next, pool.Skip(1));
    }
    target = default; return false;
}

My thinking was that this seems very similar to what a "parameter" is in Racket, so why not try to build such a feature with those as the building-blocks, so to speak?

The plan was to use #%top and #%app (although I am still on the fence about the latter), to produce and consume these values.

Thanks to @Eutro and @samth for their packages, cadnr and fancy-app, which have been invaluable in figuring out the story, so far.

The idea is that one declares an out-parameter arbitrarily, using a reader-macro called #out. This reader-macro takes the next identifier after it and adds to it a syntax-property identifying it as an 'out-parameter-identifier.

There are basically two distinct "contexts" in which an out-parameter may appear:

  • a producer context, i.e. among the formals of a procedure of one or more out-parameters,
  • a consumer context, i.e. a call to a procedure of one or more out-parameters.

In a consumer context, the system either creates a new out-parameter on the spot and records it for later use, or uses an existing one, if present.

It knows how to do this, because of #%top, which is called upon when an unbound identifier is encountered. That is to say, one knows exactly when an identifier has been recorded as an out-parameter or not, by the fact that %#top is looking at it and whether it has the required syntax-property.

The producer context I am still ambivalent about, since it requires "shielding" to my mind, although I suspect that there may still be a more elegant solution.

In any case, there exists a pair of:

  • a syntax-parameter, called current-out-identifiers, which tracks the identifiers of out-parameters during expansion, and
  • a normal parameter, called current-out-parameters, which encapsulates the visible out-parameters in a particular context (at phase-0, that is).

These two values are parameterized over the body of a producer, to ensure that out-parameters do not "leak" beyond the body, although technically they could be omnipresent due to their identity as parameters.

Finally, to allow for parameter elision, i.e. declaring something as an out-parameter but ignoring its value, we call on #%app, haha.

When #%app encounters an application containing one or more out-parameters which match the _ syntax, it replaces those with un-recorded normal parameters (among the tracked identifiers), and assigns to them generated symbols, so they exist in the current parameterization, but are not referenceable in the declaring context.

I have declined to restrict the assignment of out-parameters as in C#--meaning, that they have to be assigned in the body of an out-parameter procedure--because it is not obvious to me how one would keep track of re-assignment, i.e. what if I call #out param something else by redefining it as (define param′ param).

Technically it will still be assigned to if param′ is used, but I might not be able to detect it. So, for now, no restrictions on that, although some phase-0 checks could be in order.

As an additional note, due to the utility of #%top, it is unnecessary to use a var disambiguator to distinguish between "producer" and "consumer" contexts fresh and known identifiers, as in C#.


Now for some examples:

#lang reader "reader.rkt"

(require
  "syntax.rkt")

;; produce an out-parameter:
(define (sub1? n #out m)
  (and (< 0 n) {m (- n 1)}))

;; consume out-parameters:
(define n 4)
(and
 (sub1?  n    #out n-1)
 (sub1? {n-1} #out n-2)
 (sub1? {n-2} #out n-3)
 {n-3})
;=> 1

;; produce an out-parameter
(define (hash-ref? hash key #out val)
  (and (hash-has-key? hash key) {val (hash-ref hash key)}))

;; consume out-parameters
(define json
  (hash 'person
        (hash 'details
              (hash 'name    'john
                    'surname 'donn))))
(and
 (hash-ref?  json     'person  #out person)
 (hash-ref? {person}  'details #out details)
 (hash-ref? {details} 'name    #out name)
 {name})
;=> 'john

(define (div/mod x n #out remainder)
  (define-values (quo rem) (quotient/remainder x n))
  (and {remainder rem} quo))

(define quo (div/mod 12 5 #out rem))

(values quo {rem})
;=> 2 2

And it works with arrows:
2025-07-16113811-out-params


P.S. tell me what you think of #āŽ‹ as opposed to #out as the reader-macro symbol. I kind of like it, but it is very faint on some displays.

P.P.S. although just an experiment still, any interested Racketeers are happy to have a look, here:

Documentation and more examples/utilities still to come, time allowing.

In the 1990s a now-famous type researcher worked with a colleague. She ported a large C++ program, where a similar pattern can be used. The resulting code contained a large number of procedures that consumed boxes to be filled with return values that indicated whether they were filled (if possible). I spent weeks analyzing this code with a couple of static analysis tools. They all faltered on such ā€œout parametersā€. In the end, I had to refactor all these procedures to use multiple values (actually on occasion different number of return values). The analysis tools could then cope. (Yes, our contract and type systems don’t do too well with these patterns either.)

1 Like