7 Language and Performance
When you write a module, you first pick a language. In Racket you can choose a lot of languages. The most important choice concerns racket/base vs racket.
For scripts, use racket/base. The racket/base language loads significantly faster than the racket language because it is much smaller than the racket.
If your module is intended as a library, stick to racket/base. That way script writers can use it without incurring the overhead of loading all of racket unknowingly.
Conversely, you should use racket (or even racket/gui) when you just want a convenient language to write some program. The racket language comes with almost all the batteries, and racket/gui adds the rest of the GUI base.
7.1 Library Interfaces
Imagine you are working on a library. You start with one module, but before you know it the set of modules grows to a decent size. Client programs are unlikely to use all of your library’s exports and modules. If, by default, your library includes all features, you may cause unnecessary mental stress and run-time cost that clients do not actually use.
In building the Racket language, we have found it useful to factor
libraries into different layers so that client programs can selectively
import from these bundles. The specific Racket practice is to use the most
prominent name as the default for the module that includes everything. When
it comes to languages, this is the role of racket. A programmer who wishes
to depend on a small part of the language chooses to racket/base instead;
this name refers to the basic foundation of the language. Finally, some of
Racket’s constructs are not even included in racket—
Other Racket libraries choose to use the default name for the small core. Special names then refer to the complete library.
We encourage library developers to think critically about these decisions and decide on a practice that fits their taste and understanding of the users of their library. We encourage developers to use the following names for different places on the "size" hierarchy:
library/kernel, the bare minimal conceivable for the library to be usable;
library/base, a basic set of functionality.
library, an appropriate "default" of functionality corresponding to either library/base or library/full.
library/full, the full library functionality.
Finally, the advice of the previous section, to use racket/base when building a library, generalizes to other libraries: by being more specific in your dependencies, you are a responsible citizen and enable others to have a small (transitive) dependency set.
7.2 Macros: Space and Performance
Macros copy code. Also, Racket is really a tower of macro-implemented languages. Hence, a single line of source code may expand into a rather large core expression. As you and others keep adding macros, even the smallest functions generate huge expressions and consume a lot of space. This kind of space consumption may affect the performance of your project and is therefore to be avoided.
When you design your own macro with a large expansion, try to factor it into a function call that consumes small thunks or procedures.
|
|
As you can see, the macro on the left calls a function with a list of the searchable values and a function that encapsulates the body. Every expansion is a single function call. In contrast, the macro on the right expands to many nested definitions and expressions every time it is used.
7.3 No Contracts
Adding contracts to a library is good.
We will soon supply a Reference section in the Evaluation Model chapter that explains the basics of our understanding of “safety” and link to it. Warning Splitting contracted functionality into two modules in this way renders the code in the no-contract module unsafe. The creator of the original code might have assumed certain constraints on some functions’ arguments, and the contracts checked these constraints. While the documentation of the no-contract submodule is likely to state these constraints, it is left to the client to check them. If the client code doesn’t check the constraints and the arguments don’t satisfy them, the code in the no-contract submodule may go wrong in various ways.
The first and simplest way to create a no-contract submodule is to use the #:unprotected-submodule functionality of contract-out.
|
|
The module called good illustrates what the code might look like originally. Every exported functions come with contracts, and the definitions of these functions can be found below the provide specification in the module body. The fast module on the right requests the creation of a submodule named no-contract, which exports the same identifiers as the original module but without contracts.
|
|
Notice, however, that when you run these two client modules—
'(3.141592653589793 3.141592653589793)
problems-with-unprotected-submodule
#lang racket (define state? zero?) (define action? odd?) (define strategy/c (-> state? action?)) (provide (contract-out #:unprotected-submodule no-contract (human strategy/c) (ai strategy/c))) (define (general p) pi) (define human (general 'gui)) (define ai (general 'tra))
|
|
While this second way of creating a no-contract submodule eliminates
the run-time dependency on racket/contract, its
compilation—
The third and last way to create a no-contract submodule is
useful when contracts prevents a module from being used in a context where
contracts aren’t available at all—
creating a c/ sub-directory with the file no-contract.rkt,
placing the functionality into no-contract.rkt,
adding (require "c/no-contract.rkt") to c.rkt, and
exporting the functionality from there with contracts.
Once this arrangement is set up, a client module in a special context racket/base or for racket/contract can use (require a/b/c/no-contract). In a regular module, though, it would suffice to write (require a/b/c) and doing so would import contracted identifiers.
7.4 Unsafe: Beware
Racket provides a number of unsafe operations that behave like their related, safe variants but only when given valid inputs. They differ in that they eschew checking for performance reasons and thus behave unpredictably on invalid inputs.
As one example, consider fx+ and unsafe-fx+. When fx+ is applied to a non-fixnum?, it raises an error. In contrast, when unsafe-fx+ is applied to a non-fixnum?, it does not raise an error. Instead it either returns a strange result that may violate invariants of the run-time system and may cause later operations (such as printing out the value) to crash Racket itself.
Do not use unsafe operations in your programs unless you are writing software that builds proofs that the unsafe operations receive only valid inputs (e.g., a type system like Typed Racket) or you are building an abstraction that always inserts the right checks very close to the unsafe operation (e.g., a macro like for). And even in these situations, avoid unsafe operations unless you have done a careful performance analysis to be sure that the performance improvement outweighs the risk of using the unsafe operations.