Function and macro to define unique values

I've seen the idiom

(struct unique-value ())

to create unique values, for example as default arguments in a function to check with (eq? argument unique-value) if anything was passed for the argument.

Since I have only little experience with Racket macros so far, I took this as a motivation to define a macro around this. (But in the end, because of the helper function, the macro itself turned out much simpler than I had expected.) To be useful to as many readers as possible I added comments more extensively than I usually do, even though I certainly learned a lot myself and therefore had added some of the comments anyway. :slight_smile:

#lang racket/base

(require
  racket/format
  racket/function)

; Define a helper function `make-unique-value` to generate
; a value of a struct (that only exists locally).
;
; Note that we must return a value of the struct, not
; the struct (`unique-value`) itself because otherwise the
; custom write method isn't applied.
(define (make-unique-value name)
  (struct unique-value ()
    ; See https://docs.racket-lang.org/reference/define-struct.html#%28form._%28%28lib._racket%2Fprivate%2Fbase..rkt%29._struct%29%29
    ; and https://docs.racket-lang.org/reference/Printer_Extension.html#%28def._%28%28lib._racket%2Fprivate%2Fbase..rkt%29._gen~3acustom-write%29%29
    #:methods gen:custom-write
      ; Pre-set the name as the rightmost argument because
      ; the function for `write-proc` must use three arguments,
      ; but we need to get the name somehow into `write-proc`.
      [(define write-proc (curryr unique-value-write name))])
  (unique-value))

(define (unique-value-write unique-value port mode name)
  (write-string (~a "#<unique:" name ">") port))

; Test the helper function.
(define foo (make-unique-value "foo"))
; #<unique:foo>
(displayln foo)

; Macro that wraps the helper function and allows us to use
; the syntax `(define-unique name)`.
;
; To convert the new identifier name to something that can be
; processed in the helper function, use `quote` to convert the
; name to a symbol, see
; https://rmculpepper.github.io/malr/basic.html .
(define-syntax-rule (define-unique name)
  ; Strictly speaking, we don't need `symbol->string` here
  ; because the custom write function formats 'name exactly
  ; like "name", but let's be clear on the intended types.
  (define name (make-unique-value (symbol->string (quote name)))))

(define-unique bar)
; #<unique:bar>
(displayln bar)

I wouldn't be surprised if this existed already in a package or even the standard library, but I wanted to experiment with this. :smiley:

3 Likes

I just realized that even though the macro does little beyond the helper function, using the macro has the advantage that you can't accidentally define two unique values that print the same (which could be confusing during debugging):

(define u1 (make-unique-value "abc"))
(define u2 (make-unique-value "abc"))
u1  ; #<unique:abc>
u2  ; #<unique:abc>
(eq? u1 u2)  ; #f

On the other hand, if you use only the macro, I think you can't (in the same scope) create multiple unique values with the same printed name:

(define-unique u3)
u3  ; #<unique:u3>
(define-unique u3)  ; module: identifier already defined in: u3

Experimenting is of course always good. Regarding your Lorite hunch: One example I've seen is in Rebellion by @notjack.

2 Likes

Of course, Rebellion has everything. :slight_smile: :+1:

2 Likes

Would gensym do what you want? It produces a symbol that is guaranteed to be unique across the run of a program. Here's what I get in my local REPL:

> (gensym)        ; 'g6178304
> (gensym "foo")  ; 'foo6178305
> (gensym 'foo)   ; 'foo6178306
1 Like

That's nice! I'm quite sure now that I had seen this in the past.

Although I like the macro approach a bit more, I like that gemsym is in the standard library, so I'll probably use it the next time I need a unique value.

Also, I forgot to add: gensym has the advantage that it's a procedure, so you can pass it as an argument.

1 Like

Why don't you store the name inside the struct?

#lang racket/base

(require racket/format)

(define (make-unique-value name)
  (define (unique-value-write x port mode)
    (write-string (~a "#<unique:" (unique-value-name x) ">") port))
  (struct unique-value (name)
    #:methods gen:custom-write
      [(define write-proc unique-value-write)])
  (unique-value name))

(define foo (make-unique-value "foo"))
(displayln foo)
2 Likes

In case you don't want the number in the name, instead of gensym you can use string->uninterned-symbol

#lang racket/base
(define f1 (string->uninterned-symbol "foo"))
(define f2 (string->uninterned-symbol "foo"))
f1          ; ==> 'foo
f2          ; ==> 'foo
(eq? f1 f2) ; ==> #f
2 Likes

Good point.

Because I was so focused on the origin

(struct unique-value ())

that I forgot to check the code for your change later. :wink: The custom write method came later after the functionality was in place. When I look at my code now, your suggestion looks obvious. :slight_smile: