Fourteen Months with Clojure
blog.skyliner.io
blog.skyliner.io
I'm preparing to write a lengthy post comparing Clojure and Racket, the other Lisp I have been using for personal projects for a couple of years now. My opinion is that Racket is a better language overall, although it may be less practical in terms of writing production code and it certainly lacks some of the nice features Clojure brings. On the other hand, many of these nice features are available as libraries (not only in Racket - in most Lisps, including Elisp and Common Lisp).
For the last two years, I used StumpWM as my window manager and so I learned some Common Lisp. Again, as a language, I think Common Lisp is still better than Clojure, although it's definitely stranger.
I use Emacs as my default "computing environment", so I naturally learned Elisp, too. This is the only Lisp I used which I'd consider inferior to Clojure, but even then it works quite well for its use-case.
All in all, from the perspective of Lisp family of languages, Clojure doesn't seem to be exceptional in any aspect. It does have nice features, and I bet it feels much better compared to Java, but it also has some downsides to it which are irritating when coming from Racket or Common Lisp.
It should go without saying that this should not actually reflect poorly on Clojure as a language.
Bad clojure code is unmaintainable spaghetti code; which gets worse over time, as people attempt to 'patch on' fixes without doing the heavy lifting of trying to figure out:
- What was the original author actually trying to do?
- Why the heck did they do it like this?
- How do we create the same functionality and prove it works with these rubbish tests that only test the individual units of work, not the application function?
- Why is it all in one giant file?
I've never seen code bases descend into chaos as fast as our clojure ones have.
Nice, elegant clojure is a pleasure to work with for personal projects, but I'm never using it professionally again.
You might argue it doesn't reflect on the language, but I think it does. Given what I've seen, I'd argue that clojure has an inherent complexity that results in poor code quality outcomes during the software maintenance cycle.
...specifically, sections of bad code have a disproportionately negative effect (compared to other languages) on the surrounding code and negatively impact the entire project's code quality.
You hack something out for a deadline? You better go back and clean it up, because if you don't that codebase is screwed. shrug That's just been my experience over the last year on three different code bases.
I switched to F# and "get" FP much better now. I'd know what I was doing if I ever switched back to Clojure. But I have no good reason to personally, and all the aforementioned reasons not to professionally.
In fact I've had these reactions at various times to code in every language I've worked in (including Clojure, to be sure), but I don't agree with your argument about Clojure having greater than average inherent complexity.
Once you get comfortable with parens, the core seq functions, and even a basic understanding of laziness & immutability, so much of the inherent complexity you find in many programs just goes away. Well -- most of the time. Again, Clojure is not going to stop you from negating these niceties with bad decisions.
The one thing I'd maybe admit to it being a little above average on is the temptation to over-engineer due to novelty or feeling clever. Let's rub some core.async on it! Parallelize all the things! It's actually easier to do this because of the lack of inherent complexity in Clojure -- as the author mentions, everything is oriented around simple maps & lists, so juggling them and squeezing them through Rube Goldberg machines of transformations and mystery macros is definitely a thing you can do.
But, ideally, you just learn to . . . not do that. Like abusing Ruby metaprogramming.
Tell me you've never picked up a piece of clojure code and gone, WTF does this do? What are these global channels for? Why is the whole application constantly updating a top level full-application-state atom? Why are we blocking indefinitely with some magical invisible state hidden in a closure as we iterate over a collection of objects to process?
If you have a beautiful clojure code base, it's fantastic...
But, ideally, you just learn to . . . not do that
How do you fix a bad code base when the functions aren't pure (global injections, global channels), or are 'pure' only in the sense the input is the entire application state, including non-pure objects like a database handle? Or your functions are non-deterministic due to some kind producer/consumer race condition?I get it; don't do that.
...but what do you do when it's too late, and someone has already made those bad decisions?
Where are the debugging tools to help you figure out what's going on, and the refactoring tools to help isolate code units and replace them?
I mean, sure, you could argue that's an issue in any language, but all I can say is that I've used a lot of other languages, and the only other similar experience I've had was working with perl.
I think something that would help the Clojure community improve, especially with regard to introducing Clojure to a team is to ask why this kind of thing doesn't happen in Python.
I know Python doesn't come with atoms, but you could certainly stick the whole application state in a global variable. You wouldn't though if you're a working programmer with a modicum of experience on your first Python project. Do people just see some new idioms and forget everything they already knew about programming?
Python does have closures, and you certainly could write one that blocks due to some hidden state or does something stupid involving multithreading. This isn't seen as a problem with the language though; it's seen as a sign that the person who did it might need to work more closely with someone more experienced or that the team needs better code reviews.
Huh. I thought this is what Om Next did to great success.
* https://medium.com/adstage-engineering/realtime-apps-with-om...
[edit] i misunderstood your argument. Your not implying a single state is bad, your saying their is nothing in python to deal with it.
Why are you even using FP at that point?
Global application state should be represented in a single root storage; the application data store (ie. database), and the interactions with it should be controlled and sanitised.
If you have a thousand little places across your code base updating and reading from the database, that's horrible code too, in java or in clojure...
Modern UI frameworks are carefully controlled access and update to the display state for a UI; they happen in a controlled and orderly manner specifically to prevent the chaos you get otherwise; if not, you're doing it wrong.
This section in the om docs covers their solution, which is quite elegant: https://github.com/omcljs/om/wiki/Quick-Start-(om.next)#glob...
(notice, you don't just reach out and update atoms directly by hand; there's nothing wrong with having a global application state; that's a good thing, but directly interacting with it is not)
Of course there's no stopping a determined person from writing bad code, but there's a big difference between various languages in how easy it is to accidentally or unknowingly writing bad code.
I can't prove any of this; it's all anecdotal, but I thought I'd mention that your experience might not be typical.
(The impression I've gotten is that the persistent data structures are very nice, and the language has some good ideas to go with them, but overall it didn't entice me away from Scheme or Common Lisp. But that impression doesn't explain why you had this bad experience with troublesome Clojure code.)
Two main reasons for this are that Clojure is immutable by default, and it has minimal syntax. Immutability allows projects to be naturally compartmentalized. You can do safely do local reasoning on parts of the project. Meanwhile, simple and consistent syntax means that it's easier to read and understand the code written by others. There are far less language quirks to remember than in most languages.
I also find that the editor integration with the REPL is a huge plus as well. Whenever I run into code that I'm not sure about, I can just run it and see what it does.
Couldn't you say the same thing about most languages?
However, I'd be interested in which parts of Racket and CL klibertp thinks are making them better languages than clojure.
Is this a function of the teams or the language?
Why not? If a language's goal is to be practical (as is Clojure's), then it should take practical concerns into consideration. This is something that the Java community definitely gets right.
Because someone who has never heard of map/reduce/filter/partial will need to do some studying to leverage the spine of the language in order to make living, breathing code-organisms.
We've hired a number of devs for our Clojure projects, and none of them knew Clojure when they started. We found that this process has worked very well for us.
A user-defined function with a name indicating what you're reaching for. Occasionally you might want to write a generic cons handling function where all you can say is that it's the car of the car, but those instances are extremely rare. The only place you should be writing car and cdr compositions is in one line wrapper definitions, and there the benefits (and drawbacks; it doesn't really matter which you use) of the c*r composition functions don't really show themselves.
edit: It's the same logic as using first and rest when you're dealing with a list (that is represented with conses). If you're handling parsed JSON, why would you be talking about cars and cdrs?
(let* ((data '((user1
(email . ("bob@dom1" "bob@gmail" "hey@bob.com"))
(sshkeys . ((default . key1)
(alt1 . key2) (alt2 . key3))))))
(user (cdr (assoc 'user1 data)))
(email (cadr (assoc 'email user)))
(sshkey (cdadr (assoc 'sshkeys user))))
(list email sshkey))
The thing is one would be (rest (first (rest (assoc ..., the other is (cdadr (assoc ..., which are equivalent in effect (and the latter has way less parens). I dont understand why there's an ideological you should use this or you should use that thing going on. C*R stuff is an abstaction over combinations of FIRST/CAR and REST/CDR, and I dont see why they should be avoided at all. They've got their place, and all this thread is full of arguments backed by mere taste.I can deal with the Java stacktraces and the occasional mystery of which concrete class is actually backing IPersistentMap after however many invocations of assoc, but if the experience of writing code is going to be a pillar of the language's sales pitch, why isn't this sort of next-level debugging experience on anyone's radar? The CIDER debugger is nice, but it's not CL nice.
It might not matter: maybe being the obvious right choice for some use case in the abstract isn't that important, and contextual factors re who's doing the work and how they think are just as relevant to choice of language. Still, it seems like we tend to talk about languages now in terms of their most indispensable applications, for better or for worse.
Is anyone else out there like me who wishes there was a standalone clojure implementation that wasn't a hosted language? Add a modern package manager on top of that like cargo or mix and I would love to write new projects in it, because clojure itself is a very pleasant experience.
It's a member of the Lisp family that:
- has a lot of high quality, implementations on a lot of different platforms be they native or hosted,
- is multi-paradigm so it doesn't lock you into just functional programming, or just object-oriented, etc.,
- can go from high-level code to fast compiled code (coming close to C++ speeds, like twice as slow.. not many high-level languages manage this),
- has been an ANSI standard for a long time and because of that:
- is a stable language: code from decades ago can be run without much (or no) porting. A while ago code from the early 1970's (or was it the 1960's) was made to run relatively easily, although I cannot find the link,
- is still a joy to program in, it brings together features that have been popping up in other languages but only in CL they feel so seamless,
- has a really nice development environment in Emacs + Slime (if you can stomach Emacs),
- has IMHO a nice and helpful community,
- has a great history, you'll come across almost any aspect of software & computer history when diving into CL.
The CL community has always been hotly debated and is usually negatively depicted on HN. I've never found it to be too bad, although it has attracted it fair share of trolls in the past.
Compared to Clojure:
- Not hosted (on JVM)
- Similar disposition on nudging you toward functional programming
- Less syntactic sugar (some of Clojure's sugar I find nice, but is divisive for some)
- Older
Compared to CL:
- One canonical implementation. This is good (consistency) and bad (choice, variety)
- More biased toward functional programming than CL
- People seem to think Racket's macros are more powerful than CL's, but I'm no macro-master
- Package management (raco) feels much more pleasant, subjectively, to me.
- Develop in Emacs or DrRacket; the latter being a pretty decent IDE for newcomers and others who'd haven't seen the true light of Emacs.
- Newer, feels newer
I started carving off small chunks of time a few years ago to try and hear the music of the spheres everyone talks about with lisps. I started with CL, then tried Clojure, and have mainly been dabbling in Racket these days. Subjectively, Racket has been the best beginner experience by far. As for hearing the music of the spheres, I'm still waiting.
EDIT: I forgot Rackjure! Racket is also a framework for building new languages (languages that are compatible with Racket, generally). Rackjure adds some of the Clojure-isms to Racket and seems to be used by a number of packages
Also, Typed Racket is another such variant of Racket that uses types for safety and performance reasons.
Clozure Common Lisp (http://ccl.clozure.com/) is excellent if you're on the mac.
Windows I'd do Lispworks or Allegro (both commercial lisps), although rumor has it SBCL for windows has gotten much better. Admittedly my last experience doing anything on windows was probably ~5-6 years ago.
Great thing is, the language is standard, so generally you can just pick one and start messing around.
but the cost and price, really stand in the way of it being used in a greenfield/side project, where you work on something new without necessarily having your management approval, and then present it to them once .. it works
I wanna see this kind of tooling for other languages. The JVM gives you amazing tools that are already there for you even if you're not an expert.
In LispWorks I would for example set a memory check for a certain memory size. That would call a function when memory grows over a limit.
You then don't need to look at a heap dump, you can inspect the Lisp memory from the inside with the usual tools...
Clojure the language's coupling with the JVM is actually rather loose, precisely because it's hosted. However, Clojure the ecosystem is definitely coupled. However, I think this only rears its head when you need to incorporate a library and deploy. Once you set up your CI environment, I don't see this as being that big a deal that often.
Add a modern package manager on top of that like cargo or mix and I would love to write new projects in it, because clojure itself is a very pleasant experience.
What of Leiningen?
On the other hand, boot[0] is simpler, can do everything Leiningen can do.
Leiningen kept me from being effective with Clojure. Boot got out of the way.
I'm curious: where do you actually see that coupling?
I find that I barely notice the JVM in practice. There is the main function, sure, but otherwise I rarely use the Java APIs myself. What I do notice is the fantastic runtime with state-of-the-art garbage collection, something I've learned to appreciated after spending years with various Common Lisp implementations.
slow startup time, difficult dumping/loading images, difficult AOT native compilation, overly complex loading of code (-> custom class loaders), no TCO, no late binding for functions, host numerics shines through, delegation of functionality to the host environment using host language constructs, no resumeable condition system, jvm stack traces, error messages displaying JVM details, ...
I'm curious how you avoid the Java APIs since Clojure uses uses parts of the Java standard library for things like strings and dates rather than implementing a Clojure wrapper. Are there good Clojure libraries to use? It's been a while since I looked.
As for the APIs, I almost never use anything from Java directly (well, maybe except for Thread/sleep?). I use libraries like clj-time. Mind you, I'm not actively going out of my way to avoid the Java APIs, it's just that I don't encounter them that often in practice.
Regarding being a hosted language... that was it's intent. Use one of the best run-times available, with a better language. Granted once you design the language to be hosted, it can find other hosts as we see in ClojureScript.
It's true that being a hosted language is one of the goals of Clojure, but it's also true that being a hosted language is a non-feature (at best) for some people, including GP and me :)
Other languages which weren't 'designed to be hosted' can't be hosted? Hmm, I was under the impression that the JVM hosts a lot of standard languages, for example Scheme dialects and Common Lisp.
The main advantage of keeping Clojure hosted is in being able to leverage the host ecosystem. It's simply not practical to implement an ecosystem comparable to the JVM or Node from scratch, especially for a niche language.
But if there is a CL out there that does all that I'd give it a try tonight :)
Parallel programming: https://lparallel.org/overview/ Functional collections: https://github.com/slburson/fset STM: https://common-lisp.net/project/cl-stm/doc/index.html
Worth taking a look into definitely, but it doesn't have STM and "persistent data structures" which jfaucett seems to want. It does have solid concurrency and parallelism primitives, though.
This was an amazing read in the sense that a) it brought back all the reasons why I began using it in the first place.
b) it had some great lines - both the "when the going gets tough the tough use maps" title and this lovely tidbit:
"...maybe Scala has answers to all of these problems now, as I haven’t had the pleasure of using it in several versions. Do not @ me to talk about this."
I think you get even more benefits from Clojure if you write apps that run both server-side and client-side (in the browser, using ClojureScript).
Much thanks to the author for nicely showing a good use case for the Either monad. In general, I think there are lots of great concepts in category theory, but well hidden behind horrendous naming (bind/return, anyone?) and lack of good practical examples (well let's fmap inc over a vector here).
And since it seems everybody pitches in something about some language being inferior or superior to Clojure, I'll contribute something, too: I would not have been able to write PartsBox.io (https://partsbox.io/) without Clojure and especially ClojureScript. It boils down to practical reasons: code size, abstractions that I can build on, avoiding accidental complexity.
I don't like participating in discussions that compare programming languages these days. I feel these are often very shallow. As an example, I used to program a lot in Common Lisp, and I think it can't meaningfully be compared to Clojure/ClojureScript. You don't see that until you've written several large multithreaded applications with zero deadlocks (thanks to STM), after you've used core.async to simplify complex and bug-prone code, after you've used transducer pipelines to parallelize a large stream computation completely avoiding the horrors of Hadoop, or after you've sent your data structures over a websocket to code that is actually the same code that runs on the server (cljc, files that compile to both Clojure and ClojureScript). Seriously — how can I discuss the finer points of syntax (oh, the parentheses!) when I am able to ship React-based apps that do isomorphic rendering (the server pre-renders a page, and JavaScript plugs into it later), all in Clojure+ClojureScript, without even touching node.js? You start to appreciate those things only when you write large applications and your time is limited. Going back to my Common Lisp background, I also wrote web apps in CL, and there is no way I would even think about going back.
There are things I do not like about Clojure, but so far I haven't found anything even remotely comparable in terms of real-world productivity. So I'm sticking with what works really well, at least until something better comes along.
> There are things I do not like about Clojure, but so far I > haven't found anything even remotely comparable in terms > of real-world productivity. So I'm sticking with what > works really well, at least until something better comes > along.
This is exactly my take on it. I find myself thinking about Clojure adoption more and more these days, and where is the disconnect in all this.
I wonder if the idea of clojure both scares away "normal" programmers who are really just focused on using languages in their professional lives and want a simple, focused productive experience, and instead bring in more academic/experimental/recreational programmers who then actually dislike the fact it's made choices to promote use in the real-world.
Super fascinating stuff!
To give you a better idea: no use of component or similar, no ability to run the app 100% in a repl. A junior engineer prematurely split a monolith out into microservices that weren't truly isolated in their function (they shared the same underlying db and a handful of "libraries" that were not libraries) -- a true fucking productivity nightmare. Shipping a small fix would oftentimes involve version bumping and deploying 3 separate jar's before you could even QA it. You NEED to use your repl and reloading tools when doing Clojure. You can't rely on your test suite, jenkins, or suffer through JVM restarts every few minutes. When people do not follow these concepts Clojure becomes an absolute fucking nightmare to work with.
There is a stark difference between programming in a functional/immutable style and using a functional language. You can write imperative and procedural style programs in Clojure just as easily as you can write functional programs in Python.
How can we fix the Clojure ecosystem? It will be hard. Lisp is oftentimes an esoteric environment. You don't see a lot of Lisp/Clojure (correct me if I am wrong) in environments where you need to be pragmatic and move quickly. There also isn't a "rails" for Clojure. With the exception of Lein (which is a truly remarkable contribution to the open source community) -- there isn't a whole lot to use as a metal model or better a popular style of building and organizing your apps. It's all the rope to hang yourself with -- and if you aren't an experienced hacker you're gonna hang yourself.
Finally: I find it really tough to find solid use cases for Clojure as opposed to something else. The real things it has going for it are java interop and easy concurrency. I usually have a very hard time choosing Clojure over Python because of how quickly I (personally) can use Python to build extremely readable, modular, single-responsibility, functional, etc... code.
Using the test-refresh plugin there is no jvm reload time between test runs. You edit code and almost before you look at the terminal affected tests are rerun. I'm not sure why a small fix would require so many version bumps, but that's not a clojure specific program. If you are doing good version control and modify a library somewhere deep in the dependency chain you will have to bump version numbers up the line no matter what. As for jars, again, I'm not sure why you have three unless you are running a big distributed system, in which case that seems inevitable. Uberjars package your entire program into one easy to deploy jar. Which brings me to another major strength of jars, deployment is amazingly easy. All you have is java and any jni libs as production dependencies. Updating requires modifying only one or a few jars. To me, that's a major selling point, not a negative.
Meanwhile, deploying python to production is incredibly hard. Getting just the right set of library versions is a major pain. Do you use the system built in packages? Or perhaps you have to use pip, or another manager? Anaconda?
In my opinion clojure is absolutely great for environments where you need to be pragmatic and move quickly. Being on the JVM and having great interop really contributes to that too. Being able to use the huge java ecosystem in addition to the clojure native ecosystem has meant massive productivity gains for us.
It's surprising that others experiences are so different. Perhaps it's a lack of "best practice" documentation for clojure?
We have since moved away from it because of the inertia required to get past the curly fingernail stigma. But even with Go/Python we haven't been able to get the same velocity compared to Clojure services.
Also, I can understand the team apprehension. I don't think lisp syntax is so bad as compared to the general amount of inertia required to do clojure well: emacs or similar, proper reloading, componentized services, etc...
huh?
I don't know if you're wrong but isn't that the opposite of what PG claimed in his famous "Beating the Averages" essay? http://paulgraham.com/avg.html
As a side note: there's something very comforting when you immediately empathize so strongly with a programming problem someone is writing about
I program in Elixir now, which I'm totally loving. It's also a "no early return" and "totally immutable" language, so I wonder if any "cross pollination" between the languages is possible. This is how I would code the example given in Elixir:
with {:ok, x} <- foo(),
y when not is_nil(y) <- bar(x),
{:ok, z} <- goo(x, y) do
qux(x, y, z)
IO.puts "it worked"
true
else
{:error, :not_fooable} -> IO.puts("foo failed"); false
{:error, :no_bar} -> IO.puts("bar failed"); false
{:error, :no_goo} -> IO.puts("goo failed"); false
nil -> IO.puts("something (bar?) returned nil"); false
_ -> IO.puts("some other error"); false
end
Elixir's cool "with" / "else" macro is used for chains of calls where the intermediate steps can fail. In fact, it was introduced not too long ago, just to deal with nesting code issues like those raised here!It relies on elixir/erlang's pattern matching to specify the happy path, extracting out the success responses (e.g. {:ok, x} will match a response of {:ok, 5}, and x will hold the value 5). If any of the happy matches fail, it falls through to the `else` and you can catch any particular failures you want there.
It's a really great system. Someone should try to add it to Clojure! Not sure how well the "pattern matching" aspect of it can work, though.
I still don't feel great about error handling in Clojure in general. I can't find something that feels both practical and idiomatic. I want to use monads but it always feels like huge impedance mismatch with the rest of the language (most of the time due to nil being semantically overloaded.)
(match (foo)
(#no (log "foo failed") #no)
(x (match (bar x)
(#no (log "bar failed") #no)
(y (match (goo x y)
(#no (log "goo failed") #no)
(z (qux x y z)
(log "it worked")
#yes))))))
I imagine any language with a match macro, like Racket, could look similar. Using monads or exceptions ought to look nicer, but I'd kind of rather not pull out the big guns. (let-every [x (foo) err "foo failed"
y (bar x) err (format "bar %s failed" x)
z (goo x y) err (format "goo %s %s failed" x y)]
(qux x y z)
(handle-error err))
Where `let-every` is a macro that works like let, but stops short on the first nil/false variable, runs only the next symbol binding expression, and then runs the else-clause.There'd be nothing special about the "err" symbol on each line. It's just the next symbol binding, but on the same line as a convenience, and this means it can reference any previously valid symbol bindings.
Here's a quick & dirty implementation of that macro. I don't have a Clojure interpreter installed, so I don't know if it works.
(defmacro let-every [bindings if-body else-body]
(let [pairs (partition 2 bindings)
quad-pairs (partition 2 pairs)]
(loop [quad-pairs quad-pairs]
(if quad-pairs
(let [[quad-pair] quad-pairs
[try-pair err-pair] quad-pair
[try-sym try-expr] try-pair
[err-sym err-expr] err-pair]
`(if-let [~try-sym ~try-expr]
(recur (next quad-pairs))
`(let [~err-sym ~err-expr]
~else-body)))
if-body))))
Given the above example, it should expand to this: (if-let [x (foo)]
(if-let [y (bar x)]
(if-let [z (goo x y)]
(qux x y z)
(let [err (format "goo %s %s failed" x y)]
(handle-error err)))
(let [err (format "bar %s failed" x)]
(handle-error err)))
(let [err "foo failed"]
(handle-error err))) ;; Plain old ANSI Lisp
(let* ((x (or (foo) (error "..."))))
(y (or (bar x) (error "bar ..."))
...)
body)
I mean, if, in the end, we are going to "error out", rather than just forward-propagate `nil`.If we are going to just propagate `nil`, then if we have an `iflet` that tests the last variable, we can do:
(iflet ((x (expr))
(y (if x (bar x))
(z (if y (foo x y))) ;; z is tested by iflet
...)
it's just a bit verbose. That can be condensed with a simpler macro that doesn't have the err stuff.Substitute your favorite dialect's error thrower.
I don't think that your expansion is correct at all. Maybe you have a misplaced quote somewhere, but this one is not making sense.
I mostly use the Maybe and Either monads (and occasionally the exception monad if I'm calling some function in a third-party library that might throw an exception).
https://github.com/Engelberg/better-cond
(b/cond
:let [x (foo)]
(not x) (do (log "foo failed") false)
:let [y (bar x)]
(not y) (do (log "bar failed") false)
:let [z (baz x y)]
(not z) (do (log "baz failed") false)
:else (do (qux x y z) (log "it worked") true))You can combine `some->` with other threading macros to make it achieve the desired effect, and you can also achieve the desired effect with `if-let` as the post demonstrates, but I believe the b/cond approach to be more readable than both.
Then finally there's emacs which I really don't want to get started with.
http://paste.lisp.org/display/340427
(and, yes, I have tried spacemacs, but found it unpleasant)
For JVM development, I only bother with NREPL because vim-fireplace needs it. For library development, `lein repl` gets the job done. For application development, I copy/paste around just enough code to launch an nrepl server and run the Clojure jar directly with a vendored directory of jar files. No other tools needed really.
Still, the two screenshots there (of my Emacs config) may help convince you to give Emacs a try.
Could anyone provide me with an example of a code that would actually read nicer in clojure than in python ? Make it as arbitrary as you'd like, i'm honestly trying to understand . i've read enough about lisp bluring the line between data and code, so i'm starting to get an intuition of its benefit. I was just hoping to finaly see a real world example, and i'm a bit disappointed.
One final comment, which is often tragically overlooked, in my opinion: ease of deployment. Package management and deployment often sucks in other languages (especially in Ruby). By contrast, Leiningen is a SWEET dependency management tool, and the built-on-Java approach leaves you with a jar that you scp and you're done.
Clojure is a Lisp-like language and would be well suited to taking an input and applying successive transformations to it without keeping state along the way. Meanwhile, Python mixes imperative and functional paradigms, and managing state comes a bit easier. In particular, the Either monad is much more natural an idea in Clojure than it is in Python, and indeed does not suit Python well at all.
You asked for an example in which Clojure would "read nicer" than Python. Consider the task of being given a long string of (natural language) text and
1. splitting it into sentences,
2. tokenizing those sentences into words,
3. attaching part of speech tags to each word.
Implementing in Clojure, imagine we have ready-made functions called splitSentences (which takes a string and returns a list of strings containing sentences), tokenize (which takes a string containing a sentence and returns a list of words in that sentence), and tag (which takes a list of words and returns a list of 2-tuples whose entries are those words along with their part of speech tags). In Clojure, this might look like
(map tag (map tokenize (splitSentences text)))
In Python, this might look like sentences = splitSentences(text)
tokenizedSentences = [tokenize(sentence) for sentence in sentences]
taggedTokenizedSentences = []
for tokenizedSentence in tokenizedSentences:
taggedTokenizedSentences.append([tag(word) for word in tokenizedSentence])
I've exaggerated a bit, but that's the idea.1. A program in Clojure should not necessarily look the same as one in another language. Oftentimes the overall program architecture is going to be very different.
2. For the cases where you can translate certain concepts (example, casting a string to an int) -- oftentimes Clojure code might come across as pretty hideous and verbose.
https://adambard.com/blog/PHP-ruby-python-clojure-webapps-by...
Lisp has been around decades, surely you can find some "real world" examples that you can learn from.
Personally, I started reading the clojure documentation and going through 4clojure https://www.4clojure.com/ , a kind of simple programming introduction problems to clojure
People have solved all the questions, and you can find them online really really easily
https://gist.github.com/SegFaultAX/3607101 for example.
Just do some research, and come back once you have done a basic amount of work, and can no longer say "I've never touched clojure".
Instead, come back and say "I did some research, I worked through some 4clojure questions, and I still don't understand..." then I think you'll get a more productive discussion.
(every? #(= % “success”) (map :status (:state task)))
> Which I’d write like this today: (->> task
:state
(map :status)
(every? #(= % “success”)))
While I love threading macros, I'd argue in this case readability and comprehensibility between the two examples is more or less a wash.Personally, I find the former slightly easier to grasp at first sight. The second could be improved (again, in my personal opinion) by combining the first two lines: it seems unnecessarily granular to say, essentially, "we take a map called task, and then we get the value of its :state attribute", rather than just "we get the value of the :state attribute of the map called task".
Both could be made terser via the set-as-predicate idiom:
(->> (:state task)
(map :status)
(every? #{“success”}))The horrible if-let code the author shows is exactly the kind of thing people use macros for. I'm surprised nothing exists in clojure.core for this, but every project I've ever worked on has something like if-lets, which short-circuits on the first falsy assignment. Everyone thinks they don't need macros and then finds stuff they hate in the language which is easily fixed by macros. This is probably true of the non-mainstream paradigms in all languages. It takes some willpower and humility to learn all Haskell's lens arrow operators instead of just laughing at them.
clojure.spec is nice, and I use it in my current project, but it has performance issues (it is after all basically alpha code) and I'll be honest that I don't _really_ understand what the workflow is supposed to be. If you deal with a lot of side-effectful code you might be using conforms and asserts to keep things sane, but I'm not really benefiting from the generative testing bits. Thinking in terms of generators for code that _is_ pure just feels like another job that I've for some reason forced on myself. Anyway, at the very least, it does make the world of procedural code operating on maps a little safer and saner and you should probably try it.
My complaints about Clojure are nothing to do with the language, which I think is absolutely fine. My main complaint is that Clojure people are smart, and fundamentally believe in small libraries over frameworks, and this means that the Clojure landscape is scattered with abandoned overly-specific libraries, and abandoned and unloved big frameworks. If I could, I would pay someone full time to maintain Incanter, but it's now basically dead, and that means Clojure has no useful numerical computing environment. I am lucky that I can get by with what already exists in Incanter and what's in clj-ml for my analytics work, but nobody is going to quit Python or R for Clojure at this point unless they're smart enough and willing to write all this for themselves.
I happen to do some web stuff and I think ClojureScript is about as good a language as you're ever going to get on the browser. But the other consequence of a community where most people are smarter than you is that the best front-end frameworks are utterly inscrutable. Om has wonderful functionality that I think I would like in my app, but I am too dumb to understand all the moving parts. Reagent is very elegant but is tougher to scale to complex web apps.
I still believe that Clojure is a beautiful and productive language. Year after year I see other languages pile on new syntax while Clojure just remains brackets and functions. For the parts of my code that are pure and functional it's wonderful, for the parts that are ugly and pragmatic I have the full range of the JVM landscape available to me. I get to stay in Emacs, and Cider me custom elisp does everything I need for an IDE. But if anyone asked me to recommend a language to spend their lives in I would probably just say Python and if you do web stuff JavaScript. Bigger community, better maintained open source projects, and just a clearer roadmap to mastery.
They are much more low-level than Incanter, but also orders of magnitude faster, and intended as a foundation for the Incanter-like libraries to be built on top.
They are actively developed.
For some time I expected a Java lib to appear but whatever came was always forcibly distributed (Spark, Mahout) and/or you had to first instantiate an AnnotatedDistributionParameterFactory which instantly makes me want to puke. So there we go, incanter is not maintained but probably the best you can get on the JVM :-/
https://github.com/joshuaeckroth/clj-ml/
The only reason to tolerate a language as terrible as R is that it has such astonishingly broad library support - there is basically no algorithm you can find on Wikipedia that doesn't have some sort of R implementation. Incanter will never have this, and tbh it's very difficult to create outputs as pretty as themed ggplots. For very simple statistical analysis Incanter's okay (and I've seen smart people build lots more on top of it), but even really basic model-fitting stuff isn't built in, and if you're not smart or just don't have the time, it'd be mad to commit to that environment.
It's a shame because structurally it's quite nice, and I did buy into the idea that Lisps were well suited to this sort of work, especially being very REPL focused.
Now maybe I could make the case that I am not distributing the software but I write it in their name and on their bill. In other words, I can use it as long as my job is to write software (for them) instead of creating a product that I will sell.
Still, I prefer to stay away from it as long as possible but I agree it will be hard for some tasks such as decision trees which are unfortunately a very nice fire and forget algorithm.
There are people claiming that you can do most of classification and regression using logistic and linear regression, so I try to stick to that as long as possible and incanter allows for that.
Its a 'web framework', but really its just a module system. It tries to be a get started fast and still be able to do really complex stuff kind of framework.
I am running into the same issue in Production and I hope to use Arachne to make this easier in the future.
David isn't involved into it for many years, I also don't have free time last 2 years due my involvement into very big projects.
I would happy to accept your contributions into Incanter's development...
I agree with most of the author's opinions about the use of Clojure except that personally I dislike using multimethods.
(every? #(= % “success”) (map :status (:state task)))
actually considered crappy code? It looks perfectly natural to me, but my Lisp background is more generic.The threading macros are more useful when you have more nested code. For example:
(->> coll
(remove nil?)
(filter :available?)
(map :stock)
(reduce +))
Is perhaps a little easier to read than: (reduce +
(map :stock
(filter :available?
(remove nil? coll))))The only thing that surprises me about both of those examples is that :state is a collection of things not just one thing, but I imagine if you're more familiar with their structures that would be less of an issue.
I just grabbed an example at random there. It's not really an excessive example of nesting, but, at the time I didn't grok threading.
I think the data being manipulated is a cloudformation response or something, so, the structure isn't something I'm in control of.
I actually changed it from `(:status (:status ...` in the original, which is even dumber naming, so that it wouldn't look like a typo. Real life programming is thoroughly unglamorous I guess.
>If I were going to give you a quick summary of what our codebase is like, I’d say it’s procedural code that manipulates maps.
>One thing we do use more extensively are multimethods. We use this to dispatch asynchronous jobs in our workers, to fan out to different handlers from webhook endpoints, and to make transitions in our deploy finite state machine.
>... sometimes you find yourself boxed into writing non-idiomatic Clojure. A good example of such a situation is dealing with a morass of heterogenous functions that can return error codes.
Languages based on different paradigms excel in different situations. Lisp-like languages such as Clojure (and, more generally, functional languages), have abstractions perhaps most well suited to "pipeline" situations - take an input, apply transforms A, B, C, ... in sequence until the output is returns, bailing out if there is an error in some stage. Multimethods, which the post mentions, are a good example of this: given input X, send it off to Y based on some conditions. In software applications which have to do this sort of work, it's a good fit.
If there is complex state to be managed under many different conditions, however, these paradigms can be cumbersome to deal with.
>It is unclear to me if the category theory would still be a win on a less experienced team. I have a long history of being skeptical of things like this, but it has improved our lives recently.
The author mentions realizing that the Either monad well embodied the nature of the algorithms being implemented. Speaking from my own experience, I would suggest that teaching a team implementing the sort of software it seems the author's team was implementing would be a worthwhile investment - but only if it was reasonably clear beforehand that those abstractions would be well suited to the problem at hand.
Perhaps the most important skill to train a team up on, in my opinion, is the ability to recognize the type of problem at hand well enough to see what abstractions would be well suited to it and thus what language to use. In other words: it is very important to choose your tools well.
If I could offer some advice to the author (and others in similar positions) regarding the question of investing time in training a team in any set of abstractions, be it concepts in category theory for the sake of Clojure or something else, it would be this: analyze the nature of the problem at hand, make a decision on what tool (language, framework, whatever) you feel would be best suited, familiarize yourself with its world if you haven't already, and then have a meeting with your team in which you concisely explain that analysis and the reason behind your decision. Mention why other choices would not be as well suited, but, CRUCIALLY, also give examples of situations in which the other tools you considered would have been the best solution. If you have and reservations about your choice, explicitly state them. (In fact, you may be surprised by someone on the team having had experience in the domain at hand but not having mentioned it because it hadn't become relevant yet!)
This will not only teach the reasoning process but also give the team some information about what alternate approaches to consider when faced with a problem unlike the ones they have seen before, especially if the team is junior.
Furthermore, sometimes one discovers that the nature of the problem being worked on is, in fact, different from what it was originally understood as. It is a lot to place on one lead to always be vigilant about that, and self-doubt can creep in. If you transparently expose your thought processes to your team, you may find yourself surprised by eventually having a team member saying to you, "don't you think that this other tool/approach you had considered may actually be better suited to <situation> now"?
It is a lead's job to lead, but it is also to share as much of their knowledge, experience, and reasoning process as possible with the team. Sometimes we go down the wrong route at first, but everyone on a team should understand the why and what of every point along the way. As a lead, I make it my goal to train everyone under me well enough to replace me and to feel comfortable enough to voice any commentary on the decisions I have made.
I was going to go on, but I think I went off on a bit of a tangent. I'll leave off here.
That's very true. Glad to find out I'm not the only one who perceive it this way.
This is the reason I believe multi-paradigm languages are more practical(or better suited to solve real-world problems) - as they allow you to utilize different approaches for different problems. At the same time, being too wide/generic seems to lead to lacking of some expressiveness single-paradigm languages could provide (like lack of native pointfree style).
AHAHAHAHAHAHAHAHAHAHAHAHAHA!
Java getting practical considerations right. Oh, I so needed a good laugh.
Let's start from the basic:
No unsigned type. Bit/byte manipulation code written in Java is garbage when written by experienced programmers. When written by inexperienced programmers it's a nightmare.
Ever looked at the "class pyramid" monstrosities in Java? That includes the two primary GUI systems.
Ever debugged all the stuff coming from the type erasure that you are stuck with doing in Java?
And, let's not even gets started on the people who think they can write concurrent code (Hint: if your Java concurrency solution doesn't involve java.util.concurrent-you're doing it wrong.)
While Java gets many things right, it's ability to prevent people from falling into pitfall after pitfall is NOT one of them.
I would argue Clojure does a much better job at this simply by the fact that immutability is much more of a default choice.
Please don't post like this here. It degrades the community, takes threads into flamewars, and discredits what you're arguing for.
We detached this subthread from https://news.ycombinator.com/item?id=14008093.
Your points do have merits in showing Java is imperfect. I would even say that it is deeply flawed. I would not agree it is impractical.
The simple fact about the practicality of Java can be shown in the millions and millions of lines of code powering much of the boring business infrastructure in the United States. If Java disappeared overnight Banks, Insurance companies and much of the rest of the financial sector wouldn't open the following morning (And then we look at our phones).
Honestly had the same reaction as above. It's a hilarious laughable claim that the Java community gets this nebulous "practical concerns" thing right. Agree it's not a helpful response here but I don't blame them for falling to it. It's not exactly "forced". Probably better for @shit_hn_says though. Edit: the much less nebulous claim by 'tlarkworthy is much better.
Without Java the US military is in trouble, a majority of cell phones stop working, the printer in the office I am now fails, Netflix has some level of problems and more and more... It is debatable that Java is the single most widely deployed programming languages. I say debatable, because many of those JVMs were written in C and many of the kernels (when they need a kernel) JVMS run on are written in C, where do we draw that line.
If Java were not practical this would not have been possible. I suspect it would have looked more like Cobol, with shops of experts being able to run and maintain it, but failing and migrating away as time went on.
[0] I've heard from COBOL forums that the amount of new COBOL written each day and existing in the world keeps increasing; they've even got Object-Oriented COBOL now. I don't think it's unreasonable to assume that if COBOL had the same market pressures Java does (plus university pressure) and if the world was basically stuck with it in the same way we've had Java this century that we wouldn't be able to push it to service our needs.
[1] http://wiki.c2.com/?TheSourceCodeIsTheDesign "I believe it is time to explicitly state the long held secret of software, we do not need to do design; design is ineffective and costly."
And then asserted Java as a counterexample that is better about this.
I'm sorry, but anyone who has had even tangential exposure to Java codebases would find this equally laughable.
Java codebases are by far the worst codebases I have ever had to wade through. They even surpass C++, and I loathe C++. I presume it's the fact that you can cut/paste StackOverflow until something works in Java while C/C++ blows your foot off a lot earlier.
To be fair, I have not had to wade through large COBOL or assembly codebases, so my experience may be skewed.
And, if I prioritize your list, I arrive at C#/CLR, not Java/JVM.
Of course pretty much everyone's concurrency solutions do involve j.u.c since Java 5. They are truly powerful language facilities.
I wish I were joking. HN is not like the real world.
This is just like "git/hg is superior" arguments. Those of us in the real world are still happy when we see a business actually use source control of any form.
When was the last time you saw wait/notify? I'm willing to bet that was a long time ago.