Clojure REPLs

Closing the Loop

by Arne Brasseur

workbench with tools

Workbench 工作台

Wikipedia:

A WORKBENCH is a sturdy table at which manual work is done. They range from simple to complex designs that may be considered tools in themselves. Their size and design varies depending on the type of work that is being done.

workbench (from Wikimedia)

Workbench 工作台

workbench (from Wikimedia)

Programmer’s Workbench?

Programmer’s Workbench?

Editor!

Workbench / Editor
should provide access to tools

As a Clojure programmer, what’s the most important tool in your toolbox?

Workbench / Editor
should provide access to tools

As a Clojure programmer, what’s the most important tool in your toolbox?

REPL

Because it’s a meta-tool.
It’s a tool that provides access to tools.
It’s a tool that lets you build your own tools.

REPL

(loop []
  (-> (read *in*)
      (eval)
      (println))
  (recur))

Read

String -> Data

(read *in*)
;;=> [1 2 3]

(read-str "[1 2 3]")
;;=> [1 2 3]

Eval

Data -> Data

(eval '(+ 1 2))
;;=> 3

Print

Data -> String

(prn [1 2 3])
;; *out*| [1 2 3]

(prn-str [1 2 3])
;;=> "[1 2 3]"

Read & Print

(-> "[1 2 3]"
    read-string
    prn-str
    read-string
    prn-str
    (= "[1 2 3]"))
;;=> true

Read & Print

user=> (def identity (comp read-string prn-str))

Read & Print

user=> (def identity (comp read-string prn-str))
#'user/identity

Read & Print

user=> (def identity (comp read-string prn-str))
#'user/identity
user=> (identity 1)

Read & Print

user=> (def identity (comp read-string prn-str))
#'user/identity
user=> (identity 1)
1

Read & Print

user=> (def identity (comp read-string prn-str))
#'user/identity
user=> (identity 1)
1
user=> (identity [1 2 3])

Read & Print

user=> (def identity (comp read-string prn-str))
#'user/identity
user=> (identity 1)
1
user=> (identity [1 2 3])
[1 2 3]

Read & Print

#'user/identity
user=> (identity 1)
1
user=> (identity [1 2 3])
[1 2 3]
user=> (identity {:meetup :Taipei})

Read & Print

user=> (identity 1)
1
user=> (identity [1 2 3])
[1 2 3]
user=> (identity {:meetup :Taipei})
{:meetup :Taipei}

REPL Uses

Explore 探索 Observe 觀察

Control 控制 Change 改變

Explore 探索

user=> (require 'clj-slack.users)

Explore 探索

user=> (require 'clj-slack.users)
nil

Explore 探索

user=> (require 'clj-slack.users)
nil
user=> (def conn {:api-url "https://slack.com/api"})

Explore 探索

user=> (require 'clj-slack.users)
nil
user=> (def conn {:api-url "https://slack.com/api"})
#'user/conn

Explore 探索

user=> (require 'clj-slack.users)
nil
user=> (def conn {:api-url "https://slack.com/api"})
#'user/conn
user=> (def res (clj-slack.users/list conn))

Explore 探索

user=> (require 'clj-slack.users)
nil
user=> (def conn {:api-url "https://slack.com/api"})
#'user/conn
user=> (def res (clj-slack.users/list conn))
#'user/res

Explore 探索

nil
user=> (def conn {:api-url "https://slack.com/api"})
#'user/conn
user=> (def res (clj-slack.users/list conn))
#'user/res
user=> (type res)

Explore 探索

user=> (def conn {:api-url "https://slack.com/api"})
#'user/conn
user=> (def res (clj-slack.users/list conn))
#'user/res
user=> (type res)
clojure.lang.PersistentList

Explore 探索

#'user/conn
user=> (def res (clj-slack.users/list conn))
#'user/res
user=> (type res)
clojure.lang.PersistentList
user=> (length res)

Explore 探索

user=> (def res (clj-slack.users/list conn))
#'user/res
user=> (type res)
clojure.lang.PersistentList
user=> (length res)
12532

Explore 探索

#'user/res
user=> (type res)
clojure.lang.PersistentList
user=> (length res)
12532
user=> (type (first res))

Explore 探索

user=> (type res)
clojure.lang.PersistentList
user=> (length res)
12532
user=> (type (first res))
clojure.lang.PersistentArrayMap

Explore 探索

clojure.lang.PersistentList
user=> (length res)
12532
user=> (type (first res))
clojure.lang.PersistentArrayMap
user=> (keys (first res))

Explore 探索

user=> (length res)
12532
user=> (type (first res))
clojure.lang.PersistentArrayMap
user=> (keys (first res))
(:id :name :real_name :is_admin :is_owner :profile)

Observe 觀察

user=> (keys reloaded.repl/system)

Observe 觀察

user=> (keys reloaded.repl/system)
(:http :datomic :handler :mail-queue)

Observe 觀察

user=> (keys reloaded.repl/system)
(:http :datomic :handler :mail-queue)
user=> (.totalMemory (java.lang.Runtime/getRuntime))

Observe 觀察

user=> (keys reloaded.repl/system)
(:http :datomic :handler :mail-queue)
user=> (.totalMemory (java.lang.Runtime/getRuntime))
315621376

Observe 觀察

user=> (keys reloaded.repl/system)
(:http :datomic :handler :mail-queue)
user=> (.totalMemory (java.lang.Runtime/getRuntime))
315621376
user=> (:roles (user-by-login (db) "plexus"))

Observe 觀察

user=> (keys reloaded.repl/system)
(:http :datomic :handler :mail-queue)
user=> (.totalMemory (java.lang.Runtime/getRuntime))
315621376
user=> (:roles (user-by-login (db) "plexus"))
(:admin :member)

Control 控制

user=> (go)

Control 控制

user=> (go)
Starting system...

Control 控制

user=> (go)
Starting system...
user=> (reset)

Control 控制

user=> (go)
Starting system...
user=> (reset)
Suspending...

Control 控制

user=> (go)
Starting system...
user=> (reset)
Suspending...
Reloading files...

Control 控制

user=> (go)
Starting system...
user=> (reset)
Suspending...
Reloading files...
Restarting...

Control 控制

Starting system...
user=> (reset)
Suspending...
Reloading files...
Restarting...
user=> (add-workers! 5)

Control 控制

user=> (reset)
Suspending...
Reloading files...
Restarting...
user=> (add-workers! 5)
nil

Control 控制

Suspending...
Reloading files...
Restarting...
user=> (add-workers! 5)
nil
user=> (enable-maintenance-mode!)

Control 控制

Reloading files...
Restarting...
user=> (add-workers! 5)
nil
user=> (enable-maintenance-mode!)
:enabled

Change 改變

user=> (alter-var-root! application

Change 改變

user=> (alter-var-root! application
                        assoc-in [:api :token] "new-token")

Change 改變

user=> (alter-var-root! application
                        assoc-in [:api :token] "new-token")
#'application

Change 改變

user=> (alter-var-root! application
                        assoc-in [:api :token] "new-token")
#'application
user=> (defn previously-buggy-function [& args]

Change 改變

user=> (alter-var-root! application
                        assoc-in [:api :token] "new-token")
#'application
user=> (defn previously-buggy-function [& args]
          new-definition-that-fixes-the-bug)

Change 改變

user=> (alter-var-root! application
                        assoc-in [:api :token] "new-token")
#'application
user=> (defn previously-buggy-function [& args]
          new-definition-that-fixes-the-bug)
#'previously-buggy-function

REPL

Explore 探索 Observe 觀察

Control 控制 Change 改變

All of these combined are used to create (production code, tests, data)

REPL & Editor

Your “workbench” should provide handy access to your “tools”.

Don’t walk to the other side of the room each time you need a screwdriver.

Get yourself a Connected REPL.

Connected REPL

Basic operation: eval form in source file

*in* how do you select a form?

*out* where does the output go?

Closing the Loop!

By inserting the REPL result back into the buffer, it can be used again as data or code.

We have come full circle, the printed form can be read again.

Connected REPL

Practical Tips

§

Create a user.clj *

REPL helpers that should always be available.

lein repl automatically loads this file.

Remember: a workbench needs to be convenient, comfortable, easy to use.

* may also be called something else

user.clj ideas

user.clj Reloaded Workflow

Combination of code reloading (clojure.tools.namespace), and some lifecycle manager (Component, Integrant, Mount)

Provides a way to get to a clean slate

Why: because you don’t want to have to restart your REPL process!

(start) (stop) (reset)

user.clj Pomegranate / Alembic

Hot load dependencies

Good for quickly trying out dependencies, or for adding new libraries to the project.

Again: prevent having to restart the process

(add-dependency '[some-jar "0.1.0"])
(load-project)

user.clj Data helpers

Create helpers for things that play a prominent role in your app

(last-request)
(conn)
(db)

Data helpers

Store request/response history in memory, so you can have

user=> (request)

Data helpers

Store request/response history in memory, so you can have

user=> (request)
{:uri "/" :request-method :get ,,,}

Data helpers

Store request/response history in memory, so you can have

user=> (request)
{:uri "/" :request-method :get ,,,}
user=> (response)

Data helpers

Store request/response history in memory, so you can have

user=> (request)
{:uri "/" :request-method :get ,,,}
user=> (response)
{:status 200 :body ,,,}

Data helpers

Store request/response history in memory, so you can have

user=> (request)
{:uri "/" :request-method :get ,,,}
user=> (response)
{:status 200 :body ,,,}
user=> (session-id)

Data helpers

Store request/response history in memory, so you can have

user=> (request)
{:uri "/" :request-method :get ,,,}
user=> (response)
{:status 200 :body ,,,}
user=> (session-id)
"abcxyz"

Data helpers

Store request/response history in memory, so you can have

{:uri "/" :request-method :get ,,,}
user=> (response)
{:status 200 :body ,,,}
user=> (session-id)
"abcxyz"
user=> (session)

Data helpers

Store request/response history in memory, so you can have

user=> (response)
{:status 200 :body ,,,}
user=> (session-id)
"abcxyz"
user=> (session)
{,,,}

Local state

When debugging you might want to evaluate forms inside a function, but local variables will be a problem.

scope-capture

user=> (require 'sc.api)

scope-capture

user=> (require 'sc.api)
nil

scope-capture

user=> (require 'sc.api)
nil
user=> (defn sum [a b] (sc.api/spy (+ a b)))

scope-capture

user=> (require 'sc.api)
nil
user=> (defn sum [a b] (sc.api/spy (+ a b)))
SPY <-1> :1

scope-capture

user=> (require 'sc.api)
nil
user=> (defn sum [a b] (sc.api/spy (+ a b)))
SPY <-1> :1
 At Code Site -1, will save scope with locals [a b]

scope-capture

user=> (require 'sc.api)
nil
user=> (defn sum [a b] (sc.api/spy (+ a b)))
SPY <-1> :1
 At Code Site -1, will save scope with locals [a b]
#'user/sum

scope-capture

nil
user=> (defn sum [a b] (sc.api/spy (+ a b)))
SPY <-1> :1
 At Code Site -1, will save scope with locals [a b]
#'user/sum
user=> (sum 10 20)

scope-capture

user=> (defn sum [a b] (sc.api/spy (+ a b)))
SPY <-1> :1
 At Code Site -1, will save scope with locals [a b]
#'user/sum
user=> (sum 10 20)
SPY [1 -1] :1

scope-capture

SPY <-1> :1
 At Code Site -1, will save scope with locals [a b]
#'user/sum
user=> (sum 10 20)
SPY [1 -1] :1
  At Execution Point 1, Code Site -1, saved locals [a b]

scope-capture

 At Code Site -1, will save scope with locals [a b]
#'user/sum
user=> (sum 10 20)
SPY [1 -1] :1
  At Execution Point 1, Code Site -1, saved locals [a b]
SPY [1 -1] :1

scope-capture

#'user/sum
user=> (sum 10 20)
SPY [1 -1] :1
  At Execution Point 1, Code Site -1, saved locals [a b]
SPY [1 -1] :1
(+ a b) => 30

scope-capture

user=> (sum 10 20)
SPY [1 -1] :1
  At Execution Point 1, Code Site -1, saved locals [a b]
SPY [1 -1] :1
(+ a b) => 30
30

scope-capture

SPY [1 -1] :1
  At Execution Point 1, Code Site -1, saved locals [a b]
SPY [1 -1] :1
(+ a b) => 30
30
user=> (sc.api/letsc 1 (+ a b))

scope-capture

  At Execution Point 1, Code Site -1, saved locals [a b]
SPY [1 -1] :1
(+ a b) => 30
30
user=> (sc.api/letsc 1 (+ a b))
30

scope-capture

SPY [1 -1] :1
(+ a b) => 30
30
user=> (sc.api/letsc 1 (+ a b))
30
user=> (sum 99 42)

scope-capture

(+ a b) => 30
30
user=> (sc.api/letsc 1 (+ a b))
30
user=> (sum 99 42)
SPY [2 -1] :1

scope-capture

30
user=> (sc.api/letsc 1 (+ a b))
30
user=> (sum 99 42)
SPY [2 -1] :1
  At Execution Point 2, Code Site -1, saved locals [a b]

scope-capture

user=> (sc.api/letsc 1 (+ a b))
30
user=> (sum 99 42)
SPY [2 -1] :1
  At Execution Point 2, Code Site -1, saved locals [a b]
SPY [2 -1] :1

scope-capture

30
user=> (sum 99 42)
SPY [2 -1] :1
  At Execution Point 2, Code Site -1, saved locals [a b]
SPY [2 -1] :1
(+ a b) => 141

scope-capture

user=> (sum 99 42)
SPY [2 -1] :1
  At Execution Point 2, Code Site -1, saved locals [a b]
SPY [2 -1] :1
(+ a b) => 141
141

scope-capture

SPY [2 -1] :1
  At Execution Point 2, Code Site -1, saved locals [a b]
SPY [2 -1] :1
(+ a b) => 141
141
user=> (sc.api/letsc 2 a)

scope-capture

  At Execution Point 2, Code Site -1, saved locals [a b]
SPY [2 -1] :1
(+ a b) => 141
141
user=> (sc.api/letsc 2 a)
99

REPL files

Create a repl/ or scratch/ directory, with per-topic “repl files”

REPL/scratch files

Encourages experimentation (go ahead, make a mess)
Explicitly keeps history
Shows thought process
Demonstrates usage
Provides a home for useful snippets

REPL & Tests

Using a REPL does not absolve you of writing tests

Using a REPL is in many ways like writing tests, call a function with various inputs

→ Make a habit of using eval-and-insert, then later copy to test namespace

REPL/scratch file can also be a good source for test inspiration

The End
有問題嗎?
討論一下!

lambdaisland.com/coupon/CLJTPE2018

33% off - forever