-
Notifications
You must be signed in to change notification settings - Fork 790
The REPL and Evaluation Environments
This page describes work which is being done on the clojure.browser
branch. Everything is subject to change.
One of the reasons for creating ClojureScript is that JavaScript reaches. There are many interesting environments in which JavaScript can run. Each of these environments has something unique about it. One of the reasons that Clojure rocks is that it has a REPL which gives developers the most dynamic development experience possible. We would like to support this dynamic development experience in every environment where JavaScript runs. To accomplish this, we have created an abstraction around the JavaScript environment and disconnected the REPL from any particular implementation. This gives the REPL the same reach as JavaScript as well as allowing evaluation environment implementations to be used independently for things like automated testing and cross-environment testing.
Most projects will target a specific environment. These changes will allow you to have the full benefit of a REPL in your target environment. Currently there are implementations for two environments: Rhino and the browser. By implementing one protocol, you can easily support additional environments.
The basic usage of the REPL is always the same:
- require
cljs.repl
- require the namespace which implements the desired evaluation environment
- create a new evaluation environment
- start the REPL with the created environment
Using the REPL will also feel the same in each environment; forms are entered, results are printed and side-effects happen where they make the most sense.
(require '[cljs.repl :as repl])
(require '[cljs.repl.rhino :as rhino]) ;; require the rhino implementation of IJavaScriptEnv
(def env (rhino/repl-env)) ;; create a new environment
(repl/repl env) ;; start the REPL
This is very much the same as it was before and will behave the same as the old ClojureScript REPL.
A browser-connected REPL works in much the same way as a normal REPL: forms are read from the console, evaluated and return values are printed. A major and useful difference form normal REPL usage is that all side-effects occur in the browser. You can show alerts, manipulate the dom and interact with a running application.
There is a sample project under samples/repl
which shows how to set up a minimal browser-connected REPL. This example will walk through doing the same thing, step-by-step.
The first step is to create the browser side of the connection. This is done by adding one require and one line of code, as shown below in a file named foo.cljs
.
(ns foo
(:require [clojure.browser.repl :as repl]))
(repl/connect "http://localhost:9000/repl")
The most interesting use case for a browser-connected REPL is to connect it to a project and use the REPL to drive and develop an application while it is running. To set this up you only need to add this same code to any file in the project.
Next, compile the file in either development mode or with simple optimizations. No advanced optimizations please.
./bin/cljsc foo.cljs > foo.js
Create a host html page like the one shown below.
<html>
<head>
<meta charset="UTF-8">
<title>Browser-connected REPL</title>
</head>
<body>
<div id="content">
<script type="text/javascript" src="out/goog/base.js"></script>
<script type="text/javascript" src="foo.js"></script>
<script type="text/javascript">
goog.require('foo');
</script>
</body>
</html>
There is nothing different about this and what one would do for any other browser-based ClojureScript project.
Start the REPL using the pattern described above, but with the browser as the evaluation environment.
(require '[cljs.repl :as repl])
(require '[cljs.repl.browser :as browser]) ;; require the browser implementation of IJavaScriptEnv
(def env (browser/repl-env)) ;; create a new environment
(repl/repl env) ;; start the REPL
Once the REPL has stared you will see the message "Starting server on port 9000". At this point, open the html page to complete the connection. Once the paged is open and the connection is made, the REPL prompt will be dispalyed.
Just in case you can't think of anything interesting to do, here are some ideas.
;; the basics
(+ 1 1)
(:a {:a :b})
(reduce + [1 2 3 4 5])
(defn sum [coll] (reduce + coll))
(sum [2 2 2 2])
;; load a ClojureScript file and use it
(load-file "clojure/string.cljs")
(clojure.string/reverse "ClojureScript")
;; browser specific
(js/alert "I am an evil side-effect")
(load-namespace 'clojure.browser.dom)
(ns dom.test (:require [clojure.browser.dom :as dom]))
(dom/append (dom/get-element "content")
(dom/element "ClojureScript is all up in your DOM."))
;; load and use goog code we haven't used yet
(load-namespace 'goog.crypt)
(ns test.crypt (:require [goog.crypt :as c]))
(c/stringToByteArray "ClojureScript")
(load-namespace 'goog.date.Date)
(goog.date.Date.)
There are currently two options which may be used to configure the browser evaluation environment.
-
:port
set the port to listen on - defaults to 9000 -
:working-dir
set the working directory for compiling REPL related code - defaults to ".repl"
In the example above, two ways are shown to load new code into the environment: load-file
and load-namespace
. load-file
simply loads a single ClojureScript file. load-namespace
loads any file, ClojureScript or JavaScript, with all of its dependencies, which have not already been loaded, in dependency order.
- No additional dependencies
- Should work now in all browsers
- Security is a non-goal, this is for development and testing
To create a new environment, implement the IJavaScriptEval protocol.
(defprotocol IJavaScriptEnv
(-setup [this])
(-evaluate [this line js])
(-load [this ns url])
(-put [this k f])
(-tear-down [this]))
setup
and tear-down
do any work which is required to create and destroy the JavaScript evaluation environment. put
will set values in the JavaScript environment. put
is called to set the file name of the file containing the forms which are currently being evaluated. All three of these functions will have side-effects and will return nil.
evaluate
takes a line number and a JavaScript string and evaluates the string returning a map with the keys :status
and :value
. The value of status may be :success
, :error
or :exception
. :value
will be the return value or an error message. In the case of an exception, there may be a :stacktrace
key containing the stack trace.
The load
function takes a list of namespaces which are provided by a JavaScript file and the URL for the file and will load JavaScript from the given URL into the environment. The implementation is responsible for ensuring that each namespace is loaded once.
To create the browser-connected REPL and meet the goals described above, we use long-polling and Google's CrossPageChannel. Long-polling allows us to treat the browser as the server and CrossPageChannel helps us get around the same-origin policy.
The model for a browser-connected REPL is that the REPL is the client and the browser is the server which evaluates JavaScript code. How do we implement this without resorting to WebSockets? If we think of the connection as a series of messages being passed between the browser and the REPL, and we ignore the first message sent from the browser, then we have what we need. When the browser initially connects, the REPL will hold that connection until is has something to send for evaluation. Once the next form is read and compiled, it will be sent to the browser using that saved connection. The browser will evaluate it and send the result with a new connection. And the cycle repeats...
Browsers enforce a same-origin policy for JavaScript code. This means that the JavaScript which is evaluated in a page can come from only one origin domain. This is a problem for the browser-connected REPL because FireFox and Chome both view loading a file and connecting to localhost:9000 as different domains. It may also be a valid use case to want to connect to an application served from a totally different domain.
Fortunately, Google has also run into this problem and has created something called a CrossPageChannel. Without going into the details, this allows an iframe served from one domain (the REPL) to communicate with the parent page which was served from another domain (the application server). This is accomplished in a way that is supported by all modern browsers.
- Rationale
- Quick Start
- Differences from Clojure
- [Usage of Google Closure](Google Closure)