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... .
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 ofout-parameters
during expansion, and - a normal parameter, called
current-out-parameters
, which encapsulates the visibleout-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:
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.