Zero-cost incompatible opaque types

Something I always wanted in Typed Racket was a way to create incompatible types which are actually the same underlying type. The idea is that maybe your application has a UserId type and a VendorId type which are both Integers, but you don't want to accidentally pass a UserId into code that wants a VendorId.

I think I have found a way to do this, but I wonder:

  • Is there a better or built-in way to do this?
  • I think this is safe, but is it really?
#lang typed/racket

(require typed/racket/unsafe)

; Create unsafe-cast which should incur zero runtime cost
(module mod-unsafe-cast typed/racket/optional
  (provide unsafe-cast)
  (define-syntax-rule (unsafe-cast a b)
    (cast a b)))
(require (submod 'mod-unsafe-cast))

(define-syntax (define-opaque-type stx)
  (syntax-case stx ()
    [(_ Opaque-Type Backing-Type opaque->backing backing->opaque)
     (with-syntax ([mod-fake-predicate (datum->syntax #f (gensym 'mod-fake-predicate))])
       #'(begin
           (module mod-fake-predicate typed/racket
             (provide opaque?)
             (define (opaque? [x : Any])
               (error "should be impossible to call this")))
           (unsafe-require/typed (submod 'mod-fake-predicate)
                                 [#:opaque Opaque-Type opaque?])
           (define-syntax-rule (opaque->backing x)
             (let ([a : Opaque-Type x])
               (unsafe-cast a Backing-Type)))
           (define-syntax-rule (backing->opaque x)
             (let ([a : Backing-Type x])
               (unsafe-cast a Opaque-Type)))))]))

(define-opaque-type Foo Fixnum Foo->Fixnum Fixnum->Foo)
(define-opaque-type Bar Fixnum Bar->Fixnum Fixnum->Bar)

(define foo : Foo (Fixnum->Foo 3))
(define bar : Bar (Fixnum->Bar 4))
(list "foo:" (ann (Foo->Fixnum foo) Fixnum)
      "bar:" (ann (Bar->Fixnum bar) Fixnum))

; These generate type errors as desired:
#;(begin
    (Bar->Fixnum foo)
    (Foo->Fixnum bar)
    (ann bar Foo)
    (ann 0 Foo))
1 Like