Hacker News new | past | comments | ask | show | jobs | submit login
Clojure to die for (cjohansen.no)
173 points by cryptolect on March 11, 2014 | hide | past | favorite | 73 comments



I spent 10 or more hours writing "Building static sites in Clojure with Stasis" (http://cjohansen.no/building-static-sites-in-clojure-with-st... / https://news.ycombinator.com/item?id=7375425). It was written so people could enjoy it even with little Clojure experience. As a piece of supportive material, I pulled together this post about some (to me) exciting idioms in about 40 minutes. Results after one day: the afterthought is #4 on hacker news, and has 10k views. The "real" post has a mere 3k views, and is long gone from hacker news.

Who says hard work pays off?


I know how you feel. A off-the-cuff post [1] about why I chose a few different web-related libraries hit front page of Hacker News a while back. I spent maybe 30 minutes on it, and laughed when I noticed it had been received so well (someone else submitted it).

In comparison, a post[2] I was really excited about, that demonstrates how to improve (Korma) SQL abstraction in Clojure with Macros (I created a demo app) didn't seem to get anyone excited. I probably spent 4x as long trying to put it together. Oh well.

Anecdotally, it feels like programming-related posts with a few high level pieces of code resonate better than deeper dive posts. Possibly because they're easier to skim across while having your morning coffee.

[1] https://hackworth.be/2014/02/05/an-expanded-comment/

[2] https://hackworth.be/2014/02/18/spicing-up-korma-with-macros...


I suspect this is like the executive who said "I know over half of my advertising dollars are wasted, I just don't know which half!" (pre-AdWords).

I don't think you could have predicted ahead of time which of those would be more popular before writing them.

Having said that, the Stasis post is much longer, so it's less conducive to reading at work and quickly dashing off a comment or two on Hacker News without your boss noticing you goofing off. Hypothetically speaking, of course. :)


Perhaps an HN article analyzing which HN articles sink and which swim sometime in the future?


I've had articles sink without a trace on HN. I've had articles shoot to the top. The crazy thing is its often the same article resubmitted (by someone else) a few months later.

Based on my experience, a "worthy" article has about a 50-50 chance of making it off the new page to the home page and taking off. This is basically random. Many people think HN is a meritocracy where the best articles succeed, but its not. There is a huge random factor in the way. This isn't too bad for a short throwaway article but it's frustrating if you put hours and hours into an article and know people would like it but it goes nowhere.


There is an enormous amount of randomness in what makes it to the front page of HN.


I learned plenty from both posts. This one made me look at the other threading operators and the new some? functions in 1.5. Consider dropping "with Stasis" from that title. Well done, keep up the good work.


Thanks!

I don't have any problems with the skewed "readers per effort" metrics, it was just a fun observation. If my only goal was internet fame, I would not write huge long-ass posts :)


Hard work doesn't pay off unless you make things people want. It's all about making things that have an audience - and that's often hard to predict ahead of time :)

I think for any big win/viral/hits based endeavour - music, OSS, blogging, games - creating many things quickly and doubling down on whatever works is the best strategy.


But giving people exactly what they want only further enshrines the status quo, which is almost always crappy. Make things because you have to, I guess.

HN is pop programming culture and if you're writing Clojure, you can't expect them to appreciate it as much as, say, Node.js.


No accounting for taste, huh?

I've only recently started with Clojure and I feel like a clumsy newb most of the time, but I adore the elegance and simplicity. I like that this article gets into the benefits of hashes, maps, and vectors as first-class objects in Clojure. I'm sure the Lisp purists hate it, but as someone used to 21st century programming languages, I love it. And I like how it shows Clojure's superiority over other languages' access/manipulation of the same structures. I'd love to see a follow-on here about the advantages of immutability too.

As for the popularity of Node.js... fertilizer is a wonderful thing, but it's still made out of manure. I can't quite get around my distaste for Javascript.


Thanks for linking back to the original. I missed that one and will read it tonight - sounds like something I'd like to learn more about.


I've thought about learning Clojure but already knowing Haskell I feel that in comparison I wouldn't gain much and then loose some.


As someone who knew Haskell first and then learned Clojure, I can tell you that this assumption is incorrect. It's so completely philosophically opposite from Haskell in so many ways that it's a very good mental exercise to really grok them both. It will make you appreciate how much you're paying for that fantastic type system. Understanding this cost will allow you to make better decisions in the future about which technologies are a better fit for a given project.


Sir, your comment is intriguing and I want to hear more about it.

What is this philosophically opposite that you mention? Haskell is good at creating domain-specific languages, so I think the differences must be around the superior meta-programming facilities in lisp languages. So, what would you do with Clojure that you couldn't do in Haskell easily?


It's an oversimplification, but the basic philosophical difference from my point of view is that Haskell values correctness above all and Clojure is above all pragmatic. I think all of these major differences in the languages are reflections of this:

- static vs. dynamic typing

- pure vs. allowing side effects

- native vs. hosted

- focus on syntax vs. rejection of syntax

Depending on the problem, you may get big benefits out of one side or the other.

One particular thing that is much easier in Clojure is dealing with heterogeneous, hierarchical data. This is just so effortless because of the combination of dynamic typing and the wonderful built-in persistent data structures. This is possible but takes a lot more work to do in Haskell and is much slower to iterate on when your data changes.


This comment is spot-on.

According to the Yegge programming axis, Haskell is extremist conservative (emphasizes safety above all) and Clojure is some notches down into a more moderate zone (merely "conservative") that balances safety against pragmatism.


I'm confused. In what sense is correctness not pragmatic?

Also, have you used lenses yet? Heterogenous hierarchical data in Haskell is quite easy now.


Guaranteed correctness has a cost in the form of extra specification needed to get things done. For example, many type errors are essentially noise: they do not catch real bugs, and they create needless busy-work for the programmer to correct them.

example:

function stringify(FooObj object): return object.toString()

Now suppose we do some refactoring somewhere, and we want to call stringify not with a FooObj but with a BarObj. Since we have defined stringify as taking a FooObj, the compiler whines until we update the definition. However, depending on your philosophy, this error is nothing more than noise, because stringify would work fine on a BarObj.

Pragmatists may wish to forgo such safety in favor of greater flexibility:

function stringify(object): return object.toString()

This is the essence of liberal programming: an emphasis on flexibility and minimal specification, at the cost of reduced safety guarantees.


That example is a straw man. First, all reasonably strong type systems support polymorphism well enough to handle that case trivially. Second, type inference is a thing, so a lot of code in strongly-typed languages even looks the same as your second code example.

I'm not saying you can't come up with a fair example to support your argument. I will say, though, that it's going to be a lot harder to do so, and that such examples only rarely come up in practice. The argument for flexibility at the cost of guarantees of correctness used to be a good one, but it has weakened significantly with time, and before long will cease to be valid at all.


The example cited makes two decisions which are not articulated explicitly but are decisions one can make and which have benefits and costs. They are:

1) The function correctly executes on the class of things for which (.toString thing) has a sensible run time invocation.

2) The function delays examination of the correct class of its argument from compile time to run time.

1 is a benefit, in that in a dynamic system it is possible that something which, at compile time, does not have a sensible .toString invocation, can gain it at run time and then participate in the function.

2 is a cost, in that you have to delay classification to the instant in which you try to invoke .toString

This tradeoff is elemental and there will be systems you can build by embracing it that will never be possible in strongly typed languages. You'll be able to build isomorphs of them, but doing so will involve expressing significantly more ideas to get there.


Terms like "straw man" imply that I'm trying to debate, but I'm not. You asked how correctness could be contrary to pragmatism, and I tried to provide an example.


Whether or not it's a debate has nothing to do with whether or not your example is valid. I was just pointing out that it isn't.


Out of curiosity, how would you define my example stringify function in Haskell with "no cost" safety around the argument type?


The typeclass that provides "toString" in haskell is "Show". It defines a few functions, of which the important one is "show", which takes the original type and returns a string. A function that is equivalent to the java-ish example is:

    stringify s = show s
No type definition is necessary, because the compiler can correctly infer the correct, most generic type:

   stringify :: (Show s) => s -> String
Or, for any s in type class Show, a function that makes s into a String. Note that dispatch is done statically.

Modern type systems eliminate most of the cost of type safety through good generics and type inference. Mainstream statically typed languages are just 20-30 years behind the state of the art.

(note that the example actually can be reduced to: stringify = show)


Guaranteed correctness has a cost in the form of extra specification needed to get things done.

This is simply untrue. Take an example in Haskell:

    > show 23
    "23"
    > show (4,5)
    "(4,5)"
    > show (Just 2.34, Nothing, [2..5], 'c')
    "(Just 2.34,Nothing,[2,3,4,5],'c')"
This works for all types which are members of the class Show. We can even derive new instances for Show for our own new data types automatically:

    > data Foo = Foo Int Float deriving (Show)
    > show (Foo 3 4)
    "Foo 3 4.0"
But what happens if we omit the instance of Show from our definition?

    > data Bar = Bar Char Bool
    > show (Bar 'a' True)

    <interactive>:12:1:
        No instance for (Show Bar) arising from a use of `show'
        Possible fix: add an instance declaration for (Show Bar)
        In the expression: show (Bar 'a' True)
        In an equation for `it': it = show (Bar 'a' True)
We get all the benefits of safety guarantees with minimal extra work. This form of automatic derivation works for many type classes in the standard libraries but in the cases where it doesn't work we simply define a few methods which are specified in the class's definition. None of this is any more than what you'd need to do in a dynamic language but you get all the extra benefits of compile time safety.


It was pretty easy before with generic traversals as well.

I think people give Haskell a bad rap for handling heterogenous data, but it's actually quite good at it. You just feel the pain of a poorly specific data domain more sharply and tend to want to reify it into something nicer quickly. No such option exists in Clojure unless you use Typed—and I say this as someone quite familiar with both languages.


I've discovered programming approaches in Clojure that I'm now looking for in other languages. For instance, two of the things I looked for straight away in Julia was if there were immutable types and map functions, since I've come to really appreciate them in Clojure. Haskell is on my list of languages to try out after Julia.


Having learnt a fair bit of Lisp, and dabbled with Haskell, it seems to me that a Haskell-like language should be implemented atop a Lisp-like language, rather than vice versa. Template Haskell puts macros atop Haskell, whereas it seems things like types and syntax should be atop Lisp.


"loosing some" is the whole point :) That's the beauty of lisps, they make "the simplest possible way of doing something" just pop out, and once you see it, you just have to do it this way! You then show the middle finger to both 'powerful type systemy abstractions' and forget you ever learned them, and then the other middle finger to 'powerfully optimized down to metal programming', and you go hunting for real world problems that you can solve using 'the simplest possible ways' thinking ...until you realize that using simple and elegant tools, you ended up building a ...type system :) (http://typedclojure.org/)


Every sufficiently advanced Lisp implementation contains an ad-hoc, badly-specified, bug-ridden implementation of half of Haskell's type system.


Until you encounter a real Lisp implementation that contains an actual thorough, well specified and bug free implementation of type system more powerful than the one Haskel has.

Interested? Then take a look at the Shen[1] and enjoy having both the benefit of types and other powerful Lisp constructs in one language.

[1] http://shenlanguage.org/


Is anyone using Shen yet? The only problem, and I am a passive lurker in circles in HN, Reddit, and elsewhere it has been mentioned, is an Ocamlesque license policy. People have said this will get them nowhere. It is understood they, from the outside view, want a single language implementation, not just a standard. But everyone I see says they will not touch it with a ten foot pole for this reason.

Also ironically, this real implementation of Lisp necessitates being built on "lesser" or not "real lisps" for us commoners, like SBCL and CLisp. It also has hosts on Ruby, Python, and this even lesser lisp called "Scheme" (rest assured, this is a joke and not a troll).

http://shenlanguage.org/Download/download.html

That being said I find it very interesting. Its licensing, even just the word if you read through, sounds very aggressive and weird.

Has anyone here used Shen yet?


Indeed, the license is impossible. In addition to what you point out, it attempts to say the same thing as many as 5 times, introducing unacceptable ambiguity.

The final straws for me is that it is the creator's explicit intent that no incomplete or incorrect implementations be publicly accessible (he among other things thinks there's enough already...), and that the author broke a promise to the community, demonstrating that his word is no good.


I am confused about all these licensing "problems" everyone is talking about here. Could you enlighten me?

Sure, Ocaml and shen have "weird" licenses but I just read through them in the last few minutes and see no major issues.

The licenses for both only effect the code for the language interpreter/compiler and associated libraries itself and should have little to no effect on your own code.

If you use MIT/BSD licenses then you are almost certainly fine, with GPL there are some potential issues but most people think that dynamic linking doesn't count as "linking" in the lawyer sense and, therefore, using GPL code with something like Shen would be fine. This hasn't been tested in the courts AFAIK.


The problem you haven't noticed is that the language covering what you care about, using Shen to write programs, is in many places (potentially at least 7 by a count I just made), and you, or, say a lawyer for your company, has to read and understand the whole huge thing to make sure your intended use is OK. And that the multiple times it tries to say the same thing are not ambiguous.

It's also explicitly not open source ("We are therefore not open source."), and should you find yourself needing to fix a bug in a language implementation your legal burden massively increases.

Compare this to the licenses you name, they're all well understood, and in at least some cases validated in court. For that matter, the GPL has been validated in at least US and German courts.

As for Ocaml, it uses the Q Public License (as well as the LGPL); it's short and a quick glance didn't indicate it tries to say the same thing more than once. It's been around for a while, to the point it's an OSI and FSF approved licence, so others have looked at it, as they did for Qt prior to version 4.0., and KDE based on it prior to then. About which there was much todo, so it's be thoroughly analyzed.

So it may be "weird", but it's significantly more palatable than Shen's license.


To qualify my original statement much later, I was told offhand by people Ocaml's multi-core processor/SMP limitations continue to exist (I know Jane Street actually is putting up money to change this) because the license of Ocaml prohibited people from making any other forked implementation of Ocaml itself. Thus, it has been single-core only for many years later, because Xavier and the core team did not think SMP implementation was a priority, and have not been asked since.

Granted, I very seemingly little about this. So I could be very mistaken. Can some confirm I am wrong? I am sure someone here has far more detail than me generalized nonsense.


Yes, it's a pity it has such a license. People tend to dismiss it without even looking at it because of the license.

I don't think the language will take off unless it change licensing before it's too late, but I still find it enjoyable to use with some smallish projects.

I hope that Shen will get a successor that will make it more suitable for modern world while keeping ideas that make it unique and sound in the first place and smoothing the rough edges.


I would love to use Shen, but I refuse. Clojure's license is restrictive enough (I can't write GPL projects in it). But Shen isn't even free software by any definition.


Uhm. Can you provide some pointers for 'Cannot write GPL projects in Clojure'? That's .. new to me.


In order to package a Clojure project, I have to include clojure.jar which is an EPL licensed project, incompatible with the GPL. If I write my code in Clojure, but license it under the GPL, then that forces me to not package clojure.jar with it, making the installation process a huge pain.


I'm pretty sure that isn't a surety. See the accepted answer here https://stackoverflow.com/questions/11412160/java-libraries-... for instance. LGPL, in any case, would almost certainly be okay. Most people I talk to consider .jar's a dynamic link and dynamic links okay via the GPL but there are alternative interpretations and I don't think it has been tested one way or another in the courts.


To the best of my knowledge you can make GPL clojure code, and include clojure.jar too.

http://osdir.com/ml/clojure/2010-03/msg01337.html


Shen has the same problem as Idris and all the rest of the languages with more powerful type systems than Haskell: none of them are ready for production use. I would absolutely love to be able to work in a more powerful type system, but the support just isn't there yet. Hopefully we'll get there in the next 5-10 years, at which point I will very happily switch over.


To be fair, Haskell had that same problem for a very long time too ;-)


It was more of a joke really. It's true in the same way Murphy's laws are true...

Still, thanks for shen, gives me something to read about.


This one has a dissertation[1] behind it. Though unread (yet), I think it's well specified.

[1]: https://github.com/downloads/frenchy64/papers/ambrose-honour...


as ashley yakeley once noted, every sufficiently well-documented lisp program contains an ml program in its comments.


One of the at least occasionally useful strengths of Clojure over Haskell is the pragmatic choice Rich Hickey made by building on top of Java. This has continued with Clojurescript (Javascript) and others like Clojure on CLI, objc, etc. What this means to me is that on the occasions when I'm faced with a task like having to produce a Microsoft Excel file readable by an array of Excel versions I can leverage the wide range of libraries available in the parent environment (Apache POI, http://poi.apache.org/). The attention paid to the facilities to interoperate with the underlying ecosystem means that even given the relatively short history of Clojure it is likely that someone has already done at least some of the work to make it pleasant to use in Clojure (https://clojars.org/org.clojars.boechat107/cloxls).


Clojure programming might be more data-oriented than Haskell programming. I've never done any real-world Haskell programming, but I'd guess it'ld have a completely different feel.

The dynamic-typing means Clojure code is usually built around data-flows of maps/vectors/strings (generic information types that are trivial to integrate from different sources), trivial serialization, code-as-data.

I'd guess Haskell code is more likely to be built around abstractions based on the more powerful type system.

clojure.core.logic - a Prolog-like logic language that you can embed in the middle of a Clojure function - is a good example of something that is possible thanks to Clojure's lisp macros.


> clojure.core.logic - a Prolog-like logic language that you can embed in the middle of a Clojure function - is a good example of something that is possible thanks to Clojure's lisp macros.

import Control.Monad.Logic

And! You don't macros for that. Macros are not used much in Haskell (only for very special cases), there's no need for them.


While useful, Control.Monad.Logic is not really an alternative to core.logic. It is a search monad more than anything, and it doesn't perform any unification or constraint propagation.

The type system gets a bit in the way when implementing something like miniKanren in Haskell. I suspect Clojure is a better fit for this kind of highly dynamic problem.


core.logic doesn't really use macros does it? Maybe to simplify the syntax, but macros is in no way required to build a core.logic library, at least not in the same way as core.async requires it.

The real beauty of macros is to simplify syntax without taking a runtime performance hit.

How would you implement thread-first or thread-last in Haskell without a runtime performance hit? Template Haskell?


Is say that the things you describe as Clojure's strengths occupy one part of Haskell's wheelhouse. The not change is that the flows orient over highly composed custom and built-in ADTs, but they're honestly hardly more painful than maps/vectors/strings while buying a lot of robustness.

And if you don't like that, it's easy to use "untyped" types as well.


Always learn another language. It opens up new mental pathways and can help you appreciate the ones you already know in surprising ways.


Well, there is no such simple interoperability with java from haskell, that's for sure. But otherwise…

  user> (:key nil)
  nil
Mmmm, I'm not certain I particularly want this

  user> (nil :key)
  CompilerException java.lang.IllegalArgumentException: Can't call nil
RUN!


As many are saying it's a pragmatic choice. And it allows you to do stuff like this:

  (:foo (:bar {}))
Without throwing a NPE. Which is what I want 99.9% of the time. As someone who codes in Clojure close to 10 hours a day, I rarely see a problem like you describe.


If you use clojure.typed, I think the second example would give you a compile-time type error.


While true, the second example already gives you a compile-time error in normal Clojure (note the CompilerException). It never compiled and was never evaluated. This on the other hand compiles and is likely what the OP wanted to show:

  user> (let [e nil] (e :key))

  NullPointerException


I envy you 'cause I'm too stupid to use Haskell. I'd suggest you stay with it if it already lets you do things. And I'm staying with Clojure 'cause it works for me.


Small typo:

The link to Clojure points to "http://cjohansen.no/clojure.org" instead of "http://clojure.org".


Nice writeup. There's an error in one of the examples though:

(map :name people)

evaluates to a lazy sequence, not a vector.


In which the author gets really excited about idioms in Clojure that don't seem that exciting.


I think the article is overly focused on small, pragmatic syntax issues without explaining how the underlying language and runtime have _enabled_ them.

It might be interesting to discuss _why_ keywords or maps or sets can work as functions, by implementing the IFn interface. Perhaps explain why homoiconicity and macros enable things like thread-first and -last, and talk about how we can implement our own reader literals.


> I think the article is overly focused on small, pragmatic syntax issues without explaining how the underlying language and runtime have _enabled_ them.

That was kinda the whole point of the post.


Sounds like you have a nice topic there for your next blog post.


Agreed. This did nothing to make me really excited about Clojure or Lisp. A demonstration of differential was a better example than this.


It's not really intended as an "evangelist" post. It's just describing some details that makes a big difference to me in my day-to-day work. I guess it's more of a "hey, fellow Clojure programmer, don't you agree that these details really put the icing on the cake?" kind of post.

Maybe the other post I posted the same day would give newcomers a better impression of what using Clojure is like in practice (lots more code, more in-depth): http://cjohansen.no/building-static-sites-in-clojure-with-st...


The 'to die for' is too strong.

ps: what is the min* function ? can't find anything on google


It's an expression of enthusiasm, don't take it so seriously.

min* is like min but takes a list, it's not built in. Yes, I know about apply, I still prefer min* to (apply min) in certain cases.


Thanks, I was just affraid I'd missed some kind of builtin macro or convention. At one point I thought that -> would rewrite function #"\*$" to decorate them with apply ... paranoid noobies you know.

ps: you're a very enthusiastic person ;)


One thing that will help a lot: type (source f) to see the source code for a function or macro. Also (doc f) for documentation.


thanks for writing it. i have yet to grok or embrace Lisp-family languages, but your post was still fun to read.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: