Last active
April 27, 2021 03:12
-
-
Save jackrusher/6b128001a8034401e76c8a44ca40e4ab to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; # Maria for Experts | |
;; This is a short tour of abstractions we've made in the process of building Maria, which are also available while using the system. It is meant for people with experience using functional programming languages who already know how to evaluate forms in Maria. If you're a beginner to Maria or to programming in general, I recommend starting [here](https://www.maria.cloud/intro). | |
;; (For the impatient, Command-Enter — Control-Enter on a PC — within a code block with evaluate the code before the cursor.) | |
;; ## Notebook interface | |
;; | |
;; In the notebook tradition exemplified by iPython Notebooks, one has a mix of prose and code with the ability to visualize the results of evaluating a particular piece of code. | |
;; | |
;; For example, `jujub-bird` evaluates to a new var containing the value `15`. | |
(def jujub-bird 15) | |
;; While `squared` evaluates to a function that returns the square of the number it receives. | |
(defn squared [x] | |
(* x x)) | |
;; And, finally, we can see the effect of applying the `squared` function to `jujub-bird`. | |
(squared jujub-bird) | |
;; One problem with this approach is that changes made to earlier definitions do not effect future references automatically. So, for example, changing `jujub-bird` will not result in re-computation of `(squared jujub-bird)`. | |
;; This can lead to strange inconsistencies between the visible output at a particular moment and the actual meaning of the code. | |
;; ## Dataflow notebook | |
;; One way to work around this situation is to use a dataflow paradigm, similar to a spreadsheet, to propagate changes through the notebook based on declared dependencies between code blocks. | |
;; We call the mechanism we use for this purpose a "cell", after the concept of a spreadsheet cell. | |
;; In this case, any change to `slithy` will cause `toves` to be re-calculated, which will -- assuming these three blocks have been evaluated once in the past -- result in a circle of the correct size being drawn. | |
(defcell slithy 4) | |
(defcell toves (squared @slithy)) | |
(cell (circle @toves)) | |
;; We use `defcell` for named cells, `cell` for anonymous cells, and the `@` sigil to declare references to previously defined cells. This last convention is borrowed from the syntax for clojure `atom`s, whose semantics our cells largely share. | |
;; Note that we use explicit declaration of cells and dependencies rather than automatically calculating dependencies over all variable references so the programmer has control over the evaluation strategy they prefer in a given situation. | |
;; ## Temporal Recursion (Sorensen, 2005) | |
;; We also provide a mechanism for temporal recursion using `interval`. This is similar to Javascript's `setInterval` with the key difference that each invocation of the function will be passed the result of the previous invocation. This can be viewed as a higher order function that converts the function passed to it into an infinite recursive function that's performed at a fixed interval. | |
;; So, for example, one can create an infinite stream of integers -- i.e. a counter -- that updates every half second by passing `inc` to `interval`: | |
(defcell counter (interval 500 inc)) | |
;; Here we generate an infinite sequence of random color names at a 250ms interval: | |
(defcell a-color | |
(interval 250 #(rand-nth (seq color-names)))) | |
;; This feature is especially useful in conjuction with the dataflow properies of cells. Here we render a circle that automatically updates its color and position as `counter` and `a-color` change: | |
(cell (->> (circle 20) | |
(position (+ 60 (* 20 (Math/sin @counter))) | |
(+ 60 (* 20 (Math/cos @counter)))) | |
(colorize @a-color))) | |
;; ## Temporal recursion with sequences | |
;; Given an infinite sequence of random numbers that update once a second: | |
(defcell random-number | |
(interval 1000 #(rand-int 30))) | |
;; We can use the clojure's built-in sequence functions to maintain a running window of the last ten values from that stream (note that cells provide their most recent computed value at `@self`): | |
(defcell last-ten | |
(take 10 (conj @self @random-number))) | |
;; From which we can then generate a live bar graph using only `map` and our `rectangle` drawing primitive: | |
(cell (map (partial rectangle 12) @last-ten)) | |
;; # DOM and events | |
;; We also have a `hiccup`/`sablono`-style templating system built in. This allows someone with browser programming experience to create arbitrary HTML and SVG output that will be rendered in place: | |
(html [:div {:style {:color "white" | |
:background-color "pink" | |
:font-size 42 | |
:font-weight "bold" | |
:height 100 | |
:width 100 | |
:padding 20}} | |
"Hi!"]) | |
;; When paired a cell to maintain state: | |
(defcell switch false) | |
;; ... simple interactions are simply specified: | |
(cell (listen :click | |
(fn [] (swap! switch not)) | |
(if @switch | |
(colorize "red" (circle 40)) | |
(colorize "black" (circle 40))))) | |
;; Clicking the circle thus drawn will cause the color to toggle. | |
;; ## A practical example | |
;; Using cells one can implement a pattern similar to that seen in libraries like Reagent, where application state is stored in a single cell (similar in intent to a `ratom`). Here we create a set of controls for a visualization: | |
(defcell controls {:x 0 | |
:y 0 | |
:param-1 30 | |
:param-2 30 | |
:scale 30}) | |
;; Changes to `controls` will be carried along to dependent cells, such that these two functions that will be re-generated as needed: | |
(defcell ->x | |
#(+ (:x @controls) (* (:param-1 @controls) (Math/pow (Math/sin %) 3)))) | |
(defcell ->y | |
#(+ (:y @controls) | |
(- (* (:param-2 @controls) (Math/cos %))) | |
(* 5 (Math/cos (* 2 %))) | |
(* 2 (Math/cos (* 3 %))) | |
(Math/cos (* 4 %)))) | |
;; This anonymous cell calculated ~1400 points by plotting the values returned by the above two functions to x/y coordinates, then converting those points into a path drawn in red with a 3px stroke width: | |
(cell | |
(->> (mapcat #(vector (* (:scale @controls) (@->x %)) | |
(* (:scale @controls) (@->y %))) | |
(range 0 2200 3.146)) | |
polygon | |
(stroke "red") | |
(stroke-width 3))) | |
;; Any manual change to `controls` will trigger an immediate re-render of the above plot, but it's often more convenient to be able to change values more fluidly. | |
;; Here we build up a HTML table directly from the `controls` map, with the name of each parameter set beside a slider that updates `controls`. In these few lines of code we are able to create an _ad hoc_ user interface within our notebook and then use that to explore the data: | |
(->> @controls | |
(mapv (fn [[k v]] | |
[:tr | |
[:td {:style {:padding-right 10}} (name k)] | |
[:td [:input {:type "range" :min "1" :max "30" :default-value v | |
:on-change (value-to-cell! controls k)}]]])) | |
(into [:table]) | |
html) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment