Skip to content

The REPL and Evaluation Environments

brentonashworth edited this page Sep 1, 2011 · 19 revisions

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.

Using the REPL

The basic usage of the REPL is always the same:

  1. require cljs.repl
  2. require the namespace which implements the desired evaluation environment
  3. create a new evaluation environment
  4. 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.

Using the Rhino Environment

(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.

Using the browser as an Evaluation Environment

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.)

Browser-connected REPL Options

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"

Loading code

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.

Implementation

Goals

  • No additional dependencies
  • Should work now in all browsers
  • Security is a non-goal, this is for development and testing

The IJavaScriptEnv Protocol

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.

Browser as Evaluation Environment

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.

Clone this wiki locally