6 ms·
Here is a very simple example that I think does a great job of illustrating how Clojure was designed to deal with "data" in a straight-forward and concise way:
by adamkl 6y ago
Here is a very simple example that I think does a great job of illustrating how Clojure was designed to deal with "data" in a straight-forward and concise way:
(defn csv-data->maps [csv-data]
(map zipmap
(repeat (first csv-data))
(rest csv-data)))
This is a function that, when given a contents of a CSV file (list of sequences), converts it to a list of maps, where the keys are the column headers.
I'm sure there are clever ways of accomplishing this in other languages, but the default way a Java/C# dev would probably approach this is to create a class that represents the CSV columns, and imperatively iterate over the contents.
With Clojure, the above function is idiomatic, and 4 lines long.
When it comes to "information processing" systems, which is what a lot of us work on, having a language with a primary purpose to provide powerful and concise tools to process that information is.. I don't know, liberating? (maybe not the best choice in words).
(this example was a bit of an eye-opener for me, coming from a .Net background, as I was writing some simple Clojure code that needed to read in some data and was struck at how simple this solution is)
- adamkl 6y agoIf you really wanted to (and you understand Clojure's destructuring and lambda syntax) you could even do it in two lines: (defn csv-data->maps [[head & lines]] (map #(zipmap head %) lines))
- waynesonfire 6y agoi prefer perl
- kencausey 6y agoIf you actually supplied a Perl version maybe you could provide a convincing argument.
- adamkl 6y agoIt may look terse, but its not code golf. Any Clojure developer would understand what this does (its actually pretty straight forward), but to an outsider, it definitely looks strange. Part of using Clojure is becoming familiar with the language, its syntax, and its idioms. Rich even has a section in "Simple made easy" where he talks about this very thing; its on us to become familiar with the tools we are using.
- waynesonfire 6y agothanks, i'll take a deeper look at Clojure. I need to have a way to get my java fix and scala isn't doing it.
- ivanovb 6y agoDo you have any recommendations of online resources or books that can get a web programmer (experience in PHP / C# ) up to speed with clojure ecosystem ?
- slifin 6y agoHi fellow PHP'er and in a long time gone C#er here I would highly recommend learning the spirit of Clojure first: https://changelog.com/posts/rich-hickeys-greatest-hits https://changelog.com/posts/rich-hickeys-greatest-hits https://www.youtube.com/watch?v=vK1DazRK_a0 https://www.youtube.com/watch?v=vK1DazRK_a0 There are a few classes of tech that are uniquely Clojure: Data driven DSLs: - https://github.com/noprompt/garden https://github.com/noprompt/garden - https://github.com/weavejester/hiccup https://github.com/weavejester/hiccup - https://github.com/seancorfield/honeysql https://github.com/seancorfield/honeysql Hyper normalised relational databases: - https://www.datomic.com/ https://www.datomic.com/ - https://opencrux.com/ https://opencrux.com/ - https://github.com/replikativ/datahike https://github.com/replikativ/datahike Advanced SPA tech (hyper normalised data driven): - http://fulcro.fulcrologic.com/ - https://wilkerlucio.github.io/pathom/v2 Once you understand the spirit and rationale for Clojure it becomes apparent why other communities don't have this kind of tech yet Once it gets down to practical things I recommend using clj-Kondo with type hints, Cursive for Intellji, make sure you learn how to hot code inject new code into your running program using your editor shortcuts, and TDD in Clojure is also excellent and immediate: https://cursive-ide.com/userguide/testing.html https://cursive-ide.com/userguide/testing.html Also look out for GraalVM and Babashka we're using it to compile fast native binaries out of Clojure
- sireat 6y agoWouldn't this be this just as feasible and elegant in JS, Python or Scala? Maybe not as short but short enough. Then again with Python I'd just fire up NumPy or ugh Pandas(which is basically NumPy with benefits) for this kind of transformation.
- adamkl 6y agoYou could definitely do something like this in other languages, probably even in Java/C# using a HashMap or Dictionary, but the difference is that this is “the way” you would do this in Clojure. It’s not, generally speaking, they way you’d do it in the other languages you mentioned. I think it’s one of the reasons why people always say that learning Clojure makes them better developers even when they’re not using Clojure. The ideas that that take away from idiomatic Clojure and use in other languages.
- yyyk 6y ago"The default way a Java/C# dev would probably approach this is to create a class" No, not for a long time. The idiomatic C# way would be to use LINQ which actually predates Clojure a tiny bit. Java (since version 8 in 2014) could use Streams to similar effects. Either way the solution would be as short as your example.
- adamkl 6y agoAssuming that you are mapping the results to a Dictionary, sure it would be similar. My point was more that C#/Java developers (myself included at one point in time) would typically reach for classes to store the results, rather than just a Dictionary<String, String>. It’s not that other languages can’t to this, it just that this approach doesn’t always jive with their conventions like it does with Clojure.
- Jare 6y agoI can't help but feel that all arguments defending how this Clojure solution supposedly can't be done as elegantly in other languages, use "idiomatic Clojure" as the judging criteria. Of course the conclusion writes itself.
- adamkl 6y agoWell, you can write code a lot like this in JavaScript using a library like Ramda, and get a lot of the benefits. But then everyone that you on-board to that project has to be up to speed with how the code is being written and why. With Clojure, that understanding is table-stakes. So in that respect, the conclusion does write itself.
- onetom 6y agoI've actually tried that: https://github.com/enumatech/sprites/blob/master/lib/__tests__/sprites-tests.js#L77-L82 https://github.com/enumatech/sprites/blob/master/lib/__tests... reads quite like Clojure, but looks foreign to JavaScript programmers... I even made extensions to Ramda, so you can thread a mixture of async and non-async code with it: https://github.com/enumatech/sprites/blob/master/lib/fp.js#L51-L54 https://github.com/enumatech/sprites/blob/master/lib/fp.js#L... At the end the friction was just annoying and no one else really understood the greatness of this approach, since they didn't know Clojure...
- jlarocco 6y agoHere's a more or less equivalent implementation in Common Lisp, using alist-hash-table from alexandria: (defun csv-to-maps (csv) (loop for row in (rest csv) collecting (alist-hash-table (mapcar #'cons (first csv) row) :test #'equal))) For CSVs with only a few columns (maybe less than 10?), I'd probably go with alists instead of a hash table to save memory, and it'd look like this, using curry from Alexandria: (defun csv-to-alists (csv) (map 'list (curry #'mapcar #'cons (first csv)) (rest csv)))
- didibus 6y agoIf you read Rich's paper, there's a section where it says: > maps were built behind several abstractions. Thus, maps can start with a representation optimized for small maps and switch to a representation optimized for large collections if and when they ‘grow’ beyond a certain size. Which is pretty cool, because basically the Clojure version of your code abstracts over alist-hash-table and alist, and the most optimal type of concrete map will automatically be created based on the size of the map you are asking to create.
- jlarocco 6y agoThere's actually an implementation of the same HAMT data structure that Clojure uses available in QuickLisp and on GitHub: https://github.com/danshapero/cl-hamt/ https://github.com/danshapero/cl-hamt/ Technically, since the hash-table implementation isn't specified in the CL standard an implementation could use a HAMT for the built-in hash tables, but I don't think any do.
- didibus 6y agoThe construction of a Map is abstract in Clojure. The concrete type of Map you get is not something you specify. This isn't related to the HAMT per say. So what I mean is Clojure will use an array backed map when you ask it to construct a small map, and it will give you a HAMT backed map when the map is large. This is true as well as you add elements to a map, Clojure might automatically promote the map from an array map to a HAMT as its size grows.
- gavinray 6y agoIt's less lines in JS/TS, depending on how you format the parens and braces. let csvData = [ ["Name", "Age", "Email"], ["John", 25, "john@email.com"], ["Mary", 30, "mary@email.com"], ["Anne", 40, "anne@email.com"], ] let parseCSV = ([cols, ...rows]) => rows.map(row => row.reduce((res, val, i) => Object.assign(res, { [cols[i]]: val }), {})) parseCSV(csvData)
- adamkl 6y agoYes, as I said in my original post (and others in this thread), you can absolutely come up with elegant solutions to this in other languages. In my experience though, for every one elegant solution like yours, there will be ten developers writing nested for loops. My example was never meant to be “look at what Clojure can do! You should all be jealous! “. Only an example of how Clojure encourages a different approach to solving problems that we are all familiar with.
- gavinray 6y ago> there will be ten developers writing nested for loops. Ain't that the forever truth =/ nested for loops go brrrrrr
- dkersten 6y agoIts because the default abstractions provided as part of the language shape the way developers using that language approach problems. In Clojure, the sequence abstraction has been given a lot of thought and effort, which means that’s what Clojure developers will try to use. It also Clojure comes with a rich and powerful set if sequence functions which makes using sequences a breeze and lets you do super powerful things out if the box. You could simply implement the exact same functions in other languages too and end up with similar code, butsince its not built in, most people won’t think in those abstractions.
- dunefox 6y agoI find that much less readable than Clojure.
- caseymarquis 6y agoI'm a little late to the party, but I don't think I would hire an experienced C# developer that didn't immediately know this could be a one liner: var csv = ImmutableList<ImmutableList<string>>.Empty; var map = csv.ToImmutableDictionary(y => y.First(), y => y.Skip(1).ToImmutableList()); I would hope they would then tell me that this will throw an exception when there is a repeat header in the csv data. I'm not sure the functional and safe version is that much cleaner than a for loop: var map = csv .Where(x => !string.IsNullOrWhiteSpace(x.FirstOrDefault())) .GroupBy(x => x.First()) .ToImmutableDictionary(x => x.Key, x => x.First().Skip(1).ToImmutableList()); Edit: But I would hire a developer that knew how to do the equivalent in Clojure without hesitation.
- draegtun 6y agoHere it is in Erlang, which is another language which forces you not to think in loops: csv_to_maps([Head | Lines]) -> lists:map(fun(L) -> maps:from_list(lists:zip(Head), L)) end, Lines). Another language which works differently is Rebol: csv-to-maps: func [csv-data] [ map-each line next csv-data [map zipmap first csv-data line] ] Whats interesting about above is that `first csv-data` will be re-evaluating on each `map-each` sequence. Of course you can assign to variable beforehand. Alternatively because code-is-data and vice-versa you can use COMPOSE it inline: csv-to-maps: func [csv-data] [ map-each line next csv-data compose [map zipmap (first csv-data) line] ] :) NB. Rebol doesn't come with ZIPMAP so here's a definition for above to work: zipmap: func [keys vals] [ collect [ forall keys [ keep reduce [keys/1 vals/(index? keys)] ] ] ]
- draegtun 6y agoOpps... a superfluous parenthesis sneaked into Erlang code there!! csv_data_to_maps([Head | Lines]) -> lists:map(fun(L) -> maps:from_list(lists:zip(Head, L)) end, Lines).