9 ms·
Design Patterns in Clojure
- yenda 11y agoI would love to have some Java programmer opinion on this piece.
- daxfohl 11y agoI'll bite, downvotes notwithstanding :) One advantage of the Java is that the interfaces are named, type-checked documentation. Yes it's more verbose, but a maintainer of that code can understand exactly the intention just from the interface names, without having to decipher it line-by-line. For the Clojure code, after you've added enough comments to explain what you're doing, so that the code can be reasonably maintained, then it's the same size as the Java code, but without the type safety. Granted for some of the simpler patterns like strategy, Java's verbosity just gets in the way, though much of that has been simplified in Java 8. FWIW I prefer Scala over both in all cases. But if Scala didn't exist, I'd prefer Java for the enterprise use case, and Clojure for the small applet use case.
- bpicolo 11y ago> it just doesn't work at an enterprise level Elaborate?
- yenda 11y agoJudging by his comment I have the feeling that entreprise level means huge projects with thousands of lines of bloat code and comments which need vast amount of underpaid programmers to analyse and debug just to add minor features.
- grayclhn 11y agoWhy do you assume they're underpaid? :)
- daxfohl 11y agoBig, not-very-interesting CRUD app. Every time the data model changes, you've got to go through manually and find all the places the code needs changed, and hope you didn't miss anything. It probably didn't help that it was running on MongoDB, so two levels of dynamic typing and impossible to remember what went where, and a DB that takes whatever you give without complaining. So no compile errors and often no runtime errors either, just incorrect behavior. Switched to Scala and SQL, and code is almost as compact but typed relations are far easier to reason about; if a relation changes then you change the core type and follow the compile errors. Once it compiles, it usually works. We've not missed Clojure in the least. Scala (and even Java) simply feels more structured, with relations between types, the resulting app more "designed", which seems necessary at the enterprise level. In my 2 years, Clojure constantly felt like just hacking. Would love to see some examples of Clojure being used well at an enterprise level so I could understand better how to do so, but CircleCI's recent flopping between core.typed and prismatic just makes me think it's a square peg in a round hole.
- RussianCow 11y ago> Every time the data model changes, you've got to go through manually and find all the places the code needs changed, and hope you didn't miss anything. Isn't that what tests are for?
- daxfohl 11y agoNo, that's what types are for. Types are exactly for this. They're far less verbose than tests, they prevent compilation completely on error, they're integrated with your code, they're 100% comprehensive, and they don't contain bugs. Contrarily, tests don't work at all for model changes because the test still retains the old model anyway.
- catnaroek 11y agoI agree with your sentiment - why bother testing something that we can get the compiler to prove, once and for all? However, in Java, a typeful model of a program's specification is bound to be: (0) Imprecise, because of the holes in Java's static safety. You can establish a convention that statically unsafe features are verboten, but it will remain just that - a convention. (1) Verbose, because Java can't express even the simplest ideas without using at least 5 classes: Finite case analysis? Visitors. Higher-order functions? Strategies. Persistent data structures? Memento. Things don't get better if we move to a higher architectural level: Embedded DSLs? You need a separate class for each production rule in your abstract syntax. Type-safe database queries? Java has no structurally typed records, so you need to make a custom POJO class for every query that returns different columns. These things are incredibly unpleasant to deal with, and while it might be true that "unpleasant work builds character", I don't think it should come as a surprise that some people say "enough is enough", and seek alternatives that minimize or at least reduce their pain (like dynamically checked conformance to a schema), even at the cost of sacrificing some type safety (dynamic checks can't enforce higher-order invariants).
- erichmond 11y agoI worked in java for 6 years before moving to clojure. Your experiences don't remotely reflect mine, I can't think of a single reason I'd ever choose java over clojure anymore (as tooling matures) but I appreciate you taking the time to comment!
- d_t_w 11y agoWorked with Java from '97, Clojure for the last two years, agree with you completely.
- js8 11y ago> For the Clojure code, after you've added enough comments to explain what you're doing, so that the code can be reasonably maintained, then it's the same size as the Java code, but without the type safety. Funny, I feel the same about design patterns. So you bothered to find an abstraction in the code, you described it, gave it a name, and yet - you didn't tell the computer - here I am using this abstraction! And you rely on other humans to correctly infer what abstraction you used from reading your code, instead of just telling them. So what should really be done, design patterns, once identified, should be replaced with proper abstractions in the programming language (mostly functions?) and die. Frankly, most of them are just really fancy names for trivial things.
- daxfohl 11y agoTotally agree. A fair amount of that is done in C#, with proper events, observables, enumerables, and functional constructs. Really when I say Java previously I mean C#; I haven't worked in Java proper in over a decade, so it's entirely possible I forget how bad it was.
- kilink 11y agoI like Clojure quite a bit more than Java, but some of these are unfair if you are comparing against modern Java. For instance, the sorting example can be simplified: Comparator<User> userOrdering = Comparator.comparing(User::isSubscription).thenComparing(User::getName); // forward sort Collections.sort(users, userOrdering); // reverse sort Collections.sort(users, userOrdering.reversed()); A lot less verbose than it's made out to be.
- codahale 11y agoWhile it’s definitely the case that there are more concise domain-specific examples, as you’ve pointed out, the article’s point stands: in Clojure, a Strategy is simply passing a function as an argument to another function.
- phamilton 11y agoThe caveat is perhaps that in Java 8, a Strategy is simply passing a function as an argument to another function too. I'd love to see patterns struck down as obsolete with Java language updates.
- TwoSheds 11y ago+ Finding or creating suitable functional interface for passing the function
- Dr_tldr 11y agoEvery "language comparison" between dominant ones and less popular ones (even implicitly) ends up being a little contrived, which is unfortunate because in general Clojure is the real deal.
- arsenerei 11y ago> Reverse sorting should keep subscripted users on top. Will userOrdering.reversed() account for this?
- rfw 11y agoI love the idea! A few examples are a bit strawmanish but really when you're comparing languages like that who can resist the strawman. The Clojure solutions do sacrifice a lot of safety though, due to dynamic typing (e.g. the visitor pattern, which relies on basically strings to do its dispatching).
- jph 11y agoBrilliant-- this is among the best design pattern writeups I've ever seen, and will greatly help developers who are coming to Clojure. This would be excellent for expanding to book-length guide to help people learn functional programming along with the patterns. Thank you Misha for writing this!
- richcole2 11y agoReally nice writeup of patterns. What turned me off Clojure is that the dynamic typing makes it very hard to refactor. e.g. even renaming a function is really quite difficult, let alone adding function parameters. Forget extracting a function. The dynamic typing also means a typo can yield a nil that travels far from the mistake site making debugging difficult. There are hints towards better debugging in clojure, but last time I checked it was still very much a work in progress. Also lazy lists are easy to screw up, e.g. if you put a concat inside a reduce then stack go boom. Still this is a great post because it teaches you both good Clojure and the design patterns. Since java has closures now you can take a lot of what you learn from Clojure and put it in your well typed and refactorable Java programs.
- yenda 11y agoRefactoring is a breeze when your editor is connected to the repl, you should try clj-refactor for instance. I'm not sure what you mean with the typo story since most of the functions in your program should be free of side effects you should be able to test them as you write them.
- daxfohl 11y agoI'm always curious about the latter sentiment. Maybe I just haven't been on enough interesting projects, but I've never had a program that didn't have/need side-effecting code nearly every other line. Upload something to S3, if X then call some web service, otherwise log and call something else, etc etc etc. Pure data manipulation has always been a fairly minor component limited to dashboard statistics for me. Is purity that really your experience? What industry / types of projects?
- brandonbloom 11y agoThinking in data & functions just takes some practice. Traditionally imperative code can often be reimagined in terms of reducing over a command sequence, or interpreting some more sophisticated representation. The benefits for implementation, testing, debugging, refactoring, etc can be massive. Consider your chain of examples for "Upload something to S3, if X then call some web service, otherwise log and call something else, etc etc etc." You could represent this as a sequence of steps which could be interpreted by a central side-effecting loop. Let's call it an "action plan", in this case for your "upload" operation: (defn upload-plan [req] [{:action :upload :file (get-in req [:params :doc]) :max-size 2000 :name :doc} {:action :notify :message (fn [state] (get-in state [:data :doc])) :target [:somebody]}]) Now you can process this plan centrally: (defmulti take-action (fn [state {:keys [action]}] action)) (defmethod take-action :upload ...) (defmethod take-action :notify ...) (defn execute [plan] (let [{:keys [status]} (reduce take-action {:status :ok, :data {}} plan)] (when (not= status :ok) (log "omg!")))) While this is initially a tad more verbose, there's great reasons to do this sort of thing. For example, now have a central place to mock all side effects! You could substitute a different execute method during testing, or automatically log the "action" object that failed. More importantly, you can develop each of your "plan" functions independently. You can reuse them, re-order them, etc. You can also do hypotheticals, for example: "Which plan would cause less network round trips?" can be answered before committing to a plan. You can _record_ plans. Instrument your execute method to record and save plans to a file, then you can just check them right in as test cases. You can optimize plans. If five different functions all notify the same target of the same thing, your central execution method can filter subsequent calls. Or you could separate your "optimization" from your "interpretation" and you can ask "After accounting for idempotence, which operations will be performed?" The list goes on and on...
- greydius 11y ago> Eve: Stop talking, show me the code! I like Eve.
- js8 11y agoExcellent, definitely going to read this. I would like to see this for Haskell, though. And some Lisp idioms (from On Lisp, for example) could be thrown in for a good measure, too.
- moomin 11y agoI'll just plug my own (not as comprehensive) contribution to this meme: http://www.colourcoding.net/blog/archive/2014/10/27/design-patterns-happy-birthday-and-goodbye.aspx http://www.colourcoding.net/blog/archive/2014/10/27/design-p...
- moomin 11y agoWhilst I agree in general with the sentiment, it really doesn't help if you start with a misunderstanding of the command pattern. The essential property of command is that it's data that can be executed. Implicitly, it's possible to do other things with them: log them, undo them and so on. A better model would be a record that implements IFn.
- 0x0dea 11y agohttp://wordsmith.org/anagram/anagram.cgi?anagram=SerpentHillREE&t=1 http://wordsmith.org/anagram/anagram.cgi?anagram=SerpentHill... Subtle.
- OmarIsmail 11y agoI think the Visitor example is a mistake. Couldn't you replicate the Clojure version in Java by having a "Formatter" singleton that has a function for each of the different item type/format type combinations?
- js8 11y agoI don't think so, the whole point is that Clojure will select the correct function dynamically (that's what multiple dispatch is about). In all cases you need 4 different functions (for each type/format combinations).
- patrickmay 11y agoPeter Norvig covered this two decades ago: http://norvig.com/design-patterns/design-patterns.pdf http://norvig.com/design-patterns/design-patterns.pdf Paul Graham mentioned Norvig's work in 2002: http://www.paulgraham.com/icad.html http://www.paulgraham.com/icad.html
- deleted 11y ago[deleted]