I have just opened a GitHub issue on the main Racket repository containing a proposed significant extension to racket/unit
. The short summary is that I am proposing adding support for signatures to import
other signatures directly, primarily to allow their bindings to be used in contracted
clauses. You can read the full proposal here:
opened 09:45AM - 24 Jan 23 UTC
feature request
api design
Though it does not seem to get much use these days, I have always had an appreci… ation for `racket/unit`. First-class modules are useful. Unfortunately, just about every time I try to use it, I find I run into the same problem: it is difficult to bring bindings into scope that I want to use in my signatures’ contracts.
## Motivation
To illustrate the issue, suppose I have a signature that provides an interface to an abstract cell type:
```racket
(define-signature cell-type^
[(contracted
[cell? predicate/c]
[make-cell (-> any/c cell?)]
[cell-ref (-> cell? any/c)]
[cell-set! (-> cell? any/c void?)])])
```
Now suppose I define another signature that provides some convenience operations on cells:
```racket
(define-signature cell-ops^
[(contracted
[cell-swap! (-> cell? any/c any/c)]
[cell-copy (-> cell? cell?)])])
```
The problem with this signature is that the `cell?` predicate is not in scope. This is annoying, because the whole point of having a separate `cell-ops^` signature is to parameterize my `cell-ops@` unit over the particular `cell-type^` implementation. Currently, there are two available workarounds that I can see:
1. Make `cell-ops^` extend or `open` the `cell-type^` signature (or just explicitly make `cell-ops^` also export `cell?`).
This brings all the `cell-type^` bindings into scope, but it clutters the implementation of `cell-ops@`. Unlike with `require` and `provide`, a unit cannot directly re-export one of its imports, so `cell-ops@` must contain a `define` for each of the members of the `cell-type^` signature:
```racket
(define-unit cell-ops@
(import (prefix type: cell-type^))
(export cell-ops^)
(init-depend cell-type^)
(define cell? type:cell?)
(define make-cell type:make-cell)
(define cell-ref type:cell-ref)
(define cell-set! type:cell-set!)
....)
```
This boilerplate can be partially mitigated using a macro, but it also complicates unit linking: inferred linking fails if multiple units export the same signature. That can be further worked around using signature tags, but tags introduce their own complications.
2. Define each of the contracts of `cell-ops^` as separate signature elements, like this:
```racket
(define-signature cell-ops^
[cell-swap!/c
cell-copy/c
(contracted
[cell-swap! cell-swap!/c]
[cell-copy cell-copy/c])])
```
Though it’s a bit annoyingly verbose, this strategy works okay if there is truly only ever one `cell-ops^` implementation that is simply linked against different `cell-type^` units. However, since this is essentially just putting a contract on the unit itself, it somewhat defeats the purpose of using signature contracts.
I am not satisfied with either of these workarounds. They are awkward and frustrating, and it feels like there must be a better way.
## Proposed solutions
What I *really* want to do is to be able to import the `cell-type^` signature from my `cell-ops^` signature itself, like this:
```racket
(define-signature cell-ops^
(import cell-type^)
[(contracted
[cell-swap! (-> cell? any/c any/c)]
[cell-copy (-> cell? cell?)])])
```
Indeed, I have been considering adding support for this very feature to `racket/unit` for some time. But it raises some questions about what the semantics actually ought to be. Here are, I think, the provident questions:
* How is this import resolved when linking? Does the signature itself now need to be linked against a unit, or does the import just add linking requirements to units that export `cell-ops^`?
* Does this `import` make the `cell-type^` bindings automatically available in any unit that exports `cell-ops^`? If so, is it an error for a unit that exports `cell-ops^` to explicitly import `cell-type^` (without a tag)?
* If a unit exports `cell-ops^` with a tag, do those tags propagate to the signature’s imports? If not, is there any way to alter the tags of a signature’s exports without modifying the signature?
Broadly speaking, I think there are two distinct designs that feel plausible to me, each with a different set of answers to the above questions:
1. The simplest design is to effectively prepend each `import` declarations in a signature to each unit that exports the signature.
This feels like probably the most appropriate design for `racket/unit`, as it is consistent with the way `define-syntaxes` and `define-values` work when used in signatures. Since it defers the ultimate linking obligations to exporting units, it does not require any changes to the way linking is currently specified.
However, this approach does have some downsides. For one, it doesn’t provide any obvious way for a unit that exports the signature to control the tags on imports introduced by the signature. This means that certain linking configurations become impossible: a unit that exports the same signature multiple times (using different tags) will have no ability to link each exported signature’s body expressions against different units. This feels a bit fishy, and it seems to hint that this is something an abuse of signatures.
2. An alternative perspective is to imagine that signatures that contain expressions are effectively a combination of a distinct, miniature unit and a “pure” signature that only defines the exported names.
In this view, *signatures themselves* can have linking obligations, and it is possible to link a signature against units providing other signatures. Fully explicit linkage for the `cell-op^` example might look like this:
```racket
(compound-unit
(import)
(export TYPE OPS)
(link [([TYPE : cell-type^]) box-cell-type@]
[([OPS : (cell-ops^ TYPE)]) cell-ops@ TYPE]))
```
Note the `(cell-ops^ TYPE)` form that explicitly links the `cell-ops^` signature against the `box-cell-type@` unit.
I think this design is fairly compelling, as it feels more in the spirit of how units are supposed to work. Signatures define the language of linking obligations, and the linkage sublanguage of `compound-unit` explicitly resolves those obligations. Though the fully-explicit form is verbose, this strategy does not conceptually interfere with inferred linking, so most users should not need to write the linkage declarations explicitly.
The main downside to this approach seems to be that it is significantly more complicated to implement. Currently, signatures have no real runtime component, so a significant amount of code would have to change to adapt to this new linking model.
I find the second of these designs compelling enough that I may try to implement it myself. I do not expect a great deal of feedback on this, as I think `racket/unit` does not have many active users. However, I would appreciate any thoughts that people may have.
I have generally gotten the sense that units are not used at all by most Racketeers, so I do not expect a great outpouring of discussion on this issue. However, if you do use units, I would much appreciate if you could weigh in on the GitHub issue thread. Even minor comments would be helpful.
I did have an idea of signatures being able to hide exports, or have exports that are only visible to the contracts of other exports, which I believe would solve the issues that I've been having. It's not ideal compared to the full re-working that you've proposed, but it does also seem much easier to possibly implement.
P.S. Sorry for reviving a year old topic