On this page:
8.1 Example Interactions
8.1.1 Checking Immutable Data:   Importing a List
8.1.2 Checking Mutable Data:   Importing a Vector
8.1.3 Checking Functions that Cross Multiple Boundaries
8.2 Forms that Depend on the Behavior of Types
8.2.1 Example:   Casts in Deep, Shallow, and Optional
8.3 How to Choose Between Deep, Shallow, and Optional
8.3.1 When to Use Deep Types
8.3.2 When to Use Shallow Types
8.3.3 When to Use Optional Types
8.3.4 General Tips
8.4 Related Gradual Typing Work
8.11.1.3

8 Deep, Shallow, and Optional Semantics 🔗

 #lang typed/racket/deep package: typed-racket-lib

 #lang typed/racket/base/deep package: typed-racket-lib

 #lang typed/racket/shallow package: typed-racket-lib

 #lang typed/racket/base/shallow package: typed-racket-lib

 #lang typed/racket/optional package: typed-racket-lib

 #lang typed/racket/base/optional package: typed-racket-lib

See also: Typed-Untyped Interaction in the Typed Racket Guide.

Typed Racket allows the combination of both typed and untyped code in a single program. Untyped code can freely import typed identifiers. Typed code can import untyped identifiers by giving them types (via require/typed).

Allowing typed/untyped combinations raises questions about whether and how types should constrain the behavior of untyped code. On one hand, strong type constraints are useful because they can detect when a typed-untyped interaction goes wrong. On the other hand, constraints must be enforced with run-time checks, which affect run-time performance. Stronger constraints generally impose a higher performance cost.

By default, Typed Racket provides Deep types that strictly constrain the behavior of untyped code. But because these constraints can be expensive, Typed Racket offers two alternatives: Shallow and Optional types. All three use the same static types and static checks, but they progressively weaken the run-time behavior of types.

8.1 Example Interactions 🔗

The examples below show how Deep, Shallow, and Optional change the run-time behavior (or, the semantics) of types.

8.1.1 Checking Immutable Data: Importing a List 🔗

When typed code imports an untyped list:
  • Deep types check each element of the list at the boundary to untyped code;

  • Shallow types check for a list, and check elements when they are accessed; and

  • Optional types check nothing.

The following examples import the function string->list, which returns a list of characters, and use an incorrect type that expects a list of strings. Both Deep and Shallow types catch the error at some point. Optional types do not catch the error.

Deep types prevent a list of characters from entering typed code with the type (Listof String):

#lang typed/racket ; or #lang typed/racket/deep
 
(require/typed racket/base
  [string->list (-> String (Listof String))])
(string->list "racket")

string->list: broke its own contract

  promised: string?

  produced: #\r

  in: an element of

      the range of

      (-> any/c (listof string?))

  contract from: (interface for string->list)

  blaming: (interface for string->list)

   (assuming the contract is correct)

  at: eval:1:0

Shallow types allow a list of characters to have the type (Listof String), but detect an error if typed code reads an element from the list:

#lang typed/racket/shallow
 
(require/typed racket/base
  [string->list (-> String (Listof String))])
 
(define lst (string->list "racket"))
(first lst)

shape-check: value does not match expected type

  value: #\r

  type: String

  lang: 'typed/racket/shallow

  src: '(eval 3 0 3 1)

Optional types do not detect any error in this example:

#lang typed/racket/optional
 
(require/typed racket/base
  [string->list (-> String (Listof String))])
 
(define lst (string->list "racket"))
(first lst)

- : String

#\r

8.1.2 Checking Mutable Data: Importing a Vector 🔗

When typed code imports an untyped vector:
  • Deep types wrap the vector in a contract that checks future reads and writes;

  • Shallow types check for a vector at the boundary, and check elements on demand (same as for lists); and

  • Optional types check nothing.

The following example imports make-vector with an incorrect type that expects a vector of strings as its output. When make-vector returns a vector of numbers instead, both Deep and Shallow types catch the error when reading from the vector. Optional types do not catch the error.

Deep catches a bad vector element:
#lang typed/racket ; or #lang typed/racket/deep
 
(require/typed racket/base
  [make-vector (-> Integer (Vectorof String))])
 
(define vec (make-vector 10))
(vector-ref vec 0)

make-vector: broke its own contract

  promised: string?

  produced: 0

  in: an element of

      the range of

      (-> any/c (vectorof string?))

  contract from: (interface for make-vector)

  blaming: (interface for make-vector)

   (assuming the contract is correct)

  at: eval:3:0

Shallow catches a bad vector element:
(vector-ref vec 0)

shape-check: value does not match expected type

  value: 0

  type: String

  lang: 'typed/racket/shallow

  src: '(eval 6 0 6 1)

Optional does not catch a bad element:
(vector-ref vec 0)

- : String

0

8.1.3 Checking Functions that Cross Multiple Boundaries 🔗

Deep types can detect some errors that Shallow types miss, especially when a program contains several modules. This is because every module in a program can trust that every Deep type is a true claim, but only the one module that defines a Shallow type can depend on the type. In short, Deep types are permanent whereas Shallow types are temporary.

The following example uses three modules to create a situation where Deep types catch an error that Shallow types miss. First, the untyped module racket/base provides the standard string-length function. Second, a typed interface module imports string-length with an incorrect type and reprovides with a new name: strlen. Third, a typed client module imports strlen with a correct type and calls it on a string.

Deep types raise an error when strlen is called because of the incorrect type in the interface:

#lang typed/racket ; or #lang typed/racket/deep
 
(module interface typed/racket
  (require/typed racket/base
    [string-length (-> String Void)])
  (define strlen string-length)
  (provide strlen))
 
(require/typed 'interface
  [strlen (-> String Natural)])
(strlen "racket")

string-length: broke its own contract

  promised: void?

  produced: 6

  in: (-> any/c void?)

  contract from: (interface for string-length)

  blaming: (interface for string-length)

   (assuming the contract is correct)

  at: eval:6:0

Shallow types do not raise an error because the interface type is not enforced for the outer client module:

#lang typed/racket/shallow
 
(module interface typed/racket/shallow
  (require/typed racket/base
    [string-length (-> String Void)])
  (define strlen string-length)
  (provide strlen))
 
(require/typed 'interface
  [strlen (-> String Natural)])
(strlen "racket")

- : Integer [more precisely: Nonnegative-Integer]

6

Optional types do not raise an error either:

#lang typed/racket/optional
 
(module interface typed/racket/optional
  (require/typed racket/base
    [string-length (-> String Void)])
  (define strlen string-length)
  (provide strlen))
 
(require/typed 'interface
  [strlen (-> String Natural)])
(strlen "racket")

- : Integer [more precisely: Nonnegative-Integer]

6

8.2 Forms that Depend on the Behavior of Types 🔗

The following Typed Racket forms use types to create run-time checks. Consequently, their behavior changes depending on whether types are Deep, Shallow, or Optional.

Across these forms, the changes are roughly the same. Deep types get enforced as (higher-order) contracts, Shallow types get enforced as shape checks, and Optional types get enforced with nothing. The key point to understand is which types get enforced at run-time.

The following forms modify the contracts that Deep Typed Racket generates. Uses of these forms may need to change to accommodate Shallow and Optional clients.

8.2.1 Example: Casts in Deep, Shallow, and Optional 🔗

To give one example of a form that depends on the behavior of types, cast checks full types in Deep mode, checks shapes in Shallow mode, and checks nothing in Optional mode.

Deep detects a bad cast:
; #lang typed/racket
; or #lang typed/racket/deep
> (cast (list 42) (Listof String))

(cast for #f): broke its own contract

  promised: string?

  produced: 42

  in: an element of

      (listof string?)

  contract from: cast

  blaming: cast

   (assuming the contract is correct)

  at: eval:9:0

Shallow allows one bad cast but detects a shape-level one:
; #lang typed/racket/shallow
> (cast (list 42) (Listof String))

- : (Listof String)

'(42)

> (cast (list 42) Number)

shape-check: value does not match expected type

  value: '(42)

  type: Number

  lang: 'typed/racket/shallow

  src: '(eval 11 0 11 1)

Optional lets any cast succeed:
; #lang typed/racket/optional
> (cast (list 42) (Listof String))

- : (Listof String)

'(42)

> (cast (list 42) Number)

- : Number

'(42)

8.3 How to Choose Between Deep, Shallow, and Optional 🔗

Deep, Shallow, and Optional types have complementary strengths and weaknesses. Deep types give strong type guarantees and enable full type-directed optimizations, but may pay a high cost at boundaries. In particular, the costs for higher-order types are high. Examples include HashTable, ->*, and Object. Shallow types give weak guarantees, but come at a lower cost. The cost is constant-time for many types, including HashTable and ->*, and linear-time for a few others such as U and Object. Optional types give no guarantees, but come at no cost.

Based on these tradeoffs, this section offers some advice about when to choose one style over the others.

8.3.1 When to Use Deep Types 🔗

Deep types are best in the following situations:

8.3.2 When to Use Shallow Types 🔗

Shallow types are best in the following situations:

8.3.3 When to Use Optional Types 🔗

Optional types are best in the following situations:

8.3.4 General Tips 🔗

8.4 Related Gradual Typing Work 🔗

Shallow Typed Racket implements the Transient semantics for gradual languages [Programming-2022, PLDI-2022], which was invented by Michael M. Vitousek [RP:DLS-2014, RP:POPL-2017, RP:Vitousek-2019, RP:DLS-2019]. Transient protects typed code by rewriting it to defensively check the shape of values whenever it calls a function, reads from a data structure, or otherwise receives input that may have come from an untyped source. Because of the rewriting, Transient is able to enforce type soundness without higher-order contracts.

Deep Typed Racket implements the standard semantics for gradual languages, which is known variously as Guarded [RP:POPL-2017], Natural [TOPLAS-2009], and Behavioral [KafKa-2018]. This Guarded semantics eagerly checks untyped values when possible and otherwise creates wrappers to defer checks.

Typed Racket uses the names “Shallow” and “Deep” rather than “Transient” and “Guarded” to emphasize the guarantees that such types provide instead than the method used to implement these guarantees. Shallow types provide a type soundness guarantee; Deep types provide type soundness and complete monitoring [OOPSLA-2019].

Optional types are a widely-used approach to gradual typing, despite their unsound support for typed-untyped interactions. Optionally-typed languages include the following: TypeScript, Flow, mypy, and Typed Clojure [ESOP-2016, Bonnaire-Sergeant-2019].