Hacker News new | past | comments | ask | show | jobs | submit login
Learning Clojure: comparing with Java streams (frankel.ch)
117 points by nfrankel on Nov 4, 2018 | hide | past | favorite | 44 comments



Also, this bit:

    (->> justice-league
      (map #(:vehicles %))
      (filter #(not (nil? %)))
      (flatten))
can be simplified to just `(mapcat :vehicles justice-league)`


A few notes on why this is so:

- Keywords (i.e. `:vehicles`) can be used as getter functions, as covered in the OP.

- mapcat (like most collection functions in clojure) treats `nil` as an empty sequence


Not a lot of comments here for the number of upvotes, but the trend of the comments strikes me as lopsidedly negative and I'd like to throw in something positive to the mix - Clojure needs more people publishing beginner friendly code even if it isn't using some specific function that does the same thing with less typing. So, y'know, I'm really glad we've got people out there who are documenting how they manage to get things to work under the new and generally unfamiliar paradigm of "practically functional". Especially since Clojure's Achilles heel is the combined double-whammy of its incoherent error messages and the learning curve for a beginner trying to pick up how to do common tasks in a world with 'no variables'.

We have a programmer with >15 years experience, who is learning a new language and in the process producing code that is neat, functional (in the sense of no bugs) and not unreasonably verbose. Clojure is a language that is dense with clever ideas, it takes a lot longer than a few months to get good at using them if they are new. The terse comments throwing out improvements or making corrections seem a little harsh, I'd hoped to see people talking more about concepts than naming functions.


Author here. Many thanks for your encouragements!

Actually, I didn't feel any comments were particularly harsh. I'm learning from most of them. This is exactly the feedback I'm searching for: idiomatic Clojure.


Didn't mean to sound harsh, it's just I already replied to author with words of encouragement in the reddit thread (https://www.reddit.com/r/Clojure/comments/9u4p5m/learning_cl...) and didn't want to repeat all that comment here, so I decided instead to show briefly to other readers what can be improved.


The equivalent of `flatMap` is not `flatten`. Semantically `flatMap` is equivalent to `(comp flatten map)` – hence the name! – but Clojure does offer it as a single function named `mapcat` (”map and catenate”).


> Semantically `flatMap` is equivalent to `(comp flatten map)`

With the caveat that `flatten` is recursive and can handle a mix of collections and non-collections, while `mapcat` is non-recursive and every value returned by the mapcatted function must be a collection or nil.

(Identity returns its argument.)

    (flatten [1 [2 3] [[4 5 6]]])
    > [1 2 3 4 5 6]

    (mapcat identity [1 [2 3] [[4 5 6]]])
    > IllegalArgumentException Don't know how to create ISeq from: java.lang.Long

    (mapcat identity [[1] [2 3] [[4 5 6]]])
    > (1 2 3 [4 5 6])

    (flatten [nil [1 2 3]])
    > (nil 1 2 3)

    (mapcat identity [nil [1 2 3]])
    > (1 2 3)


Thanks, I wasn’t aware that flatten is recursive.


That's sort of implied by the name. A non-recursive flatten wouldn't really flatten much, and would be called join, or concatenate.


The flatten functions I know from statically typed languages (eg. Rust, Scala) only flatten one level. So flatMap is actually more or less equivalent to (flatten . map).


Interesting. My experience is the opposite - in dynamically typed languages like Erlang, Clojure or Common Lisp, flatten is understood to flatten lists, and flatmap is something else entirely - from what I gather, it's Scala that made the weird choice of naming a concatenation function `flatten`, thus creating `flatMap`[0], and then people started borrowing the name.

From this I provisionally conclude that "flatten" being non-recursive is an artifact of Scala confusing things; not an unusual thing in my experience with this language, but this time it seems to have gotten out of hand, instead of being contained within Scala ecosystem.

--

[0] - Even Haskell calls this `concatMap`, see [1].

[1] - https://stackoverflow.com/questions/49843262/where-does-the-...


Flatten is a steam-roller and should be used wisely: it recursively flatten a sequential collection. Most of the time this is not what you want. Most of the rest of the time when you think that’s what you want it’s a code smell telling you that the shape of your items has gone out of control. And there’s the rare occurrence where you can really use it.


Oops, indeed. Didn’t realize that Clojure flatten is recursive.


> and Clojure’s syntax is pretty limited

Erm, what? I am confused by the use of the word 'limited' in this context.


lisps can be thought of languages that avoid syntax. the code you write is more like a high level AST.


Yeah, and since you can represent using an AST any syntactic construct in other languages, it makes lisp the opposite of limited in terms of syntax.


maybe they mean 'simple' ? simple is good


I confirm: the syntax is limited. There are only a few keywords and langage constructs. The power is in the number of functions made available, or the ones you create for yourself.


Maybe it’s a problem with translation? “Limited” comes with negative connotation, as in “you can’t do as much as the other, non-limited guy”. Maybe you wanted to say “minimal” instead? s-expression based syntax is as powerful as it gets, as it allows to concisely represent any AST.


When talking about Lisp syntax, what you almost always want to say is "lisp has $most-positive-word-in-context syntax". So it's "limited" for some, "simple" for others, but the overall point is the same - no cruft and unnecessary sugar, and uniformity allows for easy programmatic transformations of code.


Doesn't Clojure have reader macros? The whole point of lisps is that you can extend them.


When I worked with Clojure the community seemed to treat macros as a nuclear option. Only use them when absolutely necessary. For some, this translated to “you can’t hug your children with nuclear arms”. If the community hasn’t changed, perhaps macros out of favor.


This still stands, macros are a really high level abstraction and do not compose. Almost always a function is enough. But if it saves a good chunk of repetition on something complex then go for it.

A good example of code with macro overkill is compojure-api. There is just so many clever macros that you need to read the original source code with a microscope to know what the code expands to.


Actually, Clojure does not have reader macros, because it was thought to be the biggest cause of the lisp curse.


Nit: Clojure does not have user-defined reader macros. There are quite a few built-in reader macros, including #? added for reader conditionals.


By this definition of terms, every Lisp dialect that ever existed had reader macros because it accumulated characters into tokens, and dispatched a recursive scan upon encountering (.


Fair enough, though definitly a nitpick :p


Then I'm surprised that it allows the user to define functions and macros.


I think most schemes also don't have reader macros. How often do you actually use them in CL?

I feel reader macros have the problem that it modifies/extend the foundational syntax. And that can become overloaded really quickly. Normal macros can change evaluation semantics, but not the fundamental syntax. Which makes it easier to navigate and understand in general. Also, reader macros are pretty hard to write in comparison.

Racket has a nice take on them though. Making them more like all new programming grammars, and defining your dialect per file. Even there though, most alternate syntax is for research or fun mostly.


> I think most schemes also don't have reader macros.

Scheme even has a fixed syntax in the RnRS reports. Lisp does not have that.

> Also, reader macros are pretty hard to write in comparison.

Not really. One just reads things from a stream and returns an s-expression.

> I feel reader macros have the problem that it modifies/extend the foundational syntax

That's a feature.

The main purpose of user-written reader macros is to extend S-expressions to support literal syntax of new user-defined data types.

There is nothing scary about it at all.

> Racket has a nice take on them though. Making them more like all new programming grammars, and defining your dialect per file

My Lisp Machine had that already in the 80s. It supported different language syntax - from various Lisp variants to Pascal, C and other languages.

> Even there though, most alternate syntax is for research or fun mostly.

On my Lisp Machine it was actually useful.


> Scheme even has a fixed syntax in the RnRS reports. Lisp does not have that.

Could you link to the RnRS in question? I'm curious.

> Not really. One just reads things from a stream and returns an s-expression.

I feel like if I agree to this, I also agree that pre-processors are easy to write and use. And that undermines the innovation and superiority of syntactic macros introduced by Lisp.

> The main purpose of user-written reader macros is to extend S-expressions to support literal syntax of new user-defined data types.

This is possible in Clojure as well. You can extend the language with new data literals. But they must begin with # + char(s) and respect the existing reader semantics, as they are processed as a syntactic macro, and not a lexical one. So you could add #queue[1 2 3] for example. But you couldn't add |1 2 3|

> On my Lisp Machine it was actually useful.

If only they'd still make those, and had improved its performance.


> Scheme even has a fixed syntax in the RnRS reports. Lisp does not have that.

https://schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-...

> And that undermines the innovation and superiority of syntactic macros introduced by Lisp.

They have a different purpose and operate on a different level.

> respect the existing reader semantics

This is an example of xapping data structure in *Lisp for for the Connection Machine

{moe->"Oh, a wise guy, eh?" larry->"Hey, what's the idea?" curly->"Nyuk, nyuk, nyuk!"}

> If only they'd still make those, and had improved its performance

Nowadays it might be easier to use an emulator running the OS.


> https://schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-...

Sorry, maybe I'm not seeing it, but what part of that link describe lexical user definable reader macros in scheme?

> {moe->"Oh, a wise guy, eh?" larry->"Hey, what's the idea?" curly->"Nyuk, nyuk, nyuk!"}

Right, so in Clojure you could extend the reader and do:

#xapping{moe "Oh, a wise guy, eh?" larry "Hey, what's the idea?" curly "Nyuk, nyuk, nyuk!"}

As a data literal for xapping data structures.


> Sorry, maybe I'm not seeing it, but what part of that link describe lexical user definable reader macros in scheme?

None. I never said it does. I said it has a fixed syntax.


Oh I see. Okay I misunderstood you.

What do you mean by fixed syntax? A formal syntax?


I believe they mean "limited in a good way", as in it doesn't have a bunch of unnecessary syntax that increases the number of characters entered, but does not add value to the language.

No pun intended, by "value" I mean "things that are worthwhile". Clojure is totally into "values". :-D


Out of curiosity, can anybody comment whether the Clojure versions of these are "lazy" in the sense that if you only take the first element from the result, does the the stream process all the results or just the first? eg:

    (first (map #(extract-name %) justice-league))
Does it map all the elements or only the first? This is actually the key aspect of streams, and more important in many ways than whatever syntax sugar is available to invoke the operations.


In Clojure, all of these are lazy by default.


Yes, map returns a lazy sequence. Lazy sequences get realized in batches of 32, so calling `first` would realize the 32 first values and then return the first one of that.


It seems strange to me that `(map extract-name justice-league)` isn't listed under "idiomatic improvements." Supposing you already had `extract-name` defined for some reason, it's cleaner and simpler to just map that unary function than wrap that function in an anonymous function - whether that wrapping is done with the `#(...)` reader syntax or explicitly constructed with `fn`.


Or indeed `(map :name foo)` instead of the suggested (and redundant, linenoisy) `(map #(:name %) foo)`.


Oh that's a pet peeve of mine. Fun fact: it's called an eta expansion/reduction/conversion.


It certainly looks like a deliberate move to introduce anonymous functions and thus justify replacing the named function.


Did I miss the Java counterpart of the examples?




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

Search: