Why Clojure? (2018)
briansunter.com
briansunter.com
The biggest problem is that it's very hard to picture why, at the end of the day, all the choices that went into Clojure come together into a productive whole for building real-world software. It's a really nice mix of terseness without preventing clarity, simple lightweight modelling paradigms, interactive development, easy access to multiple cores, and all on top of the JVM with its enormous ecosystem. It's not as Lispy as other Lisps. It's not as pure as other functional languages. It doesn't have a fancy type system. It doesn't have native performance. But it gets stuff done and it does so fairly elegantly in most cases. I've found it a really solid career choice, there's really very little that you can't solve in a satisfying way. Plus whatever you think about parentheses, Clojure syntax basically hasn't changed in 10 years, most new features are just libraries of new functions and macros, and for me that validates that it's the correct approach.
Lisp is a fascinating and very elegant language and every developer can learn something from it but I gave up waiting for the lisp revolution a while ago.
Google Erik Naggum for an extreme example of this syndrome.
On the other hand, you do have languages like XML which is basically a much more cumbersome syntax for s-expressions.
For all practical purposes Lisp doesn't even have a syntax.
The real issue is with programmers who are at beginner and advanced beginner levels all life.
This problem existed with Perl too. Agreed that a lot of text manipulation tasks that existed before the data exchange standards like JSON and XML came around don't exist anymore. The use cases for Perl have reduced. But the overall point stands of course. People are willing to write 100 classes to extract a string from data, than learn to write 10 lines of regex code.
If the only thing you are willing to learn and use is a for loop, if statement and function/class syntax. Anything will look off putting.
Just see how many programmers struggle with concepts like concurrency, pointers, recursion etc. It's just that these people struggle to hold non-trivial concepts in mind.
How can you weigh up the pro or con of a syntax when you haven't used it much? I couldn't tell you whether APL syntax was any good.
For the record I like lots of languages - dependent types are awesome. I find that the language I reach for will mostly be a function of the problem space.
Just an FYI, certain Lisps have static typing as well.
Most of the programmers today have negligible experience with Lisp.
It's also not really elitism. I'm not saying people are stupid. But people take great pain in avoiding reading documentation and understanding anything in general. Which is why I gave the Perl example.
Reading documentation for 30 mins could help you understand the issues and help you fix them at root, instead of hours of Googling. For some reason people do the latter, not the former.
Bad input doesn't always mean that the regexes don't match!
Regexes have a very happy home inside lexical analyzers for tokenizing languages. Lexers define what is correct input and match every case with some regex pattern. If there is no match, then the input doesn't contain a valid token: the lexer can loudly complain (logging an error that can be treated as fatal by the overall compile job), drop an input character and try matching again.
The typical regex solutions in Perl (Awk, ...) scripts go like this: "look for this flimsy, minimal regex somewhere in the stream and assume it's the right thing, then match this other regex in the same line and---woo hoo!--that's our item. Oh, false positives, schmozitives."
Basically, if we were to pin it down to a single difference in principles is that using regexes for searching for something small in something large is different from matching an entire input in its totality (and then getting at the desired parts).
Searching is the quick and dirty thing that's easy to reach for.
• Parens
• Curly braces
• Square brackets (why are they here, if Lisp is just s-exprs?)
• Colon-prefixed symbols
• Quote-prefixed lists
• Functions with names like >!! and ->>
That's pretty obviously syntax. You could argue it doesn't have keywords in the way C-type languages do, but magical functions that are defined as part of the language and should never be changed for all intents and purposes might as well be syntax too. So in the end the difference is kind of academic.
The argument here is lisp has a simple rule. The first element of a list says that what is to be done, the remaining are its inputs. That's really all there is to it(mostly). Using () for everything was really a kind of overloading, which is why I guess they used different opening and closing characters for different contexts.
To be precise, those are just function names, not syntax. But even Clojure having a more complicated syntax than Common Lisp or Scheme is much simpler than Java, for example. Instead of f(x, y) it's (f x y), essentially.
An executive I previously worked with commented one day "Every small language's following is a little bit cult-y. Clojure's is more so than most."
Personally, I think the technology itself is fine. The ecosystem, from my exposure to it, has a few warts. It's somewhat small, which I expect of what is still a niche language. The killer issue for me was the memes that slowed and discouraged the development of useful frameworks in favor of gluing together ad-hoc sets of libraries.
Of course, it may have just been that I was working with a batch of Clojure devs who were in those jobs because they were sold on the glories of functional programming purity. Hard to say. They were better than the Haskell shop I worked in, but not as much so as I would have liked.
When you're writing public-facing web services and forget about security while pulling together your stack, this ignorance can become very dangerous!
So there's that. But I also do find that Clojurists have not been immune to some of the snootiness that seems to infect most functional language communities. And I wonder if some of the reaction comes from that, combined with Clojure maybe bumping into the rest of the world more often due to its identity as a JVM language.
It's maybe the quintessential Cassandra language, though. As a dynamic functional language, Clojure is a great complement to Java. And, while I've also never really been comfortable with macro programming, compared to Java's accumulated decades' worth of XML and code generation and bytecode munging and and dependency graphs that have been sent through a wood chipper by annotations, s-expressions and macros strike me as a delightfully clean and maintanable-looking breath of fresh air. But all that stuff also makes it a bit of a tough sell among the kinds of folks who haven't long since abandoned Java for a language like Ruby or Python. A bit like trying to sell tie-dyed shirts out of a VW bus at a Barbara Streisand concert.
Likewise, the sort of people associated with Streisand concerts -- mainly well-to-do, cosmopolitan, white (especially, but not exclusively, Jewish) women -- would not trust a shaggy man with a rusting VW bus with their money, let alone be interested in his handmade wares when there are perfectly good boutiques on Fifth Avenue (or Rodeo Drive) to patronize if they are in need of clothing.
OP was trying to imply that the people who still use Java have fundamentally different tastes than dynamic-language programmers, and so evangelizing Clojure to them would be a fundamental mismatch in tastes, like a hippie trying to sell to upscale white women.
Frankly I'm just sick of hearing "Why you should use Clojure" over and over again. Posts about other languages center on some problem that language solved for them, or some problem they have with it. Clojure posts are always cold-call cheerleading about how good it is in a general sense. Tell me something cool you did with it. Tell me how it saved you a mind-blowing amount of work. I'm not interested in hearing for the Nth time how someone think it's an overall excellent choice.
It's kind of like when I recently evaluated Visual Studio Code (for the (1+ n)th time) to use as my default editor. Everybody I know at work uses it, and for a couple years there Hackernews made a collective O-face every time a new version dropped from Microsoft. So it must be good, right? Well, it was different enough from Emacs to force me to get used to an entirely different workflow, but not enough better than Emacs (really, for my purposes, not better at all) to justify making that enormous transition. So back to Emacs I trudge. A reasonable person may make the same call about transitioning from Visual Studio Code to Emacs when they hear advocacy from Emacs users like me.
That's where Clojure is for me -- in this limbo of it sounds good but it's not really worth it for me, personally, to switch. Plus its community has a strong contingent of douchey advocates -- not quite at the level of say, Rust or Newlisp but getting up there -- who act like Rich Hickey was God's gift to programming and make me think "So that's what Smug Lisp Weenies sound like to the rest of the world." And that further cements me in the realm of "hard pass" w.r.t. Clojure. Call it an irrational, emotion-based response.
So tell us what your workflow is, so that everyone can benefit from it.
It doesn't do much good in just saying Its magic, trust me and then leave it there.
That said, there are a lot of fundamental differences that might be painful for someone transitioning from Emacs. First obviously is just keybindings, there are great packages to handle most of the basics but like with any new editor it's a faff to get everything right over time.
Second is that is has lots of special cases of windows, which are hard to manage. In Emacs everything is the same, just a buffer. Your shell, your database client, your code, all buffers. You decide how all this is laid out and have absolute control. The same navigation, manipulation and search functionality exists everywhere. When you get an autocompletion of some text, it works the same everywhere, and the same navigation and search functions work in those menus. In VSCode, you have some text editor windows, but also tab groups which behave subtly differently, and then the panel and sidebar which are both different. I don't know how to 100% stop these things popping up, and the terminal seems like the only one available, so they're a regular annoyance to someone who is used to owning every character of screen real estate. Extension developers don't create extensions from the point of view that users want to consume textual information like everything else. It's not bad or wrong, it's just that Emacs works at a lower level of abstraction. It would be entirely valid to point out that VSCode's level of abstraction enables a more vibrant extension ecosystem.
Third is that there are modes in Emacs that are much more mature than VSCode. Calva for Clojure REPLs is pretty weak compared to CIDER in Emacs. All the git modes are inferior to Magit, even the direct clones of Magit (which are good and tastefully done). All of these are very actively developed and will get better, they're just not there yet.
Overall Emacs has fewer primitives, but they apply everywhere to everything, and I find that a much more humane environment for the kind of work I do (it's why it's a nice environment for a Lisp, which has the same philosophy to code).
This is a typical cult pattern: the “esteemed leader” who is godlike perfect and can do no wrong. Having said that, in all fairness, Rich Hickey is a great presenter and I learned a lot about functional programming from watching his talks. I completely disagree with him on types but still learned a lot.
This gets even worse with languages where indentation matters (Python, and the horrible abomination that is YAML) — which aren't even auto-indentable, because the editor has no idea what you actually mean. I'm not sure if you can avoid going insane with those.
What Lisp helps you is grokking actual operations of computing and, especially when all you have is a really dumbed down algol, opens you up to more programming methods and techniques. All of that happens on layer high above syntax.
Computing in the theoretical sense, right? I've always heard the opposite of this claim: Lisp abstracts away how computation is done on actual hardware. Isn't that what the famous Alan Perlis quote was referring to?
Specifically, ANSI Common Lisp is equipped with a function called DISASSEMBLE, and on many implementations it will provide you not only with plain assembly dump, but also with comments regarding said assembly, for example a comment specifying "we're calling X here", "this is handler for single parameter, and this is for multiple parameters" (in case of optional arguments to function)
You just select a syntactic unit with opt-up/down, then u either type over it, copy/cut/past or press left or right to go to the beginning or the end of the selection, hence using the selection as an intermediate step achieving AST-level navigation.
And of course you can teach Emacs to behave similarly, using the Expand Region (https://wikemacs.org/wiki/Expand_region) + some hacks, but I agree, that smartparens also solves this problem quite well.
UPDATE: also use avy-jump for emacs or acejump for intellij: https://plugins.jetbrains.com/plugin/7086-acejump these in combination with expand/shrink-region operations are a significant productivity boost, which is easy to learn and teach.
By contrast, I never have these kinds of semantic issues in whitespace significant languages like F# - I make mistakes that I might make less often in Scheme, but the compiler usually sets me straight since it realizes a branch isn’t returning a value, etc.
In my view parentheses versus whitespace is really a judgment call based on how your eyes read the source code. The crucial thing is having something like s-expressions.
Having said that, there's some disadvantages to the Lisp syntax as well, rightward drift due to constant nesting is real, and can make readability and even some edits a lot more annoying, whereas the flatter structure of other syntaxes doesn't have this problem as much. I still find its pros outweighs the cons personally, but your mileage may vary.
And I think that a job where you have to constantly switch between lisp and non-lisp styles would be a lot more frustrating than just using only one style and getting used to it, so I can see your pain there.
Anyway, it's way harder to adapt to "parenthesis before the function name vs parenthesis after the function name" and "semicolons after every statement vs. never use semicolons" than "punctuation is all parenthesis and commas (or newline and space as in normal Haskell) vs. punctuation uses the entire keyboard".
Meaningful vs meaningless indentation is also one of the things that don't give me any problems at all to adapt.
Overall, I don't think you need the full power of paredit when programming. For any given language, your editor movement commands understanding what tokens in this language look like will usually already be sweet and enough.
When I edit the non s-expressions languages I mentioned I do it in IntelliJ, sometimes with the vim bindings.
#include <iostream>
int main() {
std::cout << "hello, world!" << std::endl;
}
If my cursor is after "main", then M-f will move my cursor to after "std". It completely ignored "()" and "{". Another M-f moves my cursor to after "cout". Here it ignored "::". Another M-f moves the cursor to after "hello" ("<<" and the quote-mark ignored), another to after "world", and another to after "std" (quote-mark and "::" ignored) etc. It behaves similarly when hitting C-Backspace, where sometimes half a line disappears suddenly, because it was punctuation-heavy. I think once I stumbled upon code where it deleted several lines. On the other hand, when I have a variable like "big_number", then Emacs will happily jump into the middle of it. Even though it's an indivisible token in the eyes of the language's lexer. Of course there is a well-known hack of adding the "_" character to be recognised as a "word character". But it doesn't solve the issue really. The issue is that forward-word uses just one set of word-characters, instead of several sets of characters/regexes for different syntactic categories, which would enable jumping first to "cout" and then to "<<".My approach: learn 2-4 navigational moves first for a while. Then add more nav and editing moves at a later point.
As with everything: deliberate practice leads to mastery. At some point it becomes apparent that the weird syntax is a feature that has huge upsides (another one being macros for example).
Now paredit has been around for decades (1991 ? I forgot) so it's not like it's rarity.
Now that said, a little bit of paredit-fu allows for some funky coding sessions.. you can swap sexps, move blocks up down the tree .. you can even process the sexp with some elisp code in emacs.. it's very very swift.
What annoys me is the people who never expanded their knowledge beside a few paradigm .. they'll stick with python only or js .. or cpp. They're stuck onto a few libs and syntax.. it's a pity.
I maintain an up-to-date fork of Parinfer JS here: https://github.com/oakmac/parinfer
The Rust implementation is also popular and actively maintained: https://github.com/eraserhd/parinfer-rust
However, I wouldn't use it on any projects with more than maybe 3 team members for mainly three reasons:
- Very steep learning curve despite the seemly simple syntax.
- The community is shrinking with many high profile OSS projects being effectively abandoned, while most other languages' communities are growing.
- Clojure is just bringing too much freedom for most average developers.
If it's just me, I will have no hesitation and always pick Clojure as my main language.
In fact, nearly all of the claims made about Clojure here can be made about haskell more strongly.
I've half a mind to do a direct comparison on every point. I'd be interested to hear the author's thoughts on the similarities and differences.
[^1] With the obvious exceptions of s-expressions, java/js interop, and "subjectively good design".
Being able to consume any JVM library makes Clojure usable in many more professional settings than Haskell.
Haskell has decent interop with C/C++ languages, but certainly nobody uses haskell because of that.
The problem with function language adoption is people keep pushing Haskell likes it's anything more than at its essense a language exploration research project.
The fact that you'd have a comment that ignores a language because it wins by default because it practical is more proof that people evaluating languages are speaking two different... well languages.
Some are looking for what they feel have the coolest ideas, others are looking for languages with very cool ideas, much better than what they're using today, and can still be effective/ productive in. Haskell is cool if you want to think about or play with ideas. Clojure is cool ideas, and can still be productive in (ie full modern library support).
As well if you don't know, F# falls into that same bucked of cool ideas, better than your avg imperative language and can still be very productive due to language and .net library.
We're trending to a point where any non-systems language is just an exploratory language unless it's build on JVM or .Net.
Otherwise the produvtivity loss from lack of libraries is almost impossible to overcome from any possible produvtivity gains from the language.
I certainly hope this does not turn out to be the case.
Both the JVM and .NET impose a certain type system on all their client languages. Those languages have an option to embrace it, like F# or Kotlin have, or to struggle against it, like Scala has, but they don't have the option to truly pull free of it. Not without shutting out effortless interop with the rest of the platform, and, in doing so, undermining the whole purpose of being on those platforms in the first place. And, since most the interesting developments in programming languages center on type systems, that implies that huddling together on the Big Two bytecode VMs stifles a lot of really interesting innovation.
Not disagreeing at all. Interop with a library means interop/conformance with its type system. Which means if you want languages with new / innovative type systems, someone will need to build a library system to resolve this.
Either an untyped library system as large as the .NET or Java library systems, or a library that has a trivial way to tack on type transformation of some form so that each language that interops with it can minimally add a type translation layer between the two. What that looks like, I'm not certain.
But as long as engineers need to be productive, they need a robust modern library system. No new lang will get adopted if the lang author also needs to build up a complete library system, so it must be a general component.
A C-style ABI, by virtue of being the least common denominator, is probably the best bet for re-usability across paradigms. Higher level languages would want to write idiomatic façades, but they already habitually do that anyway, even on higher-level platforms like Java and .NET.
And I think that deterministic memory management is probably also a pretty important feature. You don't want your libraries all bringing their own clever ideas about object lifetimes and such. But you also need it to be very reliable; a real danger with inviting libraries written in C and C++ into your process space is that they are liable to corrupt your memory. Rust's affine type system seems like a big step in the right direction here.
Similar thoughts for the error model. I don't have any particular complaints about exceptions, except that you don't want to be bleeding them on an external API, because that ends up being another spot where languages can fail to mesh.
What's missing, though, is that there is no good cross-platform standard for libraries that work with the C ABI (neither in source nor binary form) for other languages to plug into. So that's where I get to thinking that Rust might be closer to (if not exactly at) the mark than C is.
1. Using the JVM for its capabilities, like the JITC and GC engines, which are very powerful.
2. Using the JVM for Java interop.
3. Using the JVM for generic cross-language interop.
The nice thing about the JVM especially now with Graal/Truffle is it lets you explore and choose almost any point on the spectrum between "we're totally alien and the JVM is just our runtime" and Kotlin-esque "we are basically Java v2 with perfect interop". Modern JVMs are capable of running languages as diverse as Haskell, JavaScript, Kotlin, Ruby, WebAssembly, LLVM bitcode etc. Obviously if you're coding in C or Rust and running it on the JVM via Sulong, your interop is going to be limited to creating objects and invoking simple functions on them. If you're in Ruby or Python your interop gets better: you can expect automatic translation of things like Java-world collections to a vaguely native-language like collection via the Polyglot interop layer. And if you're Kotlin then you don't even create your own collections at all, just use the JDK standard library.
The point is, you don't have to use the Java bytecode type system to interop with Java or other languages anymore. Graal has fixed that. Your language can have any arbitrary semantics and it will still be JIT compiled, GCd and it can still call into other languages. The closeness of the type system is now a choice you make that trades off ease of use of Java libraries vs whatever divergence you want to have.
As long as you don't mind either poor performance, or paying Oracle a bunch of money for good performance.
What all the fancy marketing takes pains not to say directly is that GraalVM is shareware. The open source version is just a basic version with some teaser features to get you interested, and, in classic Oracle fashion, the price of the full version is, "If you have to ask, you can't afford it."
I have no real objection to that model for other products, like BerkeleyDB. But I wouldn't want to build an entire open source ecosystem on a foundation like that.
Moreover, GraalVM is open source. Please don't try and redefine basic terms, as otherwise by your logic Linux would be shareware because Red Hat sell a better version of it, Android would be shareware, Chromium would be shareware etc. Making an open source product and selling a better version that isn't is perfectly legit. If you don't believe that, how do you envision platforms be funded?
As for the pricing, it is or was on their online shop. You could buy it with a credit card. Seems they have a problem with their store right now, but I've seen public price lists in the past. It's expensive but not at all "if you have to ask you can't afford it".
I think my point stands regardless of any quibbling about terminology. I've got no objection to some sort of free-however-you-capitalize-it-but-with-paid-premium-features model in general. But baking such a business model into something as fundamental as a cross-platform ABI standard sets a really bad precedent. Having lived through the late '90s, that sort of thing immediately brings the phrase, "embrace, extend, extinguish," to mind.
I'm glad to hear they've started publishing a sticker price. My memory isn't what it used to be, but I believe that wasn't the case when I evaluated it.
You have to distinguish between technical standards and implementations.
JVM bytecode is a technical standard for a portable, cross platform, strongly typed ABI. Anyone can implement it based on the docs.
Truffle is an open source API with a clear specification for creating interpreters. Anyone can implement it based on the docs, although there's no reason to do so given its permissive licensing.
Beyond that it's all implementation: the Graal JIT recognises when it's compiling Truffle interpreters and does it in a fast way, but it doesn't have to and Truffle interpreters can run on any JVM including those that pre-date Truffle's own existence. They just won't be as fast. GraalVM EE is a for-pay version that does an even better job, well beyond state of the art, but the open source free version is no slouch and Scala code can go faster by e.g. 20% or more even with the open source version.
There is ALSO the native-image tool that compiles things to native code ahead of time. Many people use the word Graal to mean this, because it's the most eye-catching thing in the suite, but that's not correct. It's called either native-image or SubstrateVM. The open source version of this produces code which is slower than regular HotSpot runs but has no warmup time and starts instantly. The speed drop is due to losing profile guided and speculative optimisations, it's not a pricing issue. The EE version of native-image that you pay for has various other features like the ability to gather profiles using HotSpot and then use them when compiling to win back some of the speed, but not all of it - you can't really beat Graal JITC on HotSpot for peak performance. The EE version also has some other useful features for security and sandboxing.
In other words, the Java/Oracle guys seem to be doing exactly what you'd want to see: there are standards, specifications and then implementations - all clearly separated. There are open source implementations, and better ones with support contracts that fund the development of the whole shebang.
On the other hand, there are two "child" languages of Haskell for the job: Elm, which is a frontend-focused language, and PureScript, which compiles to JS and is designed for that use case.
- GHCJS and purescript are powerful, but the learning experience might be steep[1]
- Elm is an excellent entrypoint into ML programming in the browser. Solid story for new users, and a great standard library for interactive web applications.
- ClojureScript differs from Elm in that it embraces its host, with all its power and all its wrinkles. Writing Elm is mostly a smooth experience. Read the guide[2], then you can actually build a web app.
I've spent the most time with Elm. Other people might have different experiences.
[1]: a few years since I tried, might be better now. [2]: https://guide.elm-lang.org/
> nearly all of the claims made about Clojure here can be made about haskell
, but I'm not sure about `more strongly´.
## DX with Haskell (and ML friends) I miss in Clojure:
- Harder to do sound, up-front design with data types and module interface, where implementation becomes almost trivial after when the type signatures make sense
- Consistency checks from compiler / type checker
## DX with Clojure I miss in Elm/Haskell:
- REPL ergonomics, where every action I could think of makes sense as a REPL command, leading to very small incremental pieces.
- Excellent default data structures with literal representation and serialization (EDN)
I'd love to read a point-by-point discussion of the article sections comparing Haskell and Clojure, though that's much to ask for in the comments field.
- REPL ergonomics, where every action I could think of makes sense as a REPL command, leading to very small incremental pieces.
This is the only point I would dispute. The GHC interpreter `ghci` is very powerful and offers a lot of the same upsides. Beyond this, the language server offers in-code evaluation with "-- >>> expression" syntax, which is a cool new step towards fast looping UX. Clojure is certainly great at this, but I'd say Haskell is not far behind.
Within the Clojure community, there's a perception that the Clojure REPL is one of its strongest selling points[2].
Are you using the REPL actively when developing?
Edit: really curious about the "-- >>> expression " syntax! I might have to give Haskell another go.
Edit 2: Example of this interaction in practice with VSCode[3]
[1]: https://github.com/chrisdone/intero#readme [2]: https://clojure.org/guides/repl/introduction [3]: https://github.com/haskell/haskell-language-server#features
I enjoyed developing Elm with TCR[1] a while back; also with an editor + a type checker (plus the revert part). I recompiled my whole source on each save; incremental recompilation should scale better.
[1]: https://medium.com/@kentbeck_7670/test-commit-revert-870bbd7...
But AFAIK, GHCi doesn't have any state saving functionality (I don't think the dev environment is even integrated enough for any to work), and the little code edition available is entire function only. The Haskell REPL is entirely oriented into the "you write the code on the editor -> you evaluate it on the REPL" way (again, AFAIK, the GHCi manual is huge).
That said, yes, it's a really seamless cycle of you write your code on the editor -> you evaluate it on the REPL, and yes, Haskell requires doing that very little, most problems stop at the type checker. Haskell does really not afford interactive programming, but I wouldn't classify that as a problem.
The C#-style object model is arguably an advantage for certain purposes. I always feel guilty doing it, but object oriented domain modeling really does feel more natural for certain classes of problem.
On the downside, F# lacks higher kinded types, and you're absolutely on your own when it comes to making sure things that should be pure are actually pure.
I won't dispute that it is more alien to most programmers, though, and that laziness and monadic IO require a bunch of work to get used to.
> I feel about them the way I do about switch statements - they're brittle and inextensible.
That is not the case in a language like F# or OCaml. I do note that F# was introduced only slightly before Clojure was, but pattern matching provides nicely extensible functions and are anything but brittle in those languages. Also, active patterns in F# allow one to extend the pattern matching functionality.
> The binding aspect is used to rename structure components because the language throws the names away, presuming the types are enough. Again, redundantly, all over the app, with plenty of opportunity for error.
I'm not sure what he means here. Again in a language like F#, names of the data aren't thrown away. They are pattern matched against, only being "thrown away" to do actual calculations. Nothing is ever lost where the data came from. For example:
type Shape =
| Circle r
| Square s
let area shape =
match shape with
| Circle r -> System.Math.PI * r * r
| Square s -> s * s
There's no confusion here. In fact, pattern matching in a language like F# allows one to completely remove the possibility of error. For example, this really shows off in parsing applications. Once your parsing function returns a type that can be pattern matched, it's extremely difficult to have an error in the pattern matching sections of code. These are typically the most robust parts of the application.> I'd much rather use (defrecord Color [r g b a]) and end up with maps with named parts than do color of
real*real*real*real,
> and have to rename the parts in patterns matches everywhere (was it rgba or argb?)I don't understand this either. In F#:
type Color = { R: float; G: float; B: float }
let colorFunction { R=r; G=g; B=b } = r * g * b
No names are thrown away. Also, the comment on rather using maps seems to assume the data type for every element of the data structure is the same. How do you just use maps when the underlying types of your record aren't the same?What is meant by this statement is that pattern matching violates the open/closed principal. If you add a new type to switch on, you need to update all the pattern matching code in the whole application to account for the new type.
It’s one of the two sides of the “expression problem”[0] (the other being object oriented polymorphism).
Clojure’s approach to this is to use “multi methods” which is sort of a “pattern matching”/“strategy pattern”. You are free to add in a new implementation of the multi method without having to update existing code
Here is a great post that talks about Clojure’s approach to polymorphism and covers multi methods in detail: https://aphyr.com/posts/352-clojure-from-the-ground-up-polym...
Isn't the open/closed principle more of an OOP design concept? In a statically typed language like F#, I want and expect to be notified what functions I need to update when I add a new type constructor to an existing type. This isn't a problem and is welcomed. Just because one updates functions doesn't make them brittle or inextensible. By only adding a new pattern matching branch, one is able to extend a function without affecting the other branches. However, this is getting into the statically typed nature of F#.
I think that link explains the expression problem rather superficially. It says you just need to add a new class, but neglects to mention that that also entails adding the new method overrides. So simply saying the OOP way is easy and functional programming is difficult when adding a new type is not really accurate. Same thing for adding a function in the functional programming paradigm, because it neglects you need to add a branch for all types. In reality, OOP inheritance and functional pattern matching are simply transposes of each other, and I'd argue that one is not really necessarily better or worse than the other. They're simply different organizational methods of how the data and functions on the data are organized.
Suppose you have a type `colour` defined as `RGB(int, int, int) | HSL(int, int, int)` and then you add representation as CIE. Then having to update each match on a colour is absolutely a feature not a bug. If you miss some out then your code will be wrong.
On the other hand, suppose you have various ways of serialization (JSON/XML/s-expressions). In this case, it would probably be nice if you could add a way to serialize to e.g. protobufs without having to jump around your codebase and all its clients fixing type errors. But in most languages of the kind we're discussing, you can do! You just have to represent the different serialization methods in some way other than a variant type. For instance, in OCaml you could just use classes and inheritance (although in practice you probably wouldn't because the language provides nicer tools).
(defn color-function [{:keys [r g b]}]
(* r g b))F#'s pattern matching is a clear example of it done correct.
If I want to get more into FP, is there any strong positives/negatives for either? I must say though that after using Racket for a bit, I am a fan of the parens. Makes expressions crystal clear.
Clojure is actually designed as a functional language and not as multi-paradigm as Scala.
I recently worked on a Java 11/Spring app and there is still plenty of pain in Java to go around when compared to other, more terse languages.
Yeah. It was pretty clumsy before it got less clumsy - and it makes me sad because a lot of the graph-based databased technologies glomed onto it pretty early on - and I mostly like where they say they are going. And their base compile Suxxor and they messed up their community with the new compile Python3 style.
I guess Kotlin is cool and clean - which is good. And has no ecosystem - which is bad. But stapling JVM language together for ad-hoc purposes is what we have learned how to do, neh?
For my part, I'm probably going to go back to high performance renderers and embedded systems. Like the man said bad in the day: "You can all go to hell - I am going to Texas". (Unfortunately I have been there for 30 years since I learned that I hate it.
I've used the build tools of almost all the major languages (JS/TS, Rust, Haskell, Kotlin, C etc) and I don't honestly see any which are particularly easy.
I would definitely agree with your point that most build tools aren't easy! I've used maven and grumbled about it, and I'm mostly sold on scripting my build in the same language I'm using anyway, but in my long time on the JVM, it's always been the gradle and sbt projects that end up with inscrutable and hard-to-follow build scripts.
The sbt REPL also regularly breaks existing build scripts by changing how args are passed/parsed, or even how terminal color support works, and I just get sad every time I see a new, unexpected error from one of our CI runs.
Take for example the point on syntax - is gradles groovy receiver syntax not just as esoteric as operator overloading?
My question was what makes SBT uniquely bad when the topic of build tooling comes up that people immediately comment how terrible it is.
I even went to a lot of effort to master SBT. I've read through the official docs numerous times, and Josh Suereth's Book https://www.manning.com/books/sbt-in-action, and I still feel like I don't grasp it very well. I'm just not smart enough for SBT.
Scala 3 pros:
+ Higher-Kinds + Type-classes + Decent-ish hygienic macros + Decent OO model with mixins + for-comphrension + Compile-time evaluation + Laziness + Currying
It's pretty good at all of them - but not quite.
If you're used to Haskell you'll get annoyed at the fact that the inference is only within a single statement and that polymorphic functions resolve the typeclasses in a particular way.
If you're used to Scheme, you'll get frustrated that the identifiers can't be created easily in its new macro system or you can't manipulate the scope-sets of the identifiers.
If you're used to Erlang you'll wonder why Akka can't reload functions easily.
If you're from Java you'll wonder why everyone is going on about profunctors, effects and finally tag-less.
Clojure ticks all of those and more, while most superficial comparisons concentrate on superficial aspects.
For example, you can usually find an API library for Java even from small vendors. I don’t think I’ve seen any vendor provide an Erlang API library.
Elixir/Erlang processes serve a lot of roles. If those roles don't line up cleanly your code could end up a lot more complex than necessary in other languages.
In the past the JVM had better raw performance, but I'm not sure how much that might change with the new JIT in BEAM.
Also the recent JVMs have GCs that can collect enormous heaps without stuttering of any kind.
The entire syntax for Clojure fits in a single line. Its easier to learn and being as expressive as it is, the core idioms are quickly picked up as well.
So - You can get into it quickly, very quickly if you're already familiar with FP.
The brevity of the code means that you'll produce much more robust code, which takes up a lot less screen real-estate. This allows you to grasp the functionality of any code you read, very quickly and start working on the problem.
It'll go as fast as Java, but slower than C/Rust. For some performance oriented tasks, you'll have to put in more work than makes sense, to get the performance you want. But for 99% of the Apps that are being written, Clojure will perform just fine and you'll end up with better code.
Compared to Haskell or most other FPs (not F#) you get the added benefit of being on the JVM. Write once, run everywhere. Huge libs to do everything from 3d graphics to webserving.
In most cases, I use Clojure for the above reasons summed up in this one sentence: I makes me more effective than the alternative.
ps: Having enjoyed Lisps for 20 years or so, Ive never used Par-Edit :)
But some of us absolutely don't like this syntax. The small bits of Java in the article are very readable in comparison.
This has never made any sense to me. Can someone please explain why you would still want the original vector to continue to exist with data that no longer reflects the current system? What am I missing?
https://softwareengineering.stackexchange.com/questions/2509...
I have context scratch-pad hashmap object I pass into a top-level function. It can then be decorated with extra scratchpad data all the way down the call-stack and passed into lower functions, for them to make use of. So each function can pass stuff down, but it's not available further up the call stack. It effectively looks like a stack object in terms of its semantics: as you unwind the stack you unwind history, 'undoing' changes. And the stack can take many different paths over execution.
Functions can do pretty much anything they want to the object further down the stack, without affecting other functions' inputs (parents or siblings). If it were mutable, the functions would suddenly be coupled to each other, and could change each other's data inputs. Add concurrency to that and it gets worse.
There are other ways to do this with Clojure. But I like this method, it's obvious and easy to test. It also feels reminiscent of Prolog.
In my example I'm associating new values into a hashmap, not appending to a vector, but it amounts to the same thing.
You can write the program
x = read_int_from_terminal();
y = foo(x);
println(x + ": " + y);
And you can be confident that invoking foo has no effect on the value of x that will print out on subsequent lines. x is a local variable that refers to a stateless, immutable, mathematical object. If x refers to the number 3, it will continue to do so until you personally tell it to refer to something else.In clojure, as in other functional programming environments, a vector is also a stateless, immutable, mathematical entity. Which is nice because nobody can change its state out from under you and that makes programs easier to reason about.
There are also specific use cases where this feature may shine in a specific way, for example making it easy to maintain an "undo history" when implementing a text editor. If the state of a buffer in your editor is an immutable value then it's easy to maintain a stack or list (or whatever) of all the states of the buffer - the top of the stack being the current state - and operations on the buffer simply create a new version but do not destroy any information about prior versions.
Outside of such specific use cases, though, it's just about referential transparency and enhanced ability to reason about the interactions between different pieces of code.
So in your example, it "continues to exist" in local variables to reflect the state of the system as it was when you read it, as long as you still hold a reference to the old vector. Typically, you'd ask your software to fetch a fresh copy of the vector any time you'd want new data. But that's explicit in code. You'll have fewer surprises if mutation (like vector-appends) are never shared between variables.
in Scala you have mutable collections and immutable collections - like that one; the more accessible versions that you have in your default namespace are immutable (that's supposed to be the default choice).
now in theory smaller contingent objects that span 'a few' cache lines would be easier to copy than to modify. Now my problem with that statement is that with the JDK you usually have lots of Object references (can't do a lot with primitive types), so you need to try hard in order to get an object that spans a few cache lines. You would have more of these in go, but they don't do a lot of functional style programming in go, afaik.
maybe it would make some sence to port clojure or scala to the golang runtime.
Probably a good starting point for people today is go straight for Babashka, easy to get started and fast: https://github.com/babashka/babashka
* It's slow
* Development with the REPL is slow because the startup times are glacial and REPL-oriented development usually requires tons of from-scratch restarts.
* The tooling sucks: build systems, IDEs, debuggers, etc. If you feel like writing code with just an editor and a terminal is like going back to the stone ages, you're gonna want to bash your own head in with a mammoth bone club.
* Java interop is a black art, and when you need to use it, it will ruin any sense of elegance you felt for your code originally.
* The ecosystem practically doesn't exist, unless you're willing to absorb a lot of java libraries. See above.
* The lack of static types hurts you in many ways, most of all your ability to refactor with confidence.
* Clojurescript isn't the same language, no matter what they promise you. Clojurescript is weakly typed, Clojure is strongly typed. If you aren't aware of the difference, prepare to spend weeks of your life tracking down bugs that would only be days in Clojure, and would never exist in the first place in a statically/strongly typed language.
For example there is a nice discussion over slow startup (note that for the most people the impression that it is slow comes from the Leiningen startup which starts two JVM processes): https://stuartsierra.com/2019/12/21/clojure-start-time-in-20...
Unfortunately, web apps are probably the thing that Clojure does best, and Clojure is already plenty slow there. Try it out for something typically throughput intensive (like mathematical programming or machine learning), or short lived (like CLI apps), and you'll give up almost immediately.
CLI apps are easily solved with Babashka now.
Clojure is on place #20 with a score of 1.3M vs 1.6M of the winner. That's fast enough.
More broadly: These kinf of benchmark numbers matter in a pretty small niche of web services and it rarely makes sense to make it a big factor in PL choice.
> Be kind. Don't be snarky...Please don't sneer, including at the rest of the community.
I heard that. I've read Clojure developers keeping the same REPL running for days and avoiding this problem, but I'd always be worried about the state of the global namespace not being what I thought it was. And this is especially true during development / exploration.
REPL development is what I miss so much from other languages. You typically don’t add dependencies as frequently for startup being an issue.
In the end I felt like Clojure was too clever for its own good. By relying on Java (which was a great choice) it took a lot of the oxygen out of the ecosystem for other devs to build tooling and libraries, and without that there aren't as many people participating or becoming a well known community member from their work there. Again, can't say this was the wrong choice, but in my opinion ecosystem is the #1 thing for a language and the way Clojure was done had an impact on how the ecosystem could grow.
What's wrong with Intellij with Cursive?
It’s fast, both clj and cljs but performance is non deterministic and as with most functional languages it can be hard to reason about performance. Profiling cljs is very hard
Coming from the js world I feel that tooling generally works. Especially when it comes to build systems. Very happy to not deal with webpack inconsistencies. With regards to ides there’s cursive for IntelliJ users, Calva for vscode.
I can’t comment that much on Java interop. Js interop is a mild inconvenience.
With regards to ecosystem, google ability is an issue but tempered by the simplicity of both clj and cljs.
About types, there are a class of bugs they will help to avoid. It does help with understanding intent of the code you work with. The drawback is that they help facilitate abstraction and does not help with reasoning that much about a program actually runs.
Yes cljs is a different language. But in practice it feels very much the same. I do however have a big issue with being able to push deterministic performance out of it. Keeping execution time for any frame below 16ms can be a challenge and for some type of front end stuff js is to be preferred
All of these things are quite insignificant on how fast a team can churn out good quality code using clojure. I’ve never seen any team that I’m with at the moment write correct, readable and fast code as quick.
Compared to JS, yes, it's slow. You can make Clojure run faster but you will be writing Java with parenthesis not Clojure.
Startup times are indeed atrocious, in Clojure the REPL is obligatory and not optional because of this. You can avoid REPL restarts by using a third party lib like component but you have to buy into a new architecture for your program that can be overkill for many occasions.
Java interop is not a black art but it is painful, the JVM has many great libs but the majority are not, many are loaded with lots of methods returning void, data models like everything needs to a be subclass of an abstract class, etc... So while you can develop with time how to write ad-how Clojure wrappers for your use case faster, you waste lots of time doing it. And yes, this something you have to continually di cause as you said, the ecosystem is very poor.
Without going into the static vs dynamic typing debate, I would say Clojure is at the top of the dynamic languages spectrum (the best in its class if the JVM fits nicely the problem at hand).
Clojurescript's interop with the JS ecosystem is crippled by relying on the closure compiler, which doesn't play well with anything in the JS ecosystem.
Regarding ecosystem and interop, in my experience (using clojure for about a third of the stuff at my job) I've rarely encountered a problem directly interop-ing with a java library, things like "doto" and "reify" do a good job of smoothing the rough java edges off.
More importantly I've usually had the choice of either directly using the using a pure clojure alternative or direct interop with java lib or using a clojure wrapper around the java lib. Incidentally those are my preferred choices in order (assuming the features I'm interested in are supported equally).
Perhaps I have been lucky in my requirements from the clojure / java ecosystems. I find the most important lesson I learned was to only use clojure wrappers if they are of a supremely high quality (either auto generated like cognitects aws lib or with a massive amount of momentum behind them like clj-http (wrapping on java http components)). An average quality or not super actively maintained wrapper is much worse than direct interop (again leaning heavily on the provided macros for interop to sand off the nastiness).
* Yes, if your project is very big and macro heavy, it can take some time, but startup times have improved. In any case, I BARELY need to restart my development JVM. I have one currently running that I haven't restarted for 1 week+.
* Depending on what's your cup of tea, there's emacs/CIDER or IntelliJ/Cursive. They both work well. IntelliJ/Cursive is an excellent IDE combination. I use it every day.
* Java interop is very straightforward, not sure what you mean. Sure your code might not be all pure anymore, but that's the price for solving actual problems.
* Good java libraries have wrappers. A ton of original Clojure libraries as well. https://github.com/cgrand/xforms for example allows you to easily do things that I can't even imagine doing in an imperative language.
* Static vs dynamic typing: don't want to get into that.
* "Clojurescript isn't the same language". I use both Clojure and ClojureScript every day and as far as Clojure-only code is concerned, it works in both languages 99.99% of the time. One case you can encounter issues is if you do something host-specific, like dealing with numbers. That's by design. Clojure embraces each host, does not try to reinvent it. When you just use pure Clojure data structure manipulation, it works the same across both languages and works like magic.
It might be true that Clojure is fast enough for the apps you write. I know it is not true for the apps I write and have worked on in my professional career.
There are millions of apps out there that are performance sensitive enough to not use Python or Ruby, but not so performance sensitive that they couldn't use Java.
https://www.diva-portal.org/smash/get/diva2:1424342/FULLTEXT...
Here's what someone with more than superficial understanding of the language can produce. Try not to base your argument on the first google search result next time
> I always think it's hilarious when Clojure enthusiasts try to address concerns about the language by talking about parentheses, as if that was actually the major barrier to entry.
Not that they are a major barrier, but they do scare people away. I know this because I've been tempted to try lisps before and after having a look at some source code, I was "scared" of the parenthesis. They are ugly (at least I thought at first sight) and they seem like very tedious to type. Of course I know better now, but that was _the_ reason I had not to try lispy things, at least on a couple of occasions. So yes, the "the parens are A-OK!" is absolutely warranted when trying to sell Clojure or any lisp for that matter.
> It's slow
So what? If I'm not writing anything CPU bound, I'm not very concerned about that. Clojure is fast enough for most things. The performance penalty is completely justified if it takes me half the time to write an app. This is just a trade off, or do you write all your stuff in ASM?
> Development with the REPL is slow because the startup times are glacial and REPL-oriented development usually requires tons of from-scratch restarts.
This is just very much not true. First of all, nobody is forcing you REPL-oriented programming. Second of all, it doesn't take more than a couple of seconds for most apps to restart. And third, if you use "def" and "defonce" correctly you'll hardly ever have to restart the REPL.
> The tooling sucks.
That's just your opinion, man, and it's also pretty rude. Clojure have awesome IDEs and tooling, and the aforementioned REPL-development allows you to have the same "react-hot-relading" developer experience, anywhere.
> Java interop is a black art.
Java (and Javascript) interop is pretty straight forward. You call functions and instantiate classes and stuff. No naked dancing under the moon involved.
> The ecosystem practically doesn't exist.
It maybe didn't some time ago. The ecosystem right now is thriving with pretty cool projects.
> The lack of static types hurts you in many ways, most of all your ability to refactor with confidence.
That's also pretty subjective, in my opinion the only thing that gives you confidence to refactor is unit testing.
Plus there's other ways to validate parameter and return values in Clojure, and they are not limited to the type.
You'll agree, that a return value is of a given type doesn't guarantee that it's correct.
> Clojurescript is weakly typed, Clojure is strongly typed.
Plain wrong, types are semantically equal in both languages (they are weak). The semantics of the target languages are just implementation details.
You can try this on a REPL of any variant you like:
(def foo 42)
(def foo "Hello, World!")
Sharing code between Clojure and Clojurescript is fine, especially if you stick to the built-in data types and functions. Of course if you have a lot of specific interops interleaved, it's going to be painful.Nope. Try evaluating (+ 1 "1") in Clojure as well as Clojurescript.
One is weak...it doesn't throw an error, might throw a warning because the compiler can plainly see that it is wrong, but you'll get no warnings when it is out of scope or evaluated at runtime.
The other is strongly typed, because it will actually throw an error, knowing that adding a string and a number is nonsense. While it would be better if this was caught at compile time, catching it at runtime is waaaaay better than not catching it at all.
The languages are actually different languages, because their type semantics are different. And you might want to figure such things out, as well as the far reaching implications, before you go around telling people they're wrong on the internet about a language that is laying in the mud like a crocodile, waiting for the perfect time to bite you in the ass.
Apply it to yourself my friend, you are wrong again. What you are talking about here is implicit type coercion, which doesn't have much to do with strong vs weak types.
Also in your example you do get a warning, so it doesn't seem to be a problem?
https://www.educative.io/edpresso/statically-v-dynamically-v...
And if you really do not believe that to be a problem because the compiler warned you about it, I bid you the best of luck. You're gonna need it. There are dozens of ways that bug can sneak its way past the compiler where you won't get a warning because the compiler only does local type inference.
- Intellij IDEA with the Cursive extension is very popular outside of emacs (I've met more clojure developers who use IDEs than those who don't.)
- Clojure uses the error handling mechanisms of the target runtime. You have try/catch statements and side effects are often used. It's not a no-side-effect language.
- Parentheses are almost always managed with parinfer/paredit and python-style indentation rules in production code I've seen.
You're quite right that performance will be tied to the JVM or V8/SpiderMonkey/etc.
“From [its] start in 2013, Nubank has grown to 600 Clojure developers, running 2.5 million lines of Clojure code in 500 microservices”[0]
[0] https://www.fintechfutures.com/2020/07/brazilian-challenger-...
I've designed and worked on systems with both OOP and functional-style code. Clojure is a tool that very much helps minimize technical debt. I think the minimal use of managed state in Clojure plays a crucial part.
Having done a good amount of looking through the Clojure language code itself, there's very little (if any) technical debt there.
Most reasoning is local to your changes thanks to the focus on small, pure fns dealing with immutable data.
Nesting boxes instead. It could be represented, in the editor, that way.
It could be much more legible.
Is anybody doing that?
Retooling to use nesting boxes has obvious appeal to a newcomer, but none to an existing programmer, and the cost to the corpus of existing programmers is far too large to justify.
There have been many attempts at languages that separate (or even do away with) the textual representation vs what is shown and interacted with in integrated tooling. Seemingly it has nice properties like forbidding invalid syntax. So far the common textual representation seems to be important enough to win over the AST languages.