whalesong/scribblings/cs19.scrbl

682 lines
19 KiB
Racket

#lang scribble/manual
@(require planet/scribble
planet/version
planet/resolver
scribble/eval
scribble/bnf
racket/sandbox
racket/port
(only-in racket/contract any/c)
racket/runtime-path
"scribble-helpers.rkt"
"../js-assembler/get-js-vm-implemented-primitives.rkt")
@(require (for-label (this-package-in resource))
(for-label (this-package-in web-world)))
@(define-runtime-path whalesong-path "..")
@title{CS19 instructions for Whalesong}
@author+email["Danny Yoo" "dyoo@hashcollision.org"]
@section{Installation}
Racket 5.1.2 or greater is a prerequisite for Whalesong. Brown CS
maintains its own installation of Racket 5.1.3 in
@filepath{/local/projects/racket/releases/5.1.3}.
To access it, you can add the following to your @filepath{.environment}:
@filebox[".environment"]{
@verbatim|{
pathprependifdir PATH "/local/projects/racket/releases/5.1.3/bin"
}|}
Hopefully, this should already be configured to be the default for the @tt{cs19} group
by the time you read this.
We'll install a local development copy of Whalesong in a @filepath{whalesong} subdirectory.
On the very first time we install Whalesong:
@verbatim|{
$ git clone git://github.com/dyoo/whalesong.git
$ cd whalesong
$ make
}|
The @filepath{make} step make take a minute or two, and creates a command-line program called
@filepath{whalesong} that we'll use to build Whalesong programs.
Whenever we need to update whalesong, we should do the following
@verbatim|{
$ git pull
$ make
}|
@subsection{Warning on Firefox}
Firefox unfortunately has a
@link["https://bugzilla.mozilla.org/show_bug.cgi?id=676343"]{JavaScript
bug} that prevents it from reliably evaluating Whalesong programs. As
of this writing, I have not been able to find a workaround. You
should probably use Google Chrome instead when testing your programs;
Google Chrome should be in @filepath{/contrib/bin/google-chrome}.
@section{Usage}
The @filepath{whalesong} launcher in the subdirectory will compile
programs to standalone @filepath{.xhtml} files.
Example usage: using @litchar{whalesong build} to compile a whalesong program.
@verbatim|{
fermi ~/whalesong $ cd examples
fermi ~/whalesong/examples $ cat hello.rkt
#lang planet dyoo/whalesong
(display "hello world")
(newline)
fermi ~/whalesong/examples $ ../whalesong build hello.rkt
fermi ~/whalesong/examples $ google-chrome hello.xhtml
Created new window in existing browser session.
fermi ~/whalesong/examples $
}|
@section{Examples}
There are examples in the
@link["https://github.com/dyoo/whalesong/tree/master/examples"]{@filepath{whalesong/examples}}
and
@link["https://github.com/dyoo/whalesong/tree/master/web-world/examples"]{@filepath{whalesong/web-world/examples}}.
Let's look at a few of them.
@subsection{Hello world}
Let's try making a simple, standalone executable. At the moment, the
program must be written in the base language of @racket[(planet
dyoo/whalesong)]. This restriction unfortunately prevents arbitrary
@racketmodname[racket/base] programs from compiling at the moment;
the developers (namely, dyoo) will be working to remove this
restriction as quickly as possible.
Write a @filepath{hello.rkt} with the following content
@filebox["hello.rkt"]{
@codeblock{
#lang planet dyoo/whalesong
(display "hello world")
(newline)
}}
This program is a regular Racket program, and can be executed normally,
@verbatim|{
$ racket hello.rkt
hello world
$
}|
However, it can also be packaged with @filepath{whalesong}.
@verbatim|{
$ whalesong build hello.rkt
$ ls -l hello.xhtml
-rw-rw-r-- 1 dyoo nogroup 692213 Jun 7 18:00 hello.xhtml
}|
Running @tt{whalesong build} on a Racket program will produce a self-contained
@filepath{.xhtml} file. If we open this file in our favorite web browser,
we should see a triumphant message show on screen.
@subsection{Tick tock}
Let's do something a little more interesting, and create a ticker that
counts on the screen.
The first thing we can do is mock up a web page with a user interface, like this.
@filebox["index.html"]{
@verbatim|{
<html>
<head><title>My simple program</title></head>
<body>
<p>The current counter is: <span id="counter">fill-me-in</span></p>
</body>
</html>
}|
}
We can even look at this in a standard web browser.
Once we're happy with the statics of our program, we can inject dynamic behavior.
Write a file called @filepath{tick-tock.rkt} with the following content.
@filebox["tick-tock.rkt"]{
@codeblock|{
#lang planet dyoo/whalesong
(require (planet dyoo/whalesong/web-world)
(planet dyoo/whalesong/resource))
(define-resource index.html)
;; draw: world view -> view
(define (draw world dom)
(update-view-text (view-focus dom "counter") world))
;; tick: world view -> world
(define (tick world dom)
(add1 world))
;; stop?: world view -> boolean
(define (stop? world dom)
(> world 10))
(big-bang 0
(initial-view index.html)
(to-draw draw)
(on-tick tick 1)
(stop-when stop?))
}|
}
Several things are happening here.
@itemize[
@item{We @racket[require] a few libraries to get us some additional
behavior; in particular, @racketmodname/this-package[web-world] to let
us write event-driven web-based programs, and @racketmodname/this-package[resource]
to give us access to external @tech{resource}s.}
@item{We use @racket[define-resource] to refer to external files, like @filepath{index.html} that
we'd like to include in our program.}
@item{We use @racket[big-bang] to start up a computation that
responses to events. In this example, that's clock ticks introduced
by @racket[on-tick], though because we're on the web, we can
bind to many other kinds of web events (by using @racket[view-bind]).}
]
The rest of this document describes the API.
@section{API}
@defmodule/this-package[web-world]
For the purposes of tour-guide, we'll be focusing on the
@racketmodname/this-package[web-world] library in Whalesong.
Like the big-bang in regular world, the callbacks are world-to-world
functions. One difference introduced by the web is the web page
itself: because the page itself is a source of state, it too will be
passed to callbacks. This library presents a
functional version of the DOM in the form of a @tech{view}.
The world-updating callbacks may optionally take an @tech{event} object, which
provides additional information about the event that triggered the callback.
@defproc[(big-bang [w world]
[h big-bang-handler] ...) world]{
Start a big bang computation.
}
@defproc[(initial-view [x any]) big-bang-handler]{
Provide an initial view for the big-bang. Normally, @racket[x] will be a @tech{resource}
to a web page.
@codeblock|{
...
(define-resource page1.html)
...
(big-bang ...
(initial-view page1.html))
}|
}
@defproc[(stop-when [stop? ([w world] [dom view] -> boolean)]) big-bang-handler]{
Tells @racket[big-bang] when to stop.
@codeblock|{
...
(define-struct world (given expected))
...
;; stop?: world view -> boolean
(define (stop? world dom)
(string=? (world-given world) (world-expected world)))
(big-bang ...
(stop-when stop?))
}|
}
@defproc*[(((on-tick [tick-f ([w world] [v view] [e event]? -> world)] [delay real]) big-bang-handler)
((on-tick [tick-f ([w world] [v view] [e event]? -> world)]) big-bang-handler))]{
Tells @racket[big-bang] to update the world during clock ticks.
By default, this will send a clock tick 28 times a second, but if
given @racket[delay], it will use that instead.
@codeblock|{
...
;; tick: world dom -> world
(define (tick world view)
(add1 world))
(big-bang ...
(on-tick tick 5)) ;; tick every five seconds
}|
}
@defproc[(on-mock-location-change [location-f ([w world] [v view] [e event]? -> world)]) big-bang-handler]{
Tells @racket[big-bang] to update the world during simulated movement.
During the extent of a big-bang, a form widget will appear in the
@tt{document.body} to allow us to manually send location-changing
events.
The optional @tech{event} argument will contain numbers for
@racket["latitude"] and @racket["longitude"].
@codeblock|{
...
;; move: world view event -> world
(define (move world dom event)
(list (event-ref event "latitude")
(event-ref event "longitude")))
...
(big-bang ...
(on-mock-location-change move))
}|
}
@defproc[(on-location-change [location-f ([w world] [v view] [e event]? -> world)]) big-bang-handler]{
Tells @racket[big-bang] to update when the location changes, as
received by the
@link["http://dev.w3.org/geo/api/spec-source.html"]{Geolocation API}.
The optional @tech{event} argument will contain numbers for
@racket["latitude"] and @racket["longitude"].
@codeblock|{
...
;; move: world view event -> world
(define (move world dom event)
(list (event-ref event "latitude")
(event-ref event "longitude")))
...
(big-bang ...
(on-location-change move))
}|
}
@defproc[(to-draw [draw-f ([w world] [v view] -> view)]) big-bang-handler]{
Tells @racket[big-bang] how to update the rendering of the world. The draw
function will be called every time an event occurs.
@codeblock|{
...
(define-struct world (name age))
;; draw: world view -> view
(define (draw world dom)
(update-view-text (view-focus dom "name-span")
(world-name world)))
...
(big-bang ...
(to-draw draw))
}|
}
@subsection{Views}
A @deftech{view} is a functional representation of the browser DOM
tree. A view is always focused on an element, and the functions in
this subsection show how to traverse and manipulate the view.
@defproc[(->view [x any]) view]{
Coerse a value into a view whose focus is on the topmost element.
Common values for @racket[x] include @tech{resource}s.
}
@defproc[(view-focus? [v view] [id String]) boolean]{
Return true if the view can be focused using the given id.
}
@defproc[(view-focus [v view] [id String]) view]{
Focuses the view on an element, given the @racket[id]. The view
will be searched starting from the toplevelmost node.
}
Once we have a view, we can refocus the view to different elements by using
@racket[view-left], @racket[view-right], @racket[view-up], and @racket[view-down].
@defproc[(view-left? [v view]) boolean]{
See if the view can be moved to the previous sibling.
}
@defproc[(view-left [v view]) view]{
Move the focus to the previous sibling.
}
@defproc[(view-right? [v view]) boolean]{
See if the view can be moved to the next sibling.
}
@defproc[(view-right [v view]) view]{
Move the focus to the next sibling.}
@defproc[(view-up? [v view]) boolean]{
See if the view can be moved to the parent.
}
@defproc[(view-up [v view]) view]{
Move the focus to the parent.}
@defproc[(view-down? [v view]) boolean]{
See if the view can be moved to the first child.
}
@defproc[(view-down [v view]) view]{
Move the view to the first child.}
Once we focus the view on an element, we can bind a world handler to it that
responds to events.
@defproc[(view-bind [v view] [type string] [world-updater ([w world] [v view] [e event]? -> world)]) view]{
Attach a world-updating event to the focus. When the world-updater is
called, the view will be focused on the element that triggered the
event. Common event types include @racket["click"],
@racket["mouseenter"], @racket["change"].}
When the view is on an element that we'd like to query or update, we can use
several functions:
@defproc[(view-text [v view]) string]{
Get the textual content at the focus.
}
@defproc[(update-view-text [v view] [s string]) view]{
Update the textual content at the focus.}
@defproc[(view-show [v view]) view]{
Show the element at the focus.
}
@defproc[(view-hide [v view]) view]{
Hide the element at the focus.
}
@defproc[(view-attr [v view] [name String]) view]{
Get the attribute @racket[name] at the focus.
}
@defproc[(update-view-attr [v view] [name String] [value String]) view]{
Update the attribute @racket[n] with the value @racket[v] at the focus.
}
@defproc[(view-css [v view] [name String]) view]{
Get the css value @racket[name] at the focus.
}
@defproc[(update-view-css [v view] [name String] [value String]) view]{
Update the css value @racket[n] with the value @racket[v] at the focus.
}
@defproc[(view-id [v view]) world]{
Get the unique identifier of the node at the focus.
}
@defproc[(view-form-value [v view]) view]{
Get the form value of the node at the focus.}
@defproc[(update-view-form-value [v view] [value String]) view]{
Update the form value of the node at the focus.}
@defproc[(view-append-child [d dom]) view]{
Add the dom node @racket[d] as the last child of the focused node.
Focus moves to the appended child.
Dom nodes can be created by using @racket[xexp->dom], which converts a
@tech{xexp} to a node.
}
@defproc[(view-remove [v view]) view]{
Remove the dom node at the focus from the view @racket[v]. Focus tries to move
to the right, if there's a next sibling. If that fails, focus then
moves to the left, if there's a previous sibling. If that fails too,
then focus moves to the parent.}
@subsection{Events}
An @deftech{event} is a structure that holds name-value pairs.
Whenever an event occurs in web-world, it may include some auxiliary
information about the event. As a concrete example, location events
from @racket[on-location-change] and @racket[on-mock-location-change]
can send latitude and longitude values, as long as the world callback
can accept the event as an argument.
@defstruct[event ([kvpairs (listof (list symbol (or/c string number)))])]{}
@defproc[(event-ref [evt event?] [name (or/c symbol string)]) value]{
Get an value from the event, given its @racket[name].
}
@defproc[(event-keys [evt event?]) (listof symbol)]{
Get an list of the event's keys.
}
@section{Dynamic DOM generation with xexps}
@declare-exporting/this-package[web-world]
We often need to dynamically inject new dom nodes into an existing
view. As an example where the UI is entirely in code:
@codeblock|{
#lang planet dyoo/whalesong
(require (planet dyoo/whalesong/web-world))
;; tick: world view -> world
(define (tick world view)
(add1 world))
;; draw: world view -> view
(define (draw world view)
(view-append-child view
(xexp->dom `(p "hello, can you see this? "
,(number->string world)))))
(big-bang 0 (initial-view
(xexp->dom '(html (head) (body))))
(on-tick tick 1)
(to-draw draw))
}|
Normally, we'll want to do as much of the statics as possible with
@filepath{.html} resources, but when nothing else will do, we can
generate DOM nodes programmatically.
We can create new DOMs from an @tech{xexp}, which is a s-expression
representation for a DOM node. Here are examples of expressions that
evaluate to xexps:
@racketblock["hello world"]
@racketblock['(p "hello, this" "is an item")]
@racketblock[
(local [(define name "josh")]
`(p "hello" (i ,name)))]
@racketblock[
'(div (\@ (id "my-div-0"))
(span "This is a span in a div"))]
@racketblock[
`(div (\@ ,(fresh-id))
(span "This is another span in a div whose id is dynamically generated"))]
More formally, a @deftech{xexp} is:
@(let ([open @litchar{(}]
[close @litchar{)}]
[at @litchar[(symbol->string '\@)]])
@BNF[(list @nonterm{xexp}
@nonterm{string}
@BNF-seq[open @nonterm{id} @kleenestar[@nonterm{xexp}] close]
@BNF-seq[open @nonterm{id} open at @kleenestar[@nonterm{key-value}] close @kleenestar[@nonterm{xexp}] close])
(list @nonterm{key-value}
@BNF-seq[open @nonterm{symbol} @nonterm{string} close])
])
To check to see if something is a xexp, use @racket[xexp?]:
@defproc[(xexp? [x any]) boolean]{
Return true if @racket[x] is a xexp.
}
@defproc[(xexp->dom [an-xexp xexp]) dom]{
Return a dom from the xexp.
}
When creating xexps, we may need to create unique ids for the nodes.
The web-world library provides a @racket[fresh-id] form to create these.
@defproc[(fresh-id) string]{
Return a string that can be used as a DOM node id.
}
@section{Including external resources}
@defmodule/this-package[resource]
Programs may need to use an external file @deftech{resource} that isn't
itself a Racket program, but instead some other kind of data.
Graphical programs will often use @filepath{.png}s, and web-related
programs @filepath{.html}s, for example. Whalesong provides the
@racketmodname/this-package[resource] library to refer and use these
external resources. When Whalesong compiles a program into a package,
these resources will be bundled alongside the JavaScript-compiled
output.
@defform[(define-resource id [path-string])]{
Defines a resource with the given path name.
For example,
@codeblock|{
#lang planet dyoo/whalesong
(require (planet dyoo/whalesong/resource))
(define-resource my-whale-image-resource "humpback.png")
}|
}
As a convenience, we can also write
@codeblock|{
#lang planet dyoo/whalesong
(require (planet dyoo/whalesong/resource))
(define-resource humpback.png)
}|
which defines a variable named @racket[humpback.png] whose
resource is @filepath{humpback.png}.
@defproc[(resource->url [a-resource resource?]) string?]{
Given a resource, gets a URL.
For example,
@codeblock|{
#lang planet dyoo/whalesong
(require (planet dyoo/whalesong/resource)
(planet dyoo/whalesong/image))
(define-resource my-whale-image-resource "humpback.png")
(define WHALE-IMAGE
(bitmap/url (resource->url my-whale-image-resource)))
}|
}
@section{Tips and Tricks}
@subsection{Hiding standard output or directing it to an element}
@declare-exporting/this-package[web-world]
For a web-world program, output is normally done by using
@racket[to-draw]. However, side effecting functions, such as
@racket[printf] or @racket[display], are still available, and will
append to @tt{document.body}.
We may want to disable such printing or redirect it to a particular
element on the page. For such purposes, use a combination of
@racket[current-output-port] and @racket[open-output-element] to
redirect the output of these side effect functions to somewhere else.
For example:
@codeblock|{
...
;; Redirect standard output to a div called "stdout-div".
(current-output-port (open-output-element "stdout-div"))
...
(big-bang ...
(on-tick (lambda (world dom)
(begin
(printf "Tick!\n")
(add1 world))))
...)
}|
All subsequent I/O side effects after the call to
@racket[current-output-port] will be written out to the
@tt{stdout-div}, which can be easily styled with @tt{display: none} to
hide it from normal browser display.
@defproc[(open-output-element [id string]) output-port]{
Opens an output port that will be directed to write to the DOM element
whose id is @racket[id]. Note: writing to this port shouldn't fail,
even if the id does not currently exist on the page.
}