[HN Gopher] REPL vs CLI: IDE wars
___________________________________________________________________
REPL vs CLI: IDE wars
Author : vlaaad
Score : 117 points
Date : 2021-07-01 13:46 UTC (9 hours ago)
(HTM) web link (vlaaad.github.io)
(TXT) w3m dump (vlaaad.github.io)
| godshatter wrote:
| > Don't forget the set -euo pipefail at the beginning of your
| script. What does -euo means? I don't know, I copy-pasted it and
| man set said there is no manual entry for set.
|
| 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.
| AceJohnny2 wrote:
| Off-topic, but Bash's manpage is awful and a great
| demonstration of the limitation of manpages.
|
| 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)...
| AceJohnny2 wrote:
| Perl (and Git and TCL) seem to have found a middle ground,
| where they broke up their manpages into different ones that
| refer to each other (via "SEE ALSO" section at the bottom).
| But it's sad that, in 2021, we still have such a poor
| terminal documentation system, and the improvement invented
| 40 years ago (Info) gained no steam...
| ajkjk wrote:
| It's kinda nonsense that `man set` doesn't provide that
| information. But it's kinda nonsense that those flags aren't
| the default, anyway.
| aidenn0 wrote:
| "man set" can't provide that information trivially without
| knowing your shell; dash/bash/ksh/zsh will all be subtly
| different.
|
| 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 *
| drran wrote:
| > (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
| smw wrote:
| I think you might be missing the main "aha!" of clojure REPLs vs
| REPLs in non-homoiconic languages: you can very easily execute
| small parts of the program you're editing without re-typing the
| code.
|
| 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.
| dragonwriter wrote:
| Homoiconicity isn't required for that, all you need is to be
| able to map between source and AST enough to identify
| expression boundaries.
|
| 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.
| elamje wrote:
| Maybe it's not required. However, I can confidently say
| Clojure in emacs feels like an entirely different development
| experience than my daily Python work in PyCharm/VSCode and C#
| in VS. I recommend trying it out if you haven't already.
| [deleted]
| pjmlp wrote:
| And I recommend trying out Smalltalk.
| marcosdumay wrote:
| Homoiconicity is an ill-defined concept. If you take every
| subjective feature from it, all it implies is that you are
| able to cut your code into well defined AST structures (what
| all languages have, but for some it's easier than for
| others).
|
| 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".
| js2 wrote:
| It's a well-defined concept in theory but in practice it is
| not:
|
| https://www.expressionsofchange.org/dont-say-homoiconic/
|
| https://news.ycombinator.com/item?id=20657798
| pjmlp wrote:
| Not only that, Smalltalk, Mesa and Mesa/Cedar workstations,
| all shared the same interactivity as Interlisp-D at Xerox
| PARC.
| leephillips wrote:
| Indeed; I do this every day with Vim and its term command,
| with Julia, Python, bash, Elixir--it works with any language
| with a REPL of any kind.
| nlitened wrote:
| I am not familiar with how Julia and Elixir work, but form
| my experience Python's REPL is not the same at all as REPL
| in Clojure or other lisps.
|
| 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".
| leephillips wrote:
| No, Julia and Elixir are the same as Python in that
| regard (Python is more annoying because of the
| indentation problem). I was just talking about
| interactive evaluation of expressions selected in the
| editor. Although I've messed with Clojure a bit, I admit
| I don't fully appreciate what you're describing. I'm a
| little afraid to find out, frankly, because it sounds
| seductive.
| shele wrote:
| Julia is much closer to Clojure than to Python in this
| regard, which brings back the point from above that
| homoiconity isn't the key ingredient
| leephillips wrote:
| How so? In julia, one problem is that you can't redefine
| datatypes in the REPL. You need to restart.
| shele wrote:
| Yes, that restart is painful exactly because it
| interrupts the "built your program in memory while you're
| writing the source code file" OP talks about.
| utxaa wrote:
| my inkling is that the more special forms the more
| difficult it is to repl a language. if everything is an
| expression not only can you eval parts of it, but that is
| enough to support quite a bit of extensibility sans
| macro.
| [deleted]
| dgb23 wrote:
| There is a significant, qualitative difference between using
| a language that designed for REPL use and one that isn't.
|
| 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.
| tomrod wrote:
| This is the draw for Jupyter Notebooks as well. Execute code
| blocks. I remember when I learned this in Matlab.
|
| 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.
| bookofsand wrote:
| Step1: Write a test.
|
| 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.
| dec0dedab0de wrote:
| What if you don't know what you want it to do yet? Maybe
| you have a new API or library you're working with and the
| documentation is lacking. Maybe you're exploring some
| dataset trying to figure out what kind of information can
| be gleaned from it.
|
| 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.
| bookofsand wrote:
| A. Then don't submit the exploratory test in your next
| PR.
|
| 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.
| pjmlp wrote:
| Now try to fix the code from the unit test when it breaks
| and redo the unit test, from the point where it broke.
| mypalmike wrote:
| Throw a breakpoint in the unit test.
| pjmlp wrote:
| It won't work on the CLI version, and unless you are
| using an IDE with a backtracking debugging it won't help.
| cbm-vic-20 wrote:
| Modern Java (11+) has jshell, which has interactive goodies
| like tab completion, documentation display, paren/bracket
| matching, etc.
| utxaa wrote:
| it's something.
| simiones wrote:
| The homoiconicity of Lisp is not what gives it this superpower.
| It is rather Lisp's dynamic nature that actually allows you to
| keep running your project while also developing it that is
| crucial - something that Clojure running on the JVM can
| actually only approximate.
|
| 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.
| utxaa wrote:
| i get the magical aspect of repls in scheme and CL. not in
| clojure. not sure why. so i don't like clojure, but i feel
| like i should ?? why is that?
| tsimionescu wrote:
| As I said, I think Clojure is significantly iimpacted in
| how much dynamic reconfiguration it can handle by compiling
| down to the JVM.
|
| Still, personally I have little to no experience with
| Clojure, so I can't really comment too deeply.
| fnord77 wrote:
| I can do this in Java and C with the right IDE.
| utxaa wrote:
| i don't love java but no one can argue with the fact that it
| has fantastic tooling.
|
| properly written java is a joy.
| winrid wrote:
| You can do this in IDEA's IDEs too, just place a breakpoint and
| then you can "evaluate expression" in that context to call
| functions, read values, etc.
| keymone wrote:
| you just have to try to understand the difference. the
| friction is zero in a proper repl, unlike breakpointing and
| evaling.
| fnord77 wrote:
| I remember doing this with emacs and C in the 90s
| winrid wrote:
| Nice. I definitely want to give emacs a solid try at some
| point.
| utxaa wrote:
| i've used emacs for 20 years. i still spend 80% of my day
| in it ... and vscode is vastly superior for development.
| BoxOfRain wrote:
| Kotlin and IntelliJ IDEA really are a joy to work with in my
| opinion, they're very much my primary tools at the moment.
| I'd never really been exposed to functional programming
| features until I got into Kotlin and now I'm learning Scheme
| of all things, it's been a bit of a gateway drug for me.
| lennoff wrote:
| When you evaluate a form deep in a function, how do you handle
| variables that are defined "outside"? How can you evaluate such
| expressions?
| Scarbutt wrote:
| By evaluating outer forms first.
| slifin wrote:
| Cursive has a repl powered debugger just put your breakpoint
| there use the repl to trigger the call then use intellji's
| debugger to step through
|
| 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
| notriskfree wrote:
| Smalltalk famously let you code inside the debugger. You
| can run code with methods that don't exist yet and add them
| in the debugger when it notices they are missing.
| synthc wrote:
| Cider (the clojure plugin for emacs) has a variant that
| prompts you for the the values for those variables.
| utxaa wrote:
| hmmm ... what if it's a closure, and it depends on a huge
| lexical environment?
| eckesicle wrote:
| Which function is that? I couldn't find it in the docs
| weavie wrote:
| Oh nice. That's one place where Clojure beats Common Lisp
| then..
| PuercoPop wrote:
| Does it? Asking for a value when encountering an unbound
| variable is a default restart $ 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 \*
| weavie wrote:
| Actually, yes I seem to remember it doing that a while
| ago. I think my Emacs setup has messed up somehow. Now I
| just get: 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..
| fiddlerwoaroof wrote:
| Yeah, in fact, Clojure needs special tooling for this,
| while it's built into CL's execution model.
| weavie wrote:
| You can essentially give values to global variables with same
| name as those outside variables.
| notriskfree wrote:
| I suspect you are right. Rebinding an inner function inside
| another function that contains variables probably will not
| work. But you could still alter and rebind the entire thing.
| And that is still flexible; you can still change a running
| program on the fly with only that ability. And you can always
| reference objects at the top level.
| Graziano_M wrote:
| The repl maintains an environment.
| dgb23 wrote:
| You can use a REPL powered debugger. You can interact with
| the debugger to a higher degree than with most mainstream
| languages. It's not quite as powerful as CL though, from what
| I've read.
|
| Or you pull out expressions and test them in isolation with
| given (assumed) inputs.
| utxaa wrote:
| > 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.
|
| 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.
| a-dub wrote:
| protip; you can bring this flow to any language with a repl using
| vim-slime and a terminal manager like tmux.
| revscat wrote:
| That was my impression. I've been doing this for years with
| Ruby, tmux, and some custom zsh widgets.
|
| https://github.com/jchilders/dotfiles
| geokon wrote:
| Thanks for introducing me to `add-lib` This is going to be a huge
| time saver :) More info here:
| https://insideclojure.org/2018/05/04/add-lib/
|
| 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.
| marcosdumay wrote:
| > Maybe I'm "holding it wrong" but this causes a lot of
| friction and lost time.
|
| 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.
| geokon wrote:
| I don't really follow.. How is it a non problem in functional
| code? For instance you have some recursive idempotent
| function that blows at some point. Wouldn't you wanna see the
| state at which things broke?
|
| Just because things are functional doesn't mean you always
| knows the inputs at all times
| simiones wrote:
| What does this have to do with imperative state?
|
| 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 .
| dghf wrote:
| This should work just as well: (csv/read-csv
| (slurp "my-csv-file.csv"))
|
| Or if you prefer the threading-macro style:
| (-> "my-csv-file.csv" slurp csv/read-csv)
| geokon wrote:
| Oh thanks. Yeah. I thought maybe I was noobing it up :)
|
| Always nice to learn something new
| thom wrote:
| CIDER ships with a perfectly adequate debugger if you want it.
| Might require a bit more manual labour than some environments
| but you're never stuck just staring at a stack trace if that's
| what's bothering you.
|
| 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.
| jefurii wrote:
| Linked to in the article: UNIX as IDE
| (https://blog.sanctum.geek.nz/series/unix-as-ide/)
| tomconnors wrote:
| This post could be summarized as "write as much of your project's
| tooling as you can in the project's main programming language and
| the project's main programming language should be Clojure" and I
| agree wholeheartedly.
| toomanybeersies wrote:
| The general gist of the article also applies to Ruby too.
| blacktriangle wrote:
| I think their point is bigger than that. Historically one of
| the major points against using Clojure to write your tooling
| was the slow startup times which are just painful from the CLI.
| It looks like the clj-exec idea linked to in the article is the
| secret sauce that makes moving to writing your tooling in
| Clojure a workable idea, since now you have a unified calling
| convention for both calling tooling from the REPL during
| development but also activating your tooling from the CLI in
| some CI or build pipeline where the massive Clojure startup
| times don't matter.
|
| 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.
| Borkdude wrote:
| Also check out https://babashka.org: it offers Clojure
| scripting with very fast startup time. It also has a task
| runner (similar to make, just, etc.) that can be used to
| store long invocations (like clj-exec tends to have).
| utxaa wrote:
| what about the horrible debugging experience?
| lateusername wrote:
| Considering its ecosystem, what do most here use Clojure for or
| what do you think is its sweet spot? I used it in a project with
| pg/ring/reitit and while some things were nicer it was not better
| enough to make me switch from pg/nodejs/express for new projects.
| Used emacs with inf-clojure (found cider too bloated and buggy,
| was tempted to port some features of cider to a fork of inf-
| clojure) and macros were helpful in a couple of places, but other
| than that, you can write JS almost as how most use Clojure. Also
| found JS to be faster unless you got out of your way to write
| Javaish Clojure, which reminds that is was a time sink having to
| deal with Java libs(it's really not as "seamless" as most
| advertise) because of no Clojure equivalents.
| bcrosby95 wrote:
| Nowadays I use Clojure as my primary general purpose
| programming language.
|
| 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.
| blacktriangle wrote:
| Clojure's sweet spot is very much the "situated program" Hickey
| likes to talk about. Clojure is a great fit for line of
| business apps where maps really are the right choice for
| modeling your domain.
|
| 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.
| dgb23 wrote:
| Some Clojure frameworks and libs do provide REPL support for
| things that we would otherwise expect to be CLIs, such as running
| migrations, (re-)starting services and so on.
|
| 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.
| airocker wrote:
| REPL will not work well with multitheading/futures in Clojure.
| utxaa wrote:
| or the ridiculous namespace system.
| [deleted]
| mgiannelis wrote:
| I want to publish thois theory on tech business news
| https://www.techbusinessnews.com.au
|
| How would I go about adding this to a feed?
| dig1 wrote:
| > I use add-lib branch of tools.deps.alpha that allows me to add
| dependencies dynamically at the REPL and then start using them
| immediately, just like in the shell.
|
| You could do this with (battle-tested) pomegranate [1] ages ago,
| which is used by leiningen as the default resolver.
|
| [1] https://github.com/clj-commons/pomegranate
___________________________________________________________________
(page generated 2021-07-01 23:01 UTC)