Your preferences are my command. :)
**** Actual Performance Bottlenecks (and Workarounds) ****
(1) Plot's sending of `snip%` instances from untyped to typed code made
them so slow that they were unresponsive. I got around it by making
helper functions to create the snips, and inserting their types directly
into the base type environment using the
`typed-racket/base-env/extra-env-lang` language. (See
"plot-gui-lib/plot/private/gui/lazy-snip-typed.rkt".)
(2) Again in Plot, I needed to give types to `dc` (from `pict`), and
helper functions to create `pdf-dc%`, etc., instances. To keep rendering
plots using those targets from taking $MANY MB memory each (I think it
was in the range 7-14 MB), I inserted the helper functions' types
directly into the base type environment. (See
"plot-lib/plot/private/no-gui/evil.rkt".)
(3) In Pict3D, see "pict3d/private/gui/pict3d-bitmap.rkt" and
"pict3d/private/gui/pict3d-canvas.rkt" for more examples of the same.
Using `require/typed` would have forced me to favor either typed or
untyped use of Pict3D for rendering on canvases and bitmaps. Using
`extra-env-lang` is a little dangerous, but favors both.
(4) Again in Pict3D: Racket's FFI doesn't have a typed interface, so I
wrote a subset of it using the `extra-env-lang` language. I couldn't
have used `require/typed` because `memcpy` and similar functions have
cases that can't be distinguished by arity.
(5) I did the same for OpenGL. I could have used `require/typed`, but
speed tests showed I would get at most 5000 OpenGL calls per 60Hz frame
that way, which is too few to do anything serious. Using
`extra-env-lang` to insert the types into the base type environment, the
upper bound is around 60000 OpenGL calls per frame, which is plenty.
(6) Anything polymorphic takes a lot of time to cross the boundary,
especially if it's higher-order. One higher-order example is using
`math/array` from untyped code: operations on newly created arrays take
over 50x the time they do in typed code. Jens Axel and Ryan Culpepper
have worked around it by wrapping matrices (which are arrays) with
something like
(struct matrix ([value : (Matrix Real)]) #:transparent)
or some concrete type other than `Real`. Then they create wrapper
functions for `math/matrix` exports. The `matrix?` contract is O(1), so
`matrix` instances cross over cheaply.
(7) Untyped Pict3D users wouldn't tolerate a deep check of all shapes in
a scene every time it crosses the contract boundary. (Complex scenes
have thousands of shapes, and scenes cross over a lot.) But I also want
users to eventually be able to extend Pict3D with new kinds of shapes.
So scene functions that with polymorphism would look like this:
(: add-shape (All (A) (-> (Scene A) A (Scene A))))
look like this instead:
(: add-shape (-> Scene Shape Scene))
where `Shape` is a struct type that new shapes must inherit from.
**** General Observations ****
(A) The contract boundary makes objects very slow and very memory-heavy.
(B) Using `extra-env-lang` to work around (A) doesn't seem to be
terribly dangerous.
(C) The contract boundary makes operations on polymorphic data types
slow, and for higher-order data, they stack. For polymorphic,
first-order data, they're O(n), where n is the size of the data. For
polymorphic, higher-order data, operations are O(n*k), where k is the
number of crossings.
(D) Tricks (6,7) for getting around performance bottlenecks in (C) don't
generalize to full polymorphism.
(E) I've used `extra-env-lang` to insert types into the base type
environment a lot. But I've often been able to avoid using it by
changing the types of data (e.g. polymorphic to monomorphic), or by
moving or shrinking an abstraction boundary. In the examples I gave,
changing types or boundaries wouldn't work or would be too invasive (2),
or would change an existing user-facing API in a backward-incompatible
way (1).
(F) Some applications (5) are ridiculously sensitive to contract
overhead, even fast first-order contracts.
(G) There are functions that `require/typed` can't deal with because it
can't generate contracts for them (4). This isn't usually a problem when
you're writing both the typed and untyped code, but it's a problem when
the untyped code you want to use in TR isn't yours and has been in
widespread use for over a decade.
**** Comments and Speculation ****
I'll assume speculation is more OK now that we have examples. :)
It's in our nature to want to tackle (G) first because it's
well-defined. But it's also the least pressing. (You can always write an
untyped wrapper and use `require/typed` to import the wrapper. I know
this goes against TR's goals, but the workaround is easy and obvious.)
Here's how I would prioritize:
1. Polymorphic, first-order data
2. Polymorphic, higher-order data
3. Class contracts
4. Contracts for weird functions like `memcpy`
In the meantime, throw us a bone like `require/typed/unsafe` to replace
`extra-env-lang`. I would love to have something like it, if only to
avoid creating two new files and remembering all the crazy incantations
when I have to use `extra-env-lang`.
Neil ⊥