REPL vs CLI: IDE wars
vlaaad.github.io
vlaaad.github.io
In emacs, for instance, you often use `eval-last-sexp`, by default bound to C-x C-e. This lets you move your cursor to a particular point in the file, often deep in a function, and get the results of just the form(s) you're pointing at.
This is a superpower! It lets you test small pieces of your code without writing any test harnesses or scaffolding. Try it with an editor that embeds the repl and has these commands, and you'll never want to develop any other way.
It does cause you to want to structure your code in 'repl-friendly' ways, so that you can, say, restart your main server or reset your state without restarting the whole process.
Plenty of development environments for non-homoiconic languages provide the ability to evaluate a selected expression (not just the last one) in a linked REPL on demand.
I love the Lisp family, but I don't know why its advocates often sound like they haven't seen a dev environment for a non-Lisp language since the early 1980s when pointing to “unique” advantages of Lisps.
So, yeah, anything people do with Lisp could be done for any other language, it only requires more complex tooling. How much more complexity depending on the language, and it varies from "barely perceptibly more" into "that's completely not practical".
See the book Practical Common Lisp for an intro: https://gigamonkeys.com/book/
Those boundaries you talk of are the crux of the issue. A highly dynamic, completely expression based language is going to enable a much different experience. Homoiconicity also plays an important role here, because you can ispect and parse code within the language, with the same functions and algorithms as everything else.
In Clojure, you literally built your program in memory while you’re writing the source code file, updating the memory representation of your running program part-by-part without ever stopping it.
This experience is absolutely magical, and it’s very hard to go back to edit-compile-run loop in other languages after that. It is completely different from “my language has a REPL and I can execute parts of code in it”.
in particular this does not mean that it is better but the same advantages of being able to put anything into anything that you find in expression languages like rust in lispy languages are also syntactically in editing source code.
since I started using shrink/grow selection in vscode I have much increased interest for simple syntax aware editing, something simple would be moving blocks of code token by token
For example, in a full CL, you can modify a class that is currently being used, and objects of that class will adjust accordingly (for most modifications). This is not possible for the JVM - to modify a class, you would have to unload it, which can only happen if the Class object is garbage collected, which requires all instances of the class to be GCd, and the ClassLoader that loaded that class as well - only then could you add a field with a default value to the class.
Still, personally I have little to no experience with Clojure, so I can't really comment too deeply.
$ sbcl
This is SBCL 2.1.1.52.HEAD.321-f8a57bcca, an
implementation of ANSI Common Lisp.
More information about SBCL is available at
<http://www.sbcl.org/>.
SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses. See the CREDITS and COPYING files in the
distribution for more information.
\* (\* x x)
debugger invoked on a UNBOUND-VARIABLE in thread
#<THREAD "main thread" RUNNING {1001860103}>:
The variable X is unbound.
Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.
restarts (invokable by number or by possibly-abbreviated name):
0: [CONTINUE ] Retry using X.
1: [USE-VALUE ] Use specified value.
2: [STORE-VALUE] Set specified value and use it.
3: [ABORT ] Exit debugger, returning to top level.
(SB-INT:SIMPLE-EVAL-IN-LEXENV X #<NULL-LEXENV>)
0] 2 Enter a form to be evaluated: 3
9
\* Debugger entered--Lisp error: (void-variable k)
And then a stack trace. No option for restarts.It works if I run from the command line though..
If you want to run expressions in context just use the expression editor like a normal repl
Something I haven't tried yet is redefining functions at debug time can't see why it wouldn't work though
Or you pull out expressions and test them in isolation with given (assumed) inputs.
REPL are super nice for this!
I've had a hard time in Java, Fortran, etc. in determining the right development patterns that are used in this space to be productive.
Step2, IDE: Right click + Run.
Step2, CLI: $ run <test name>
Bonus: The test is now part of your automated test suite, and will be ran many times to ensure things don't break.
Maybe it takes 3 minutes to get it into the state where it failed, so instead of starting over every change you just want to modify the one function over and over.
Doing it in a repl like jupyter has you run the actual code many times as you're writing it so ensure things don't break before you ever save the file. Of course thats not an excuse to skip tests, but if I was going to pick one, I would choose running the code as I write it, instead of testing it after the fact.
B. Then memoize the expensive function to disk. Bonus: Can keep multiple versions on disk, thus can tweak the expensive function while having fast reloads with guaranteed correct version.
but that depends on how the environment is looking at the time.
during development, in scheme, i just have a function (reset) that reloads all the files of an app. sometimes i create a thread and poll the filesystem and reload everything that changed in the last second. that way i never have to C-x anything. if i need to eval a subexp i just jump on the repl and do it.
In fact they weren't always unresponsive, seems Jetbrain's products have regressed in the last year, but mostly Webstorm. I use WS on OSX and Win10, and on both I get all kinds of slowdowns - different projects too. Tried disabling features, plugins, different GC, increased heaps. I may have to fire up J. Mission control at this point.
IDEA and Clion on Win10 are mostly fine.
It would just be cool to have all of these features, but the frontend be a responsive CLI.
properly written java is a joy.
This article comes at a perfect time, we're just starting a new Clojure project and were looking into how to automate tooling since we'd been burned by Clojure startup times before. Looks like clj-exec means we can now unify our work on Clojure.
Hopefully there will be some way to just "reload" your whole `deps.edn` though
"If you get an error, the execution stops by default and you get a stack trace."
I'd say the other missing piece of REPL development is that while you get a stack trace, you don't get a program state like you do with GDB or ELisp. Maybe I'm "holding it wrong" but this causes a lot of friction and lost time. I'd be curious how others approach this. And that all being said, the CLI doesn't relaly offer a better alternative here.
For entirely replacing the CLI I think that since Clojure is a general purpose language there ends up being a tad more boiler plate than you'd like.. It's not at the point where you're gunna just run `clj`, load in some library with `add-lib` and start messing around b/c things are just a tad too clunky.
For instance if you wanna read in a CSV file (I had to look this up)
(-> "my-csv-file.csv"
(io/file)
(.getCanonicalPath)
(io/reader)
(csv/read-csv :separator \,)
(#(into [] %)))))
Uhh.. so you're prolly gunna want to wrap that up in a helper function. I personally end up making a dummy "project" where I keep a bunch of helper functions and then doing my REPL "scripting" and messing around in that. It feels a bit wrong.. but at least to me it looks like a solvable limitation. Given a nice set of helper libraries you could probably get to a point where a bare `clj` REPL would be as ergonomic as a more explicitely interactive language like R/MATLAB/etc. (csv/read-csv (slurp "my-csv-file.csv"))
Or if you prefer the threading-macro style: (-> "my-csv-file.csv" slurp csv/read-csv)Always nice to learn something new
I think using external dev dependencies and rich dev/user.clj files is pretty common on Clojure projects. Your REPL isn’t just somewhere to interact with the current codebase directly, it’s a framework for building that software and managing its environment more generally.
But I'll take another look in my next project
It is a completely non-problem for functional code. It is a big problem for imperative code.
You can't just write all of your code in a functional style, but depending on what you are doing the limit gets larger or smaller. So it's normal that this will be a showstopper for some people, and irrelevant to others.
Just because things are functional doesn't mean you always knows the inputs at all times
Take this program:
compute x = 1 `div` (x - 3)
map compute [1,2,3,4]
Would seeing that it crashed in `compute` in `map` be enough info to debug, or finding out that `x` was 3 when it did also help?Edit: fixed to use integer division so we actually crash .
Use "man bash" to find the list of built-in commands. Scroll way down, or search for "SHELL BUILTIN COMMANDS".
The "e" command-line switch to the set command tells the script to exit immediately if there is a non-zero return value. The "u" switch tells the shell to treat unset variables as errors when performing parameter expansion. The "o" switch enables the following option (in this case "pipefail"). "pipefail" tells the script to return the value of the rightmost command that returned with a non-zero value in a pipeline. This is all paraphrased, the details are in the man page.
Not that the content is bad (it's great!), but as OP (and you) demonstrated, finding relevant information is close to impossible in the humongous document.
In Bash's defense, its manpage is a concession to people's habits, and its documentation really rests in its Info page. Except that doesn't seem to be popular either (I'll admit to being still inexperienced with its UI, after decades of sparse usage)...
https://www.gnu.org/software/bash/manual/html_node/index.htm...
However, if you are running bash, "help set" is fine.
FWIW, I really don't like pipefail as a default (e.g. piping to "head" causes random pipefails) and -e also has some confusing semantics. Also either failglob or nullglob are more important than either and -C is useful for some scripts as well.
[edit]
Simple example of confusing "set -e" semantics:
The following does not print "hi" and shows returns error status, as expected:
(set -e; echo hi); echo $?
But put it in an if statement, and it's suddenly success, and does print hi: if (set -e; false; echo hi); then echo hello; fi
The same problem applies to functions. The below function will remove all files in the current directory if it's called from a conditional, but not otherwise! foo() {
set -e
cd /some_directory # set -e means we exit if this fails
rm -rf *
}
I think just defining a die() function and using it after any command that must succeed is more verbose, but less error prone: cd /some_directory || die "chdir failed"
rm -rf * > (set -e; echo hi); echo $?
It works in my shell. :-/ It looks like you forgot to insert `false` command.You are pointing to the problem with -e not working in subshell/deep functions, because of POSIX. Right? It's described in bash documentation: http://www.gnu.org/software/bash/manual/html_node/The-Set-Bu...
> I think just defining a die() function and using it after any command that must succeed is more verbose, but less error prone:
Yep. It's the style I developed 12 years ago, when working at Bazaarvoice, when I was lead of devops team. I created the whole library for bash, to use this pattern consistently. See https://github.com/vlisivka/bash-modules#error-handling
https://gist.github.com/zachriggle/8574964d2e3078cdfae84b574...
e.g.
An error occurred on ./test:18
Frame 1 (./test:21)
18 false
19 }
20
21 >>> a
Frame 2 (./test:6)
3 source TRAPERR.zsh
4
5 a() {
6 >>> b
7 }
8
9 b() {
Frame 3 (./test:10)
7 }
8
9 b() {
10 >>> c
11 }
12
13 c() {
Frame 4 (./test:14)
11 }
12
13 c() {
14 >>> d
15 }
16
17 d() {
Frame 5 (./test:18)
15 }
16
17 d() {
18 >>> false
19 }
20
21 a #!/bin/bash
. import.sh strict log
a() {
b
}
b() {
c
}
c() {
d
}
d() {
false
}
a
Result: $ ./test.sh
[test.sh] PANIC: Uncaught error.
at d(./test.sh:13)
at c(./test.sh:10)
at b(./test.sh:7)
at a(./test.sh:4)
at main(./test.sh:16)
However, it stops to work at some point, e.g. in `for` loop, or in a subshell. zsh is better in this regard.As far as sweet spots go, due to pervasive immutability, I think it can be a good choice anytime you're dealing with concurrency.
If you're dealing with relatively straightforward web applications, outside certain specialized scenarios (and certain really bad choices) I don't think language choice matters.
We recently evaluated Clojure vs node for an upcoming project and went with Clojure because while most of the work really is just querying a db and writing JSON, we also do a fair amount of heavy reporting and pdf generation which will bring node to a crawl relatively speaking. Rather than have to take on the operational complexity of microservices for the slower parts of our app we just went with a Clojure monolith. The draw of a single language to work with for both client and server was very tempting, but ultimately the JVM won out on the server.
I'd be amiss if I didn't also say the whole Deno situation has us worried about the long-term implications of spinning up a brand new node project, where the JVM seems far more reliable logistically.
Can you share your approach to generating PDFs in Clojure?
We're not at a point where we've figured out how we're doing report generation yet. Internally we do not have extensive experience with either Clojure or Node, but we have heard issues from colleagues with Node and running into performance issues by trying to do too much on the main thread. Since our load demands are not that high, and we're migrating from Rails so we already expect a very nice performance bump, rejection of Node is more a choice of avoiding potential downfalls rather than a fully informed decision based on experience with Node.
The startup time criticism is valid, but in context of development you typically don’t restart it except you pull in deps (very rare) or something terrible happens (rare).
Then, there is also borkdude/babashka which is a Clojure powered scripting tool with fast startup times due to GraalVM.
You could do this with (battle-tested) pomegranate [1] ages ago, which is used by leiningen as the default resolver.