Injecting local contracts for prefab constructors

35 views
Skip to first unread message

Sage Gerard

unread,
May 9, 2021, 6:23:41 PM5/9/21
to Racket Users

I have a project with 57 prefab structure types. I need to construct instances using a local contract (module level contracts do not fit my needs here). Since I cannot define guards, the solution is easy enough.

(struct foo (num) #:prefab)
(define/contract make-foo (-> real? foo?) foo)

Problem: I already have a few hundred constructor calls without contracts. I could either A) rewrite them all to use contracted constructors, or B) attach local contracts in a sweet spot so that I don't have to rewrite anything else.

I prefer option B, but it doesn't look like I can attach a local contract to a constructor with `struct` alone, or even with an impersonator. When I hack around to rebind or hide the constructor's identifier, I break compatibility with `match` and `defstruct*`.

If you were in my position, what would you do?

--
~slg

Ryan Culpepper

unread,
May 9, 2021, 7:57:36 PM5/9/21
to Sage Gerard, Racket Users
I'm not clear on what constraints you're working under with respect to modules, but hopefully you can adapt this to your needs.

One option is to use a combination of `define-module-boundary-contract` (or `define/contract`) and `define-match-expander` to bind a name that can be used as a contracted constructor and as a match pattern. (If you want to extend the struct type, though, you still need to use the real one.)

Another option would be to "forge" a new compile-time struct-info based on the original struct-info but replacing the constructor.

Minimally tested sample implementations attached.

Ryan


--
You received this message because you are subscribed to the Google Groups "Racket Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to racket-users...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/racket-users/0a16cfbe-4789-a939-796e-5f6f9da21626%40sagegerard.com.
prefab-contract.rkt

Philip McGrath

unread,
May 9, 2021, 8:02:36 PM5/9/21
to ry...@racket-lang.org, Sage Gerard, Racket Users
Here's another minimally-tested sample implementation. A more robust solution might try to chaperone the struct type, as well, to protect reflective access to the constructor—but I wonder if that really makes sense when you are working with prefab structs. If you can explain more about your requirements, it might be possible to suggest better approaches.


prefab-transformer.rkt

Sage Gerard

unread,
May 9, 2021, 8:56:07 PM5/9/21
to phi...@philipmcgrath.com, ry...@racket-lang.org, Racket Users

Of course, if you're okay with a longer email. Before that, thank you both for volunteering your time to code something out. I enjoyed running into a `define-module-boundary-contract` in the wild for the first time.

I sometimes print output in a (read)able form because I like analyzing my logs. The data I print includes prefab structures, with type ids matching the topic they cover or the statements they make. You can see that I declare prefab structure (sub)types cutely labeled "messages" here. [1]

The idea is that I can either print (for example) a $package:output:built instance as a localized string, or just toss the instance itself into writeln. When I read instances back from a port, the struct accessors will help me filter and match. Hopefully all this shows where my head is.

Now for the problem. Look at [2], but don't worry about what it means. I just wanted you to see the constructor call in the exception handler. If I made a mistake and wrote that line such the exception was placed directly in the instance, then I wouldn't be able to (read) that instance back later! I cannot allow #<...> forms in my output, or some symbol soup that happens to be readable, but doesn't constitute the value it used to be.

TL;DR I want to protect the invariant `(equal? V (read (open-input-string (~s V))))` for each V I print to an output port.

Finally, as to why I didn't want the module boundary contract. The module that declares prefab structure types is also primarily responsible for creating all instances of those types. I rarely cross module boundaries when applying the constructors.

[1]: https://github.com/zyrolasting/xiden/blob/master/package.rkt#L49
[2]: https://github.com/zyrolasting/xiden/blob/master/security.rkt#L100

On 5/9/21 8:02 PM, Philip McGrath wrote:
Here's another minimally-tested sample implementation. A more robust solution might try to chaperone the struct type, as well, to protect reflective access to the constructor—but I wonder if that really makes sense when you are working with prefab structs. If you can explain more about your requirements, it might be possible to suggest better approaches.


On Sun, May 9, 2021 at 7:57 PM Ryan Culpepper <rmculp...@gmail.com> wrote:

Sage Gerard

unread,
May 9, 2021, 9:09:29 PM5/9/21
to Racket Users

Almost forgot, just in case someone asks: I want to avoid checking for invariant violations when I print. That would entail checking a bunch of values in accumulated program output, where it would be awkward to do something non-printing related, let alone raise an error. When I am printing logs, all invariant violations come down to what went a constructor, because all program output is encoded as prefab structures. That's why I want to raise any errors on instantiation.

--
~slg

Hendrik Boom

unread,
May 10, 2021, 7:05:13 AM5/10/21
to Racket Users
On Sun, May 09, 2021 at 10:23:34PM +0000, Sage Gerard wrote:
> I have a project with 57 prefab structure types. I need to construct instances using a local contract (module level contracts do not fit my needs here). Since I cannot define guards, the solution is easy enough.
>
> (struct foo (num) #:prefab)
> (define/contract make-foo (-> real? foo?) foo)
>
> Problem: I already have a few hundred constructor calls without
> contracts. I could either A) rewrite them all to use contracted
> constructors, or B) attach local contracts in a sweet spot so that I
> don't have to rewrite anything else.

Is there any chance you could do the "rewriting" in a macro so you
wouldn't actually have to change the few hundred constructor calls
when you use option A)?

-- hendrik

>
> I prefer option B, but it doesn't look like I can attach a local contract to a constructor with `struct` alone, or even with an impersonator. When I hack around to rebind or hide the constructor's identifier, I break compatibility with `match` and `defstruct*`.
>
> If you were in my position, what would you do?
>
> --
>
> ~slg
>

Sage Gerard

unread,
May 10, 2021, 10:03:17 AM5/10/21
to racket...@googlegroups.com

I hope so! That's what I understood option B to mean.

I could adjust the define-message macro according to the info in Ryan and Phillip's attachments, but I'm not well-educated on trapping struct operations. Phillip: Your example and email gives me the impression that when you add a chaperone, you can proxy all struct operations, including the gap left by the chaperone for the constructor. Is that true?

The macro I'd want has to preserve all characteristics of all generated struct bindings, which makes things tricky since the options for `struct` normally operate on a strict subset. e.g. #:[extra-]constructor-name seems to operate on the constructor alone, and the #:extra- variant does not preserve the match-expander/super-id characteristics. #:constructor-name is closer, but the original constructor id is no longer directly callable by the declaring module.

David Storrs

unread,
May 12, 2021, 10:12:29 AM5/12/21
to Sage Gerard, Racket Users
If you're willing to accept a low tech solution, might I suggest this:

$ perl -i.bak -lpe 's/\(foo/\(make-foo/g' *.rkt


Also, I'll self-plug and point you towards this:

#lang racket
(require struct-plus-plus )

(struct++ foo ([a real?]) (#:omit-reflection) #:prefab)

(define checked (foo++ #:a 7)) ; contract is checked                                          
(define not-checked (foo 7)) ; contract is not checked                                        
(writeln checked)
(writeln not-checked)

This defines the contracts using define/contract.  It's not clear to me if that meets your needs, but hopefully it does.

Reply all
Reply to author
Forward
0 new messages