490 lines
19 KiB
Racket
490 lines
19 KiB
Racket
#lang scribble/sigplan
|
||
@(require "common.rkt" racket/string racket/sandbox)
|
||
|
||
@; TODO 'need to' etc
|
||
@; TODO clever examples
|
||
@; TODO definition pane for examples
|
||
@; +------------
|
||
@; | f \in \interp
|
||
@; |
|
||
@; | > (f x)
|
||
@; | y
|
||
@; +------------
|
||
@; TODO remove all refs to implementation?
|
||
|
||
@title[#:tag "sec:usage"]{What we can learn from Values}
|
||
|
||
We have defined useful elaborators for a variety of
|
||
common programming tasks ranging from type-safe string formatting to
|
||
constant-folding of arithmetic.
|
||
These elaborators are implemented in Typed Racket@~cite[TypedRacket], a macro-extensible
|
||
typed language that compiles into Racket@~cite[plt-tr1].
|
||
An important component of Typed Racket's design is that all macros in a program
|
||
are fully expanded before type-checking begins.
|
||
This protocol lets us implement our elaborators as macros that expand into
|
||
typed code.
|
||
|
||
Each elaborator is defined as a @emph{local} transformation on syntax.
|
||
Code produced by an elaboration is associated with compile-time data that
|
||
other elaborators may access.
|
||
In this way, we handle binding forms and propagate information through
|
||
@emph{unrelated} program transformations.
|
||
|
||
@parag{Conventions}
|
||
@itemlist[
|
||
@item{
|
||
Interpretation and elaboration functions are defined over symbolic expressions
|
||
and values; specifically, over @emph{syntax objects}.
|
||
To distinguish terms and syntax objects representing
|
||
terms, we quote the latter and typeset it in green.
|
||
For example, @racket[(λ (x) x)] is a term implementing the identity
|
||
function and @racket['(λ (x) x)] is a representation that will evaluate
|
||
to the identity function.
|
||
Values are typeset in @exact|{\RktVal{green}}| because their syntax and
|
||
term representations are identical.
|
||
} @item{
|
||
Syntax objects carry lexical information, but our examples treat them as
|
||
flat symbols.
|
||
} @item{
|
||
We use an infix @tt{::} to write explicit type annotations and casts,
|
||
for instance @racket[(x :: Integer)].
|
||
These normally have two different syntaxes, respectively
|
||
@racket[(ann x Integer)] and @racket[(cast x Integer)].
|
||
}
|
||
]
|
||
|
||
@; =============================================================================
|
||
@section{Format Strings}
|
||
|
||
@; TODO add note about the ridiculous survey figure? Something like 4/5 doctors
|
||
@; TODO note regexp is the first?
|
||
Format strings are the world's second most-loved domain-specific language (DSL).
|
||
@; @~cite[wmpk-algol-1968]
|
||
All strings are valid format strings; additionally, a format string may contain
|
||
@emph{format directives} describing @emph{where} and @emph{how} values can be
|
||
spliced into the format string.
|
||
@; TODO scheme or common lisp?
|
||
Racket follows the Lisp tradition@~cite[s-lisp-1990] of using a tilde character (@tt{~})
|
||
to prefix format directives.
|
||
For example, @racket[~s] converts any value to a string and @racket[~b] converts a
|
||
number to binary form.
|
||
|
||
@exact|{
|
||
\begin{SCodeFlow}\begin{RktBlk}\begin{SingleColumn}\Scribtexttt{{\Stttextmore} }\RktPn{(}\RktSym{printf}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{"binary($\sim$s) = $\sim$b"}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{7}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{7}\RktPn{)}
|
||
|
||
\RktOut{binary(7) = 111}
|
||
|
||
\begin{SingleColumn}\end{SingleColumn}\end{SingleColumn}\end{RktBlk}\end{SCodeFlow}
|
||
}|
|
||
|
||
If the format directives do not match the arguments to @racket[printf], most
|
||
languages fail at run-time.
|
||
This is a simple kind of value error that could be caught statically.
|
||
|
||
@exact|{
|
||
\begin{SCodeFlow}\begin{RktBlk}\begin{SingleColumn}\Scribtexttt{{\Stttextmore} }\RktPn{(}\RktSym{printf}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{"binary($\sim$s) = $\sim$b"}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{"7"}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{"7"}\RktPn{)}
|
||
|
||
\RktErr{printf: format string requires argument of type $<$exact{-}number$>$}
|
||
|
||
\begin{SingleColumn}\end{SingleColumn}\end{SingleColumn}\end{RktBlk}\end{SCodeFlow}
|
||
}|
|
||
|
||
Detecting inconsistencies between a format string and its arguments is straightforward
|
||
if we define an interpretation @racket[fmt->types] @exact|{$\in \interp$}| for
|
||
reading types from a format string value.
|
||
In Typed Racket this function is rather simple because the most common
|
||
directives accept @code{Any} type of value.
|
||
|
||
@exact|{
|
||
\hfill\fbox{\RktMeta{fmt->types} $\in \interp$}
|
||
|
||
\begin{SCodeFlow}\begin{RktBlk}\begin{SingleColumn}\RktSym{{\Stttextmore}}\mbox{\hphantom{\Scribtexttt{x}}}\RktPn{(}\RktSym{fmt{-}{\Stttextmore}types}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{"binary($\sim$s) = $\sim$b"}\RktPn{)}
|
||
|
||
\RktVal{{\textquotesingle}}\RktVal{[}\RktVal{Any}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{Integer}\RktVal{]}
|
||
|
||
\RktSym{{\Stttextmore}}\mbox{\hphantom{\Scribtexttt{x}}}\RktPn{(}\RktSym{fmt{-}{\Stttextmore}types}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{{\textquotesingle}}\RktVal{(}\RktVal{$\lambda$}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{(}\RktVal{x}\RktVal{)}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{x}\RktVal{)}\RktPn{)}
|
||
|
||
\RktVal{\#false}\end{SingleColumn}\end{RktBlk}\end{SCodeFlow}
|
||
}|
|
||
|
||
Now to use @racket[fmt->types] in an elaboration.
|
||
Given a call to @racket[printf], we check the number of arguments and
|
||
add type annotations using the inferred types.
|
||
For all other syntax patterns, @racket[t-printf] is the identity elaboration.
|
||
|
||
@exact|{
|
||
\hfill\fbox{$\elabf \in \interp$}
|
||
|
||
\begin{SCodeFlow}\begin{RktBlk}\begin{SingleColumn}\RktSym{{\Stttextmore}}\mbox{\hphantom{\Scribtexttt{x}}}\RktPn{(}\RktSym{t{-}printf}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{{\textquotesingle}}\RktVal{(}\RktVal{printf}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{"$\sim$a"}\RktVal{)}\RktPn{)}
|
||
|
||
\RktSym{$\perp$}
|
||
|
||
\RktSym{{\Stttextmore}}\mbox{\hphantom{\Scribtexttt{x}}}\RktPn{(}\RktSym{t{-}printf}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{{\textquotesingle}}\RktVal{(}\RktVal{printf}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{"$\sim$b"}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{"2"}\RktVal{)}\RktPn{)}
|
||
|
||
\RktVal{{\textquotesingle}}\RktVal{(}\RktVal{printf}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{"$\sim$b"}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{(}\RktVal{"2"}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{{\hbox{\texttt{::}}}}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{Integer}\RktVal{)}\RktVal{)}
|
||
|
||
\RktSym{{\Stttextmore}}\mbox{\hphantom{\Scribtexttt{x}}}\RktPn{(}\RktSym{t{-}printf}\mbox{\hphantom{\Scribtexttt{x}}}\RktSym{printf}\RktPn{)}
|
||
|
||
\RktVal{{\textquotesingle}}\RktVal{printf}\end{SingleColumn}\end{RktBlk}\end{SCodeFlow}
|
||
}|
|
||
|
||
The first example is rejected immediately as a syntax error.
|
||
The second is a valid elaboration, but will lead to a static type error.
|
||
Put another way, the format string @racket{~b} specializes the type of
|
||
@racket[printf] from @racket[(String Any * -> Void)] to @racket[(String Integer -> Void)].
|
||
The third example demonstrates that higher-order
|
||
uses of @racket[printf] default to the standard, unspecialized behavior.
|
||
|
||
|
||
@; =============================================================================
|
||
@section{Regular Expressions}
|
||
Regular expressions are often used to capture sub-patterns within a string.
|
||
|
||
@racketblock[
|
||
> (regexp-match #rx"(.*)@(.*)" "toni@merchant.net")
|
||
'("toni@merchant.net" "toni" "merchant.net")
|
||
]
|
||
|
||
The first argument to @racket[regexp-match] is a regular expression pattern.
|
||
Inside the pattern, the parentheses delimit sub-pattern @emph{groups}, the dots match
|
||
any single character, and the Kleene star matches zero-or-more repetitions
|
||
of the pattern-or-character preceding it.
|
||
The second argument is a string to match against the pattern.
|
||
If the match succeeds, the result is a list containing the entire matched string
|
||
and substrings corresponding to each group captured by a sub-pattern.
|
||
If the match fails, @racket[regexp-match] returns @racket[#false].
|
||
|
||
@racketblock[
|
||
> (regexp-match #rx"-(2*)-" "111-222-3333")
|
||
'("-222-" "222")
|
||
> (regexp-match #rx"¥(.*)" "$2,000")
|
||
#false
|
||
]
|
||
|
||
Certain groups can also fail to capture even when the overall match succeeds.
|
||
This can happen when a group is followed by a Kleene star.
|
||
|
||
@racketblock[
|
||
> (regexp-match #rx"(a)*(b)" "b")
|
||
'("b" #f "b")
|
||
]
|
||
|
||
Therefore, a catch-all type for @racket[regexp-match] is fairly large:
|
||
@racket[(Regexp String -> (U #f (Listof (U #f String))))].
|
||
Using this general type is cumbersome for simple patterns
|
||
where a match implies that all groups will successfully capture.
|
||
|
||
@exact|{
|
||
\begin{SCodeFlow}\begin{RktBlk}\begin{SingleColumn}\RktSym{{\Stttextmore}}\mbox{\hphantom{\Scribtexttt{x}}}\RktPn{(}\RktSym{define}\mbox{\hphantom{\Scribtexttt{x}}}\RktPn{(}\RktSym{get{-}domain}\mbox{\hphantom{\Scribtexttt{x}}}\RktPn{[}\RktSym{full{-}name}\mbox{\hphantom{\Scribtexttt{x}}}\RktSym{{\hbox{\texttt{:}}}}\mbox{\hphantom{\Scribtexttt{x}}}\RktSym{String}\RktPn{]}\RktPn{)}\mbox{\hphantom{\Scribtexttt{x}}}\RktSym{{\hbox{\texttt{:}}}}\mbox{\hphantom{\Scribtexttt{x}}}\RktSym{String}
|
||
|
||
\mbox{\hphantom{\Scribtexttt{xxxx}}}\RktPn{(}\RktSym{cond}
|
||
|
||
\mbox{\hphantom{\Scribtexttt{xxxxx}}}\RktPn{[}\RktPn{(}\RktSym{regexp{-}match}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{\#rx"({\hbox{\texttt{.}}}*)@({\hbox{\texttt{.}}}*)"}\mbox{\hphantom{\Scribtexttt{x}}}\RktSym{full{-}name}\RktPn{)}
|
||
|
||
\mbox{\hphantom{\Scribtexttt{xxxxxx}}}\RktSym{={\Stttextmore}}\mbox{\hphantom{\Scribtexttt{x}}}\RktSym{third}\RktPn{]}
|
||
|
||
\mbox{\hphantom{\Scribtexttt{xxxxx}}}\RktPn{[}\RktSym{else}\mbox{\hphantom{\Scribtexttt{x}}}\RktVal{"Match Failed"}\RktPn{]}\RktPn{)}\RktPn{)}
|
||
|
||
\RktErr{Error: expected $<$String$>$, got $<$(U \#false String)$>$}
|
||
|
||
\end{SingleColumn}\end{RktBlk}\end{SCodeFlow}
|
||
}|
|
||
|
||
|
||
We alleviate the need for casts and guards in simple patterns
|
||
with a parentheses-counting
|
||
interpretation that parses regular expressions
|
||
and returns the number of groups.
|
||
If there is any doubt whether a group will capture, we default to the general
|
||
@racket[regexp-match] type.
|
||
|
||
@todo{fbox} @racket[rx->groups] @exact|{$\in \interp$}|
|
||
|
||
@racketblock[
|
||
> (rx->groups #rx"(a)(b)(c)")
|
||
3
|
||
> (rx->groups #rx"((a)b)")
|
||
2
|
||
> (rx->groups #rx"(a)*(b)")
|
||
#false
|
||
]
|
||
|
||
The corresponding elaboration
|
||
inserts casts to subtype the result of calls to @racket[regexp-match].
|
||
It also raises syntax errors when an uncompiled regular expression contains
|
||
unmatched parentheses.
|
||
|
||
@todo{fbox}
|
||
|
||
@racketblock[
|
||
> (t-regexp '(regexp-match #rx"(a)b" str))
|
||
'((regexp-match #rx"(a)b" str)
|
||
:: (U #f (List String String)))
|
||
> (t-regexp '(regexp-match "(" str))
|
||
⊥
|
||
]
|
||
|
||
|
||
@; =============================================================================
|
||
@section{Anonymous Functions}
|
||
|
||
By tokenizing symbolic λ-expressions, we can interpret their domain
|
||
statically. @todo{fbox} @racket[fun->domain] @exact|{$\in \interp$}|
|
||
|
||
@racketblock[
|
||
> (fun->domain '(λ (x y z) (x (z y) y)))
|
||
'[Any Any Any]
|
||
> (fun->domain '(λ ([x : Real] [y : Real]) x))
|
||
'[Real Real]
|
||
]
|
||
|
||
When domain information is available at calls to a @racket[curry] function,
|
||
we elaborate to a type-correct, indexed version of @racket[curry].
|
||
Conceptually, we give @racket[curry] the unusable type @racket[(⊥ -> ⊤)] and
|
||
elaboration produces a subtype @racket[curry_i].
|
||
|
||
@todo{fbox} @exact|{$\in \elab$}|
|
||
|
||
@racketblock[
|
||
> ('(curry (λ (x y) x)))
|
||
'(curry_2 (λ (x y) x))
|
||
]
|
||
|
||
@;Our implementation generates each @racket[curry_i] at compile-time by folding
|
||
@; over the interpreted domain.
|
||
This same technique can be used to implement generalized @racket[map] in
|
||
languages without variable-arity polymorphism@~cite[stf-esop-2009].
|
||
|
||
@racketblock[
|
||
> (define (cite (p : String) (d : String))
|
||
(printf "~a v. ~a, U.S.\n" p d))
|
||
> (define plaintiff*
|
||
'("Rasul" "Chisholm"))
|
||
> (define defendant*
|
||
'("Bush" "Georgia"))
|
||
> (map cite plaintiff* defendant*)
|
||
Rasul v. Bush, U.S.
|
||
Chisholm v. Georgia, U.S.
|
||
]
|
||
|
||
Leaving out an argument to @racket[printf] or passing an extra list
|
||
when calling @racket[map] will raise an arity error during elaboration.
|
||
On the other hand, if we modified @racket[cite] to take a third argument
|
||
then the above call to @racket[map] would raise a type error.
|
||
|
||
|
||
@; =============================================================================
|
||
@section{Numeric Constants}
|
||
|
||
The identity interpretation @exact{$\RktMeta{id} \in \interp$}
|
||
lifts values to the elaboration environment.
|
||
When composed with a filter, we can recognize types of compile-time constants.
|
||
|
||
@todo{fbox id-int = int}
|
||
@racketblock[
|
||
> (int? 2)
|
||
2
|
||
> (int? "~s")
|
||
#false
|
||
]
|
||
|
||
Constant-folding versions of arithmetic operators are now easy to define
|
||
in terms of the built-in operations.
|
||
Our implementation re-uses a single fold/elaborate loop to make
|
||
textualist wrappers over @racket[+], @racket[expt] and others.
|
||
|
||
@todo{fbox}
|
||
@racketblock[
|
||
> (define a 3)
|
||
> (define b (/ 1 (- a a)))
|
||
Error: division by zero
|
||
]
|
||
|
||
Partial folds also work as expected.
|
||
|
||
@racketblock[
|
||
> (* 2 3 7 z)
|
||
'(* 42 z)
|
||
]
|
||
|
||
Taken alone, this re-implementation of constant folding in an earlier compiler
|
||
stage is not terribly exciting.
|
||
But our arithmetic elaborators cooperate with other elaborators, for example
|
||
size-aware vector operations.
|
||
|
||
|
||
@; =============================================================================
|
||
@section[#:tag "sec:vector"]{Fixed-Length Structures}
|
||
|
||
Fixed-length data structures are often initialized with a constant or
|
||
computed-constant length.
|
||
Racket's vectors are one such structure.
|
||
For each built-in vector constructor, we thus define an interpretation:
|
||
|
||
@todo{fbox vector->size in interp}
|
||
@racketblock[
|
||
> (vector->size '#(0 1 2))
|
||
3
|
||
> (vector->size '(make-vector 100))
|
||
100
|
||
> (vector->size '(make-vector (/ 12 3)))
|
||
4
|
||
]
|
||
|
||
After interpreting, we associate the size with the new vector at compile-time.
|
||
Other elaborators can use and propagate these sizes; for instance, we have
|
||
implemented elaborating layers for thirteen standard vector operations.
|
||
Together, they constitute a length-aware vector library that serves as a
|
||
drop-in replacement for existing code.
|
||
If size information is missing, the operators default to Typed Racket's behavior.
|
||
|
||
@racketblock[
|
||
> (vector-ref (make-vector 3) 4)
|
||
⊥
|
||
> (vector-length (vector-append '#(A B) '#(C D)))
|
||
4
|
||
> (vector-ref (vector-map add1 '#(3 3 3)) 4)
|
||
(unsafe-ref (vector-map add1 '#(3 3 3)) 4)
|
||
]
|
||
|
||
For the most part, these elaborations simply manage sizes and
|
||
delegate the main work to Typed Racket vector operations.
|
||
We do, however, optimize vector references to unsafe primitives and
|
||
specialize operations like @racket[vector-length], as shown above.
|
||
|
||
|
||
@; =============================================================================
|
||
@section[#:tag "sec:sql"]{Database Schema}
|
||
@; db
|
||
@; TODO Ocaml, Haskell story
|
||
@; TODO connect to ew-haskell-2012 os-icfp-2008
|
||
|
||
Racket's @racket[db] library provides a string-based API for executing SQL
|
||
queries.@note{Technically, we use @tt{sql} as an abbreviation for @tt{postgresql}.
|
||
Although the Racket library supports many database systems, we have only implemented
|
||
a postgres front-end.}
|
||
After connecting to a database, SQL queries embedded in strings can be run
|
||
for effects and row values (represented as Racket vectors).
|
||
Queries may optionally contain @emph{query parameters}---natural numbers
|
||
prefixed by a dollar sign (@tt{$}).
|
||
Arguments substituted for query parameters are guarded against SQL injection.
|
||
|
||
@todo{format the multi-line strings}
|
||
@racketblock[
|
||
> (define C (sql-connect #:user "admin"
|
||
#:database "SCOTUS"))
|
||
> (query-row C
|
||
"SELECT plaintiff FROM rulings WHERE name = '$1' LIMIT 1"
|
||
2001)
|
||
'#("Kyllo")
|
||
]
|
||
|
||
This is a far cry from language-integrated query@~cite[mbb-sigmod-2006] or
|
||
Scala's LMS@~cite[ra-icfp-2015], but the interface
|
||
is relatively safe and very simple to use.
|
||
|
||
Typed Racket programmers may use the @racket[db] library by assigning
|
||
type signatures to functions like @racket[query-row].
|
||
This is somewhat tedious, as the distinct operations for querying one value,
|
||
one row, or a lazy sequence of rows need similar types.
|
||
A proper type signature might express the database library as a functor over the
|
||
database schema, but Typed Racket does not have functors or even existential types.
|
||
Even if it did, the queries-as-strings interface makes it impossible for a standard
|
||
type checker to infer type constraints on query parameters.
|
||
|
||
The situation worsens if the programmer uses multiple database connections.
|
||
One can either alias one query function to multiple identifiers (each with a specific type)
|
||
or weaken type signatures and manually type-cast query results.
|
||
|
||
By associating a database schema with each connection, our elaboration technique
|
||
can provide a uniform solution to these issues.
|
||
We specialize both the input and output of calls to @racket[query-row],
|
||
helping catch bugs as early as possible.
|
||
|
||
Our solution, however, is not entirely annotation-free.
|
||
We need a schema representing the target database; for this, we ask
|
||
the programmer to supply an S-expression of symbols and types
|
||
that passes a @exact{$\RktMeta{schema?} \in \interp$} predicate.
|
||
|
||
@; (@racket[schema?]@exact{$\,\circ\,$}@racket[id]) @exact{$\in \interp$}
|
||
|
||
@racketblock[
|
||
> (define scotus-schema
|
||
'([decisions [(id . Natural)
|
||
(plaintiff . String)
|
||
(defendant . String)
|
||
(year . Natural)]]))
|
||
]
|
||
|
||
The above schema represents a database with at least one table, called @racket[decisions],
|
||
which contains at least 4 rows with the specified names and types.
|
||
In general a schema may specify any number of tables and rows.
|
||
We statically disallow access to unspecified ones in our elaborated query operations.
|
||
|
||
In addition to the @exact{$\RktMeta{schema?}$} predicate, we define one more
|
||
interpretation and two elaborations.
|
||
The first elaboration is for connecting to a database.
|
||
We require a statically-known schema object and elaborate to a normal connection.
|
||
|
||
@exact{$\RktMeta{sql-connect} \in \elab$}
|
||
@racketblock[
|
||
> (sc '(sql-connect #:user "admin"
|
||
#:database "SCOTUS"))
|
||
⊥
|
||
> (sc '(sql-connect scotus-schema
|
||
#:user "admin"
|
||
#:database "SCOTUS"))
|
||
'(sql-connect #:user "admin"
|
||
#:database "SCOTUS")
|
||
]
|
||
|
||
The next interpretation and elaboration are for reading constraints from
|
||
query strings.
|
||
We parse @tt{SELECT} statements and extract
|
||
@itemlist[
|
||
@item{the names of selected columns,}
|
||
@item{the table name, and}
|
||
@item{an association from query parameters to column names.}
|
||
]
|
||
@todo{fbox}
|
||
@racketblock[
|
||
> "SELECT defendant FROM decisions WHERE plaintiff = '$1'"
|
||
'[(defendant) decisions ($1 . plaintiff)]
|
||
> "SELECT * FROM loans"
|
||
'[* decisions ()]
|
||
]
|
||
|
||
The schema, connection, and query constraints now come together in elaborations
|
||
such as @exact{$\RktMeta{query-exec} \in \elab$}.
|
||
There is still a non-trivial amount of work to be done resolving wildcards and
|
||
validating row names before the type-annotated result is produced,
|
||
but all the necessary information is available, statically.
|
||
|
||
@racketblock[
|
||
> (t '(query-row C
|
||
"SELECT plaintiff FROM decisions WHERE year = '$1' LIMIT 1"
|
||
2006))
|
||
'((query-row C
|
||
"SELECT ..."
|
||
(2006 : Natural))
|
||
:: (Vector String))
|
||
> (t '(query-row C
|
||
"SELECT * FROM decisions WHERE plaintiff = '$1' LIMIT 1"
|
||
"United States"))
|
||
'((query-row C
|
||
"SELECT ..."
|
||
("United States" : String))
|
||
:: (Vector Natural String String Natural))
|
||
> (t '(query-row C "SELECT foo FROM decisions"))
|
||
⊥
|
||
]
|
||
|
||
Results produced by @racket[query-row] are vectors with a known length;
|
||
as such they cooperate with our library of vector operations described in
|
||
@Secref{sec:vector}.
|
||
Accessing a constant index into a row vector is statically guaranteed
|
||
to be in bounds.
|
||
|
||
@; casts can fail, especially if you wildcard without saying all the rows
|
||
|