24 ms·
Prior to Clojure I feel that I didn't really know how to do things well. In the context of the domain I have the most experience with, web applications, I'd su
by Naomarik 6y ago
Prior to Clojure I feel that I didn't really know how to do things well.
In the context of the domain I have the most experience with, web applications, I'd summarize the bulk of programming as defining data, data transformations and shoving data through APIs. Once you get the hang of it, Clojure makes it trivial to transform datastructures from A to B. This has enormously expanded the kinds of problems I can solve.
When Rich says
"It was impossible to
avoid the sinking feeling that I had been “doing it wrong” by using C++/Java/C# my whole career."
I feel somehow that this is the case for the majority of people but they don't realize it yet because their experience is the most popular trendy language or framework. I've seen many examples of libraries in different languages having enormous amounts of commits and hundreds issues for problems that are trivial to solve in Clojure.
I was in the same boat, constantly grabbing for frameworks and if one wasn't available that made my task simple, would struggle trying to bend the frameworks or language I was using to come up with a solution.
I'm not a language geek and I don't write code outside my work for fun. I want to spend the least amount of time possible in front of the computer and sleep knowing my web applications won't topple over. Clojure has fit me so well that I don't think I would have accomplished what I have in other languages.
- natdempk 6y agoCould you elaborate on a problem that illustrates this property of Clojure? This sounds awesome, but I have a hard time understanding what you're getting at without knowledge of Clojure.
- Scarbutt 6y agoNothing really different from writing functional JS with lodash and being conscious about immutability (or use immerjs). For macros you can use Babel.
- Myrmornis 6y agoAre you speaking from experience of Clojure?
- Scarbutt 6y agoYes, heavily. Clojure influenced(mostly because for Rich Hickey talks) many parts of the JS functional programming ecosystem. Many functions in lodash and other libraries(immutablejs etc...) are somewhat directly or indirectly influenced from ideas Clojure help make more mainstream. Even Java the language itself has been heavily influenced by Clojure. So this ecosystems implement the best ideas from niche influential languages, you can use Clojure's "programming model" without leaving the productivity and practical benefits of a big ecosystem. This has been more prominent in JS since the language lends itself better to FP.
- Myrmornis 6y agoBy "experience of Clojure", I meant experience of Clojure.
- gmfawcett 6y ago"By 'X', I meant X" doesn't help to explain what you mean. Can you clarify?
- Myrmornis 6y agoThe first X was in quotes, so it was a purely lexical reference to the words themselves, without referring to any conventional meaning that those words might have. The second X was unquoted and so by that I meant to convey the usual meaning in English. Another way of saying it would have been: erm right but experience of Javascript is not experience of Clojure, regardless of any influence Clojure may have had on modern Javascript.
- gmfawcett 6y agoOK. But you didn't ask him to describe his Clojure experience, you just asked whether he was speaking from Clojure experience -- and he said that yes, he was. Asked and answered?
- devin 6y agoThat's kind of like saying you can write immutable data structures and then bolt on STM and try to force yourself to stay in functional territory as much as possible in any language, which the paper addresses. You can do this, and Rich did it in C#, but in my experience it's not long before you start to have clashes of philosophy with other libraries you depend on and with the language itself, not to mention people you work with. Believe me, I tried on one project to write as Clojure-y in Ruby as I could, and as soon as you reach for a library you need to interact with that doesn't play by the rules, things get weird if you want to keep things functional and immutable all the way down. You need to flagrantly violate established idioms in the core of the language, and often you wind up reimplementing things in the core of the language. People who aren't familiar with why you wrote it a particular way will come in at some point later and imperative or OOP it up.
- dominotw 6y agoRich hickey explains at this timestamp in the video, https://youtu.be/2V1FtfBDsLU?t=2426 https://youtu.be/2V1FtfBDsLU?t=2426 "The information programming problem" Whole video is gold, is my fav programming talk of all time.
- drcode 6y agoThree examples of this: 1. core.async: So the guys who built golang did it because they had this cool idea for creating coroutines using channels. However, since it required very low-level functionality to be part of the core of the programming language, they thought they'd have to design a brand new language to implement their idea. However, shortly after golang was released, Rich and some other clojure folks implemented the same feature in clojure as a totally ordinary external library, proving that the core of clojure was general enough to support this. And it wasn't just a gimmick: I use core.async every day and think it is better than golang's implementation. 2. The Expression Problem: One of the core challenges in language design is designing it so that (simplifying a bit) you can transparently add support for new function methods to objects designed by a third party. Clojure makes this easy https://www.infoq.com/presentations/Clojure-Expression-Problem/ https://www.infoq.com/presentations/Clojure-Expression-Probl... 3. Lots of languages have attempted to allow you to write a single library that you can use both on the client and the server, having your code transpiled to different languages in each case. However, this type of feature is rarely used in production, because there are usually lots of headaches involved, with many limitations. However, for clojure developers it is perfectly normal (and usually expected) that all code is written in cljc files & so that it can be used on both the client (transpiled to javascript) and the server (transpiled to java). It is easy to do this, even for cooperatively multithreaded code.
- hy3rm0n10us 6y ago2. requires static typing in my personal opinion cf. http://lambda-the-ultimate.org/node/4136#comment-62959 http://lambda-the-ultimate.org/node/4136#comment-62959
- mumblemumble 6y agotl;dr on the above: The Expression Problem was originally expressly formulated with maintaining a Haskellian level of static type checking as one of its core requirements. Saying you've solved it, when your solution involves on dynamic typing, or even casts in an otherwise static language, is arguably akin to kicking the ball across the center field line and then claiming you've scored a goal. (See: http://homepages.inf.ed.ac.uk/wadler/papers/expression/expression.txt http://homepages.inf.ed.ac.uk/wadler/papers/expression/expre...)
- Naomarik 6y agoI think Reagent would be the easiest and most visible example. Reagent was my intro to React, I had no experience prior nor did I care to learn it at the time. The reagent wrapper around react made it so intuitive to use that I was writing productive (albeit warty code because I had 0 experience with functional programming/lisp/clojure) my first day. I had loads of experience writing Javascript, but at that time React looked cryptic to me without reading any documentation and Reagent did not. Another commenter mentioned lodash. If I was forced to use JS directly, lodash would be the first thing I reach out for to solve problems. But in Clojure you also get "cljc" which is a file extension that lets you use code both in Clojure and Clojurescript land simultaenously. With this you can write things like form validation code that's exactly the same front and backend.
- dgb23 6y ago(not OP) I've got a completely contrived example for you, but those are things that are typically easier to do in Clojure than in the languages I know otherwise: Let's say you're writing a web-application that consumes JSON form an external source. You have to send out responses that accept and reject requests and you add some meta-data. You have to meet a bunch consistency guarantees in your business logic like: a person cannot buy more than X products at discount Y in a time-period Z. Or some other arbitrary rules. The external source decides to add an additional field in their JSON tomorrow. With Clojure this isn't considered a breaking change, you don't change a single line of code since you don't care about their new field at all. Next week, the external source has a bug, they send out messages that are well-formed, but inconsistent with your business logic. You already wrote function specs for those and your messages automatically "explain" why your spec deems those inconsistent. Again, you don't change a single line. A month afterwards the maintainers of the external source went through a major "refactoring". Now they changed the structure of their messages towards you and you are forced to be compatible. In Clojure this is a matter of adding a new spec and moving around some expressions, or better: composing a new (v2) top level function to meet the new spec. Again, this is hyperbole, but there is some truth in it as well.
- yawaramin 6y agoNone of this sounds like a problem in a statically-typed system with proper data modelling. In fact for the complete refactor of the external source we would do something very similar to Clojure, i.e. update our decoders for the external data. In fact I'd argue it would be even easier because the compiler would help check all the data is accounted for.
- dgb23 6y agoI agree with you in the sense that these things “done proper” would be robust. It is meant as a pointer to what is specifically one strength of Clojure in comparison. From my experience marshaling/deserialization and bubbling up errors as an introspectable useful message are things that are more involved in statically typed languages than in Clojure. Typically statically typed languages are very good at internal consistency. But require more ceremony and maintenance regarding outside sources. This is a viable tradeoff in some cases but not so in others.
- adamkl 6y agoHere 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.
- jonahbenton 6y agoThe are aspects of solution shapes that Clojure enables that are unique and mostly completely different than other languages, but it is hard to appreciate them if you don't know it. One's intellectual appreciation is literally limited to the languages one speaks (both human- verbal and eg visual, like art, and computer programming). I look at the benefits more concretely. It is usually possible to express a solution in Clojure with a fraction of the keystrokes another language requires, and Clojure also gives you knobs to control how many keystrokes you devote to the solution vs the stuff under the solution. You can be all solution domain and be really lean, or you can make some reusable infrastructure to make the solution-side shorter, at the cost of something bigger overall. The secret of good writing is usually distilled as- tell less, say more. Take away, cut, remove, until you get to the essence. So many languages REQUIRE you to tell more, with ceremony and patterns and boilerplate. Clojure has some too, but for any given semantic intention, it generally requires the least ceremony. It is breathtaking to wield a tool that lets you say, in a couple hundred lines, what it takes many thousands in other languages. It is also hard, and such tools are sharp and can be humbling. But when you arrive at a place where there is no more to take away- I have not felt that "wow" in another language in quite the same way.
- didibus 6y agoI feel the other answers to your comment failed to really illustrate the property. The history paper Rich Hickey wrote alludes to this throughout it all. Basically, data is central, everything about the language makes modeling data simple, first class, and non-ceremonious. You can take the data your domain relies on, and use it directly, in the same structure the domain structures it, no abstraction needed, no mappings, no adapters, just straight up. It's really hard to communicate what a data centric approach feels like. But I will try. Keep in mind, there are a multitude of details and features across the entire language which all focus on this data centric approach, and they all serve a role and come together to create this property. It is not any one feature, but really the sum of all of them which enable the property to surface. I will mention only a few to give an idea. #1 Flexible data representation Imagine we have a business, and they have product listings, and their products are uniquely identified by vendor and name. In pseudo-model we would have: vendor+name -> product Now in Clojure we can model this as: {[vendor, name] product} That's it. This will now be the data-structure we will use. We're now going to write a bunch of operations over it which map to the operations the business does in its day to day with regards to its product listing. Think about how you'd model that in other languages. In JavaScript, which also has a pretty flexible data model, this won't work, because keys to JS Objects cannot be a composite. One would need to have a string encoding and some mapping from the input to the string encoded variant and back, such as: {"vendor_name": product} In Java, one would refrain from using a Map<List, Product>, and instead would model this as classes. Maybe you'd have: public class Listing { String vendor; String name; Product product; } List<Listing> listings; But now you can't easily lookup for a product by vendor+name. So maybe one would instead do: public class ProductId { String vendor; String name; } Map<ProductId, Product> listings; Except it turns out that for certain products, but not all, the business also distinguishes them by manufactured year. And some other are distinguished in addition not by manufactured year, but color. In Clojure you'd just have: {[vendor, name] product} {[vendor, name, color] product} {[vendor, name, manufactured-year] product} In Java you'd now have: public class ProductId { String vendor; String name; String color; String manufactured-year; } Map<ProductId, Product> listings; Where color or manufactured-year can be null. And maybe you don't find the Java one that bad quite yet. So let's now talk about a second data centric feature: #2 Value semantics It turns out, in Java, the above code does not work. If the business says, find me the product Kraft MacNCheese, you might be tempted to do: ProductId productId = new ProductId("Kraft", "MacNCheese"); listings.get(productId); But the productId you created is not equal to the productId of equal value which is currently stored in your Map. Two objects in Java are equal if they are the same object, not if they have the same value. In Clojure: (get listings ["Kraft", "MacNCheese"]) That's it. In Clojure, things of the same logical value are equal by default. As such, Clojure has value semantics. Or in other words, equal data is equal, as it should be in my opinion. #3 Data is same in memory and out So we've come to the point where we want to export our listings. In Clojure we'd do: (spit "/home/user/listings/my-listings" listings) That's it. And when the user wants to import listings: (-> (slurp "/home/user/listings/my-listings") read-string) Because all data in Clojure can be serialized and deserialized as is by default. This gives us back our exact listings data-structure that we had. #n And like I said, there's many more such little features all thought of from a data centric perspective. When you add them all up, you start to realize everything is just data needed to be manipulated from one shape to another, and moved from one place to another.
- rhlsthrm 6y agoWhat stack do you use for web application development for clojure?
- Naomarik 6y agoMost notable things I use are Re-frame frontend and Datomic for back.
- ashtonkem 6y agoI’m in the opposite boat. I went from Clojure to Java. I got really tired of “finishing” my work, only to discover that it was riddled with bugs from all kinds of weird edge cases. My favorite is having to use (map vec <thing>) all over the place, because the seq abstraction leaks like a sieve. To be fair, I also worked in a domain that was extremely poorly suited to Clojure. The “data first” approach of Clojure falls to pieces the moment you have a bunch of data that’s syntactically similar, but semantically different. This requires a lot of cross-method coordination, creating a huge mess and a risk of missed cases.
- Naomarik 6y agoEveryone's problems, context, and experience is different, but from where I stand it sounds like you may have had an a better experience using a data spec library like Metosin's malli. One of the pain points I had before was different kinds of data flowing through my system. Clojure spec never made complete sense to me and was a bit difficult to write specs that matched my data. It was easier to write data that matched the specs. Malli does this a lot more easily with human errors that have made my life enormously easier. It's been a game changer for me.
- ashtonkem 6y agoI don’t know Malli; and I haven’t used Clojure since 2017, so things might have changed. So if I’m misunderstanding the purpose of that library, my mistake. The issue wasn’t that we would use the wrong data in the wrong place. The issue is that we had a bunch of data that was syntactically similar (huge overlap in keys), but semantically different (key numerical results must be calculated differently based on the minor differences between maps). So the issue is less “the right data got to the wrong place” and more “in this data pipeline there are multiple places where the same data must be treated differently based on minor differences”. This is actually pretty tricky to do in a maintainable way in Clojure; technically possible but really hard to avoid breaking when you need to extend it. In the end it actually turned out to be an issue that classes and objects solved brilliantly. The solution was to have the pipeline accept an interface (syntactic similarity) and have different classes for the behavior (semantic differences).