Structuring a script for REPL development

Hello! I just started a week ago playing with babashka to rewrite some of mi CI scripts.

I wanted to ask about how should I structure my code, so I can easily build the script progresively by using the repl.

Basically, my scripts consist of a main function with various functions that apply to the result of the step before. My intial idea then was something like:

(defn -main [argument]
  (let [result1 (fn1 argument)
        result2 (fn2 result1)
        result3 (fn3 result2)]
    result3))

With this, I can bind argument manually in the repl, and can apply (fn1 argument), but successive steps become cumbersome (I would need to keep def’ing the result of the previous step)

clj> (def argument "foo")
clj> ;; evaluate fn1
clj> (fn1 argument)
clj> ;; re-evaluate fn1 with changes
clj> (fn1 argument)
;; etc

So I thought of putting everything in a comment , without needing to define the intermediante functions:

(comment
  (def argument "foo")
  (-> argument XXXX YYYY) ;; => result1
  )

This way I can easily go step by step of the transformation of the argument into the result I want. Having finished this, I now have to “backport” everything into the initial let of my main function, and the cycle repeats.

How could I improve this workflow?

Pardon my ignorance, and let me know if something is not clear

Thank you for precisely describing the problem that deflet aims to solve:

You can include it in your bb.edn under the :deps key and then use it from babashka.

1 Like

Hi @viperML, welcome! And thanks for the question.

My day-to-day workflow uses a lot of REPL interactivity and Emacs’s ability to work with file buffers. I had your question before and have developed my workflow to solve it (see more background on my blog here). Just be warned that my workflow might not be a good fit for you depending on your tolerance to work with unsaved files, broken s-expression syntax, and keeping track of your REPL state in your head. I know people who like to keep balanced parentheses all the time. So, just be warned.

My workflow is by introducing temporary unpaired parenthesis in the file buffer and never saving them to the file/disk. I’ll use your example for the rest of the explanation. Note the point (or the cursor) is noted as -!- so this symbol is not the actual code.

First, I’ll bind the argument Var, so it’s available in my REPL session. So move the point (or cursor) after the (def argument ...) form and evaluate the last s-expression. So far, we haven’t introduced any broken syntax, so this is safe to save to the file.

(defn -main [argument]
  (let [result1 (fn1 argument)
        result2 (fn2 result1)
        result3 (fn3 result2)]
    result3))

(comment
  (def argument "foo")-!-  ;; => #'user/argument
  )))

Then, move the point/cursor into the -main function after the (fn1 ...) form, add temporary code to close off the (let ...) form, and return the intermediate result, result1. Evaluate the last s-expression before the point/cursor.

(defn -main [argument]
  (let [result1 (fn1 argument)] result1)-!-  ;; => VALUE FOR RESULT1
        result2 (fn2 result1)
        result3 (fn3 result2)]
    result3))

(comment
  (def argument "foo")
  )))

And now, the value of result1 is available in the REPL as *1. Feel free to bind it to a Var with (def -result1 *1) in the REPL if you want to use it later. Otherwise, undo the changes and move on to the next form to inspect, such as:

(defn -main [argument]
  (let [result1 (fn1 argument)
        result2 (fn2 result1)] result2)-!-  ;; => VALUE FOR RESULT2
        result3 (fn3 result2)]
    result3))

(comment
  (def argument "foo")
  )))

You can also apply this temporary-unbalanced-parenthesis technique inside the comment block:

(comment
  (def argument "foo")
  (-> argument
      fn1)-!-  ;; => VALUE FOR RESULT1
      fn2
      fn3)
  )

Remember! Undo the changes when you are done with the temporary code.

Again, this workflow depends on your appetite for a few things. @borkdude’s solution looks very promising, too, so I’m here to provide you with a different option.

deflet looks interesting but how do you get around a let value being passed a function param?

At the moment I duplicate the let as defs in a comment and replace the param with another def. It’s a bit messy though

@rtb Not 100% sure what you mean, but do these ideas maybe inspire a solution?

(require '[borkdude.deflet :refer [deflet]])

(defn foo [x]
  (#_deflet identity ;; switch back to deflet in production
    (def x x) ;; capture x argument, inspect in the repl
    (inc x) ;;=> 2
))

(comment
  (foo 1) ;; call foo with inputs   

or:

(require '[borkdude.deflet :refer [deflet]])

(comment
  (def x 1) ;; def x to 1, now we can jump to the function body and evaluate stuff

(defn foo [x]
  (deflet
    (inc x) ;;=> 2
))
1 Like

Yes that’s really interesting. I need to do some experimenting but I think that will solve it. Thank you!

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.