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))