Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Clojure is Capable (ahungry.com)
198 points by ahungry on Dec 28, 2018 | hide | past | favorite | 56 comments


So Clojure and CLJS are amazing. If I were to pick a Lisp for a real world project, Clojure is the obvious pick. The thing is, I'm curious to hear from people who DO use Lisp in the real world.

I would characterize my experience with it as 1: Really fun. 2: Productive on a small scale due to the tooling, interop, libraries, and community being innovative and awesome, and 3: Infinitely flexible, because you know .. it's a Lisp. Development becomes like doodling on a piece of paper, like a stream of consciousness.

But I ended up choosing Scala for a recent, fairly large, project of mine and I'm convinced it was the right decision. Without a static type system I would have simply lost a handle on the complexity of the project. And this is while working alone (the case where Lisp traditionally has the upper hand).

A few questions for people who use Lisp in production. No need to answer all, just things I'm curious about:

* During refactoring, do you feel more flexible, or less? I can see both sides to the argument. On one hand, static types are more rigid, and getting something to compile during a refactoring can be a pain. Often for the right reasons, but often it's due to some annoying technicality in the type system. But once you get there, "if it compiles, it runs" is a cliche that is pretty accurate in my experience. On the other hand, with a dynamic type system, you can slowly evolve a code base and see incremental progress during a refactoring, but I can never get over the dreadful feeling of "what did I forget?". Has clojure.spec worked out in terms finding bugs during refactoring/maintenance and general QA?

* Do you make use of the Lispy-ness (Homoionicity, macros, code is data, etc ...) or is it generally an anti-pattern to use these language features in daily development?

* Do you wish you had static types? If so, have you tried gradual typing like Typed Clojure? And if you don't miss types, why? I remember someone saying of Typed Clojure "We went to Clojure to get away from types!". And I'm still trying to understand the mindset.

I often have the feeling that I'm not smart enough to use Lisp. If I had 2x the brain power, I would be able to use macros to conjure up some black magic in 100 lines what would take a mortal 5k in a lesser language. But even if I manage such a feat, I couldn't imagine bringing on another developer to that codebase.


We've used Clojure in production for years.

1. "Has clojure.spec worked out in terms finding bugs during refactoring/maintenance and general QA?" - Spec worked out as a much more useful library than we expected. You could describe any data structures with it, then validate, generate, regenerate it. I think spec is a must have for any Clojure programmer from 2018.

2. "Do you make use of the Lispy-ness" Macros are super useful sometimes, but in general we try to avoid them, because we prefer to solve everything with just pure functions. Homoiconicity and the "code is data" aspect are must haves in any programming language after you realised their power.

3. "is it generally an anti-pattern to use these language features in daily development?" macros are feel like cheating sometimes, and we fear the consequences, but homiconicity is useful for us on a daily basis.

4. "Do you wish you had static types?" We prefer to create our own "types" up to complex data structures. A built-in type system seems like a downgrade from here.

5. "I often have the feeling that I'm not smart enough to use Lisp." I think you are just scared from the unknown, Clojure is an alien technology compared to most of the currently popular languages.

Any kids could learn programming in Clojure due to its simplicity and the REPL driven workflow. Its an interactive experience and very likeable.


Can you elaborate a bit on your own "types"? I am very interested and intrigued.


Yes, I could show you a simple example:

Let's create our own 78 digits long string type:

(def hex-digit (set "0123456789"))

(defn hex-digit? [x] (contains? hex-digit x))

(defn hex-str? [s] (every? hex-digit? (seq s)))

(s/def ::hash (s/and hex-str? #(= (count %) 78)))

::hash is now registered, now we go to the REPL:

(s/valid? ::hash "23179372")

;;=> false

(s/valid? ::hash "nope93721907914920047210715459933122004671648400678953445710500236944435987060")

;;=> false

(s/valid? ::hash "231793721907914920047210715459933122004671648400678953445710500236944435987060")

;;=> true

That's cool, but we want to generate too:

(defn hex-str-gen [n] (let [digit (g/elements hex-digit)] (g/fmap clojure.string/join (g/vector digit n))))

(s/def ::hash (s/with-gen (s/and hex-str? #(= (count %) 78)) #(hex-str-gen 78)))

We extended our ::hash spec with a generator function, so we can do this now:

(g/generate (s/gen ::hash))

;;=> "310584715385467847758653938894742415543975445356609397864862925839413265904779"

(+ Pro tip: you could spec functions too)

Learn more about spec here: https://clojure.org/guides/spec


I don't fully get the syntax (I've had a tiny bit of Scheme experience) but wow that's awesome.


Very interesting! Thank you!


Could you describe examples of using homoiconicity daily?


90% of our codebase is in databases. Less than 10% belongs to namespaces and being compiled after you boot up the system.

This is not a conventional way at all. It would be really hard to explain properly, but I have a simple example for you: The REPL

It means: Read -> Eval -> Print -> Loop and is a very basic and common thing that relies on homoiconicity, haha


90% of codebase in database? I've never heard of this before. I'm extremely curious, could you explain more? What are the tradeoffs and why did you guys go with this approach for the problem you're solving?


I use Common Lisp rather than Clojure at work.

- During refactoring, do you feel more flexible, or less?

A lot of it depends on how the code has been written. There are definitely parts of our code base that I really dread going near and there have been plenty of times when I have broken stuff by making a change without fully understanding the code. It is quite easy to write incomprehensible lisp code.

With Clojure being more pure and opinionated things are probably not quite as hairy as it can get in CL. But if you write your code well CL can be fairly easy to refactor. Generally preferring to use CLOS (the object system) rather that just property lists and hash tables will give you some typing information that can make life a lot easier. Avoid nested lists as data structures - there is nothing worse than code that does stuff like `(dosomething (cadar list) (cddar list)`

- Do you make use of the Lispy-ness?

Macros tend to only be used for writing framework code. There is generally no need to use them when writing business logic.

The aspect of Lisp that is often not mentioned is the iterative development. When code is data it becomes possible to evaluate code against a running system. This is, in my opinion, the nicest feature of Lisp. No compile times, you can change code on the fly, evaluate and test functions as you are writing them. Development becomes a real conversation with the compiler. You can even connect into the live server if needed to debug problems.

- Do you wish you had static types?

Yes.


Clojure is a great language. I've used both Clojure and Scala in my personal and professional life.

* In refactoring, I almost always make mistakes in Clojure that Scala will catch. I feel empowered rather than limited by the Scala compiler 9 times out of 10.

* Homoiconicity makes me feel great, but I find I don't actually miss it (directly) in Scala. That said, Scala has a complex grammar, and I miss the overall consistency of s-expressions.

* I always miss static types. I'd love to try the new spec improvements in Clojure 1.10; debugging mistakes prior to that are a nightmare. I don't need static types to be happy, but I do need real tooling support when things go wrong.

Clojure has a lot of lifting power and I would like to keep using it. I find that once I get past type errors, programs work very well. Why would I spend time getting past those if I don't have to?


Yeah, I agree with this, other than not having used Scala. Static typing communicates intent to the user of your functions, they help you think about the problem before going all-in.

Rich presented some new ideas for where he wants to take Spec, but I didn't think anything very new made it into 1.10 regarding spec.

Personally I use clojure whenever I can. I can quickly build up a project using the fast and loose data types, and when my conception of the project solidifies, I convert over to records and protocols. That's usually enough to get 80% of the way there. The free and loose ad-hoc data structures get me across the finish line, and it's there that Spec comes into play.


Yeah I think I was thinking of the error message improvements rather than any changes that are specific to Spec in 1.10.


I programmed Java for years before I started using Clojure so I will make that distinction here to answer your questions

> During refactoring, do you feel more flexible, or less?

Both. More flexible because in Java you have 5x more code (was worse before lambdas). Less because I sometimes look up the code and don't know (and can't immediately infer) the types anymore. Usually I have a handful of integration tests that show me immediately when there is a problem but I wish there was a way to show the inferred types in my IDE.

The problem is though usually only a big one for the one big state where you have lots of nested maps, vectors, arrays and so on and only if you modify the structure of this state. For smaller things, fixes are fast and easy, even though they still take 10 minutes while they could take 5. I feel it's kind of the price I pay to use Clojure but then again there's probably a reasonable solution to it - Clojure infers the types anyway, so why not show them somehow.

I use clojure.spec partly, often when I need to throw useful error messages (e.g., a user facing REST API). It's a little bit tedious but it's always a good decision in retrospect.

> Do you make use of the Lispy-ness?

Yes and no. on the positive side: I don't ever need to look up the syntax of anything (I still make mistakes in Java switch for example) and using case and multi-methods I can really interpret quickly a DSL in a web server (e.g., binary arithmetic operations on OHLC bar time series) and I wouldn't want to implement that (safely) in another language.

> Do you wish you had static types? If so, have you tried gradual typing like Typed Clojure?

I tried for a brief two days gradual typing and didn't get it. I wish I had static typing exactly when refactoring or debugging.

> I often have the feeling that I'm not smart enough to use Lisp.

Oh, I'd even say the opposite, i.e., I am smarter because I use Lisp.

I know Sapir-Whorf is "deprecated" but I feel I am thinking faster because I am thinking in much less convoluted structures compared to when I programmed in Java.


I know this isn't answering a question you asked, but next time you feel like diving into clojure, focus on Records and Protocols. I think you'll get some relief from the free fall feeling.


At a Clojure meetup I attended a couple years ago, a long-time Java programmer asked "how do you manage huge projects without static types?"

The best answer I could think of was, "don't manage huge projects." Instead, divide projects as early as possible in development into supporting libraries, each capable of being independently tested and maintained.

In my experience, applications developed with this attitude coalesce themselves into a small ecosystem of mostly generic library code with a domain-specific apex "application" entrypoint to bind them.

In an application factored like this, type checking isn't something you want as much anymore, since the constituent libraries interface with one another at a level above. They present and consume "public" (intra-application) APIs, the behavior of which are defined not just by types but by at least documentation and tests. The thing that static types let you do -- have huge amounts of code, woven together with structural dependencies -- is no longer a thing you need or want. It would seem bad, actually, since what you'd have is an inscrutable mass only a machine can understand.

I haven't worked enough with large non-Clojure codebases to say whether or not this approach is superior, but I can say that it scales. Another nice thing about it in a business setting is that supporting libraries lend themselves to open-sourcing, and a relationship with an open source community is a good thing for a company's development efforts/hiring/contracting in my experience.

To answer your specific questions:

* I don't think I write more unit tests (I should probably write more tests in every language I use)

* I don't have any experience with clojure.spec

* All of the features of Lisp work together to make it awesome, including macros. In particular, in the context of an "ecosystem of libraries" approach, macros are a fantastic tool for exporting a DSL from a supporting library. They're also use for DSLs in the apex code, where application-specific behavior can be defined in terms of a DSL.

* I don't really wish I had static types - I love Lisp the way it is. But I have found various languages for structural checking useful with certain kinds of sub-program, like receiving data from the outside world, and for writing compilers. If you write a compiler as a series of transformations, it's nice if the shape of the code between transformations is well-defined somehow.

You're definitely smart enough to use Lisp. I think the key to success is not necessarily smarts, but attentiveness to modularity or cognizance of the difference between "library" and "application" types of code.

* There's a Paul Graham essay related to this idea: http://www.paulgraham.com/progbot.html

* I think this approach is applicable not just to Lisp, but maybe to any dynamic language with a sufficiently rich set of generic collections and with the concepts of a "module" or "package".


Wonderfully stated.

One thing that deep, static type systems often lend themselves to is complex types, and those complex types lead to complicated designs; and if I have the option, I'd rather spend my time designing a simple architecture than exhaustively testing and validating a complicated one.

Since using clojure (roughly the last 3 years in data-pipeline style architecture) I try to follow a principle of writing functions that do just one thing, simply, and do it well, keeping the interface as basic as possible. Almost all of the time when writing clojure, I'm writing functions that act on a couple of built-in types and return one built-in type, usually the same one as before.

Additionally, I try to isolate side effects.

Following these principes, I find that even when I'm dealing with a stinker of a namespace or running into a runtime error, it's pretty cheap to try it out in the REPL and get to refactoring.

That said, I kind of want my cake and to eat it too. I do miss static types -- just not as much as I missed them when I was writing Javascript or Ruby. If I write a new clojure library now, yes, I would try to instrument it (at least the public api) with clojure-spec. If I could re-design clojure, I'd leave out null and maybe build a more intuitive interface to clojure-spec to use it as a kind of progressive validation system via annotations and type hints.


> static type systems often lend themselves to is complex types, and those complex types lead to complicated designs

I don't know how to call this. But it's something I find true too. And to a point, it feels like a lot of businesses leverage that as a justification point for their cost.


It reminds me of a great quote from the book Computing Fundamentals [0] regarding Wirth's design philosophy:

>Wirth’s philosophy of programming languages is that a complex language is not required to solve a complex problem. On the contrary, he believes that l languages containing complex features whose purposes are to solve complex problems actually hinder the problem solving effort. The idea is that it is difficult enough for a programmer to solve a complex problem without having to also cope with the complexity of the language. Having a simple programming language as a tool reduces the total complexity of the problem to be solved.

[0] https://www.cslab.pepperdine.edu/warford/ComputingFundamenta...


I'd add that most solutions to complex things require a tiny abstraction that brings beauty and ease, not the feeling you get in a messy warehouse.


Thanks for the insightful answer! Building an ecosystem of libraries to do most of the heavy lifting is generally what I strive for as well. It's certainly the goal, and I'll usually end up there for a given project. Perhaps one thing that makes me fond of static types is that, more often than not, I don't actually know how or what to modularize until after the project is nearly built in the first place. And I use types not only as a QA tool, but also as a tool for reasoning. The feedback loop between me encoding logic in types, and the IDE providing me with errors, is a big part of how I understand what I'm building, as I'm building it. I imagine that Clojure development on the REPL can actually serve the same purpose.

I think the idea of bottom-up programming also resonates with me, though I tend to find it hard to use as a general approach to programming. In my mind, it's similar to the development of DSLs, which I do fairly infrequently, and with care. There is an undeniable elegance to it ... find a set of primitive operations that can be composed into larger ones. My development style usually goes in either direction: Either super generalized, for which I'll take the bottom-up DSL approach, or the more typical case where it's more approachable for me to be explicit with types/code.


Myself, I sort of do both bottom up and top down programming.

Usually I start at the top and write out high level code as if everything I needed magically existed. That helps me think about what I want and how I think I'll use it and that helps and clues me in to what low level stuff I need to build. Then I start to think about how to build those things and that usually goes bottom up.

I do this iteratively. When I get stuck I go back to high level top down, figure out what I'm missing, go build that, and repeat.


That's called Ping-pong Design, and is a perfectly legitimate approach.


My approach to modularizing from the start is to do bottom up development, and to treat each namespace as a little library. Ultimately, pretty much all problems can be viewed as data transformation pipelines, and those naturally map to independent components doing different kinds of transformations. I find that the APIs tend to form naturally around domain boundaries as you move data from one context to another.

The REPL is indeed a major component of doing the Clojure workflow where you literally run each function as you're writing it. This approach also encourages refactoring as you go since you can make changes and see the results immediately.


I feel more flexible, clojure.spec helps a lot and is a wonderful tool to add to your reflection/parsing/validation/testing toolboxes. The REPL driven development and immutability defaults means it isn't saved in a file until in runs.

Clojure idioms put those black magic macros or even complex ones in the "last resort" category. It is very much focused on representing problems as data then composing general functions to work with them. That is ranked as more elegant than the macro driven dsl style and similar ones. They are still used and are very useful.

I find types are one thing among many, all affecting each other, when it comes to how productive I am and how much I enjoy it. Clojure is unreasonably effective and spec solved the class of problem that I would have liked to have gradual types for so I don't miss them no. Gradual typing is cool though, I'm sure I'd make use of it.


"Not smart enough to use Lisp." I hear that more often and I do not understand it. I chose Common Lisp (CL) because it is an easy language not because it's hard (I would have chosen Haskell or C++ then).

I'm not sure I can answer your questions. While I do use CL at work for smaller jobs I've only used it for bigger personal / solo projects, so I'm not sure that counts.

About refactoring: it's not a thing that comes up very much. Not in the sense that it was a thing like when writing enterprise Java. Maybe because I try to keep my code composable I don't have to change it in many places? But this isn't limited to Common Lisp for personal projects: also bigger projects with multiple persons at work in C++, Python, etc. refactoring isn't such a big thing that I'd use it as an argument when chosing programming languages.

Also once your code settles down, when you're going from the exploratory phase to the "let's make this ready for maintenance"-phase, CL has the option to annotate functions with types. Not at the level of Haskell but close enough to catch most things.

That's what I like about CL: it's very practical and multi-paradigm. It doesn't force into a specific paradigm like object oriented or whatever fad comes next.

I never have that feeling "what did I forget?". I'm sure I do forget things, but by building up code function by function and testing those functions one gets a pretty good feel for the code base. BUT, I've never worked on CL projects with multiple people, so I get where you come from.

I love being able to use macros and the Lisp syntax is one of its main appeals to me. It is so simple and straight-forward. Algol-like languages with their curly braces, brackets, statement-ending, etc. etc. feel like such a chore. I try not to overuse macros, in fact I rarely use them but they're there when I need them and make the language very powerful and, if used correctly, more readable and easier to understand.

"Code is data" … Do you mean something like this?: http://www.moserware.com/2008/04/towards-moores-law-software...

I guess I've been using Lisp long enough that I don't quite get what it means but I use it automatically.

"Do you wish you had static types?" — Not really, like I said one can add garnish to the CL code to get something that is good enough, but even then I rarely use it. However, I only use it in mission-critical stuff and I don't write much of that.


> Maybe because I try to keep my code composable I don't have to change it in many places? But this isn't limited to Common Lisp for personal projects: also bigger projects with multiple persons at work in C++, Python, etc. refactoring isn't such a big thing that I'd use it as an argument when chosing programming languages.

I find refactoring is the most important part of programming; as the problem (or one's understanding of it) evolves, the code that describes it needs to evolve to match. When I worked in Python I found that I hit a wall where as my codebase got bigger, changes to my model took longer and longer to apply, because I had no way of reliably knowing what was coupled to what else; types solved that.

(I like lightweight syntax but not to the extent of giving up infix operators/methods)


"Without a static type system I would have simply lost a handle on the complexity of the project."

A hardcore Clojure developer will probably tell you that Spec is, in some sense, even better than having a static type system. I haven't used Spec, so I can not say. But I do find, when I'm writing Clojure code, I write pre-conditions on every function, so every function has a contract. This goes a bit beyond what you can get from static type checking, in the sense that I'm testing all of my assumptions, including key names in maps, or at least, this is less work.

There is also the advantage of testing against real world data, instead of mock data. I've had the problem, when using Java, that all the unit tests passed, but the app died in the real world, because the real world data (perhaps from a 3rd party API) was different from our mock data. In such a case you need run time contracts to help find the problem, and static types offer no help (depends on the project really, but the more your app interacts with 3rd parties, the less useful static types are).

I'm not proposing this as The One True Way, but I would propose this as the Really Easy Way to write sane code. I don't have to think about it. It's like writing a comment for the function, but it gets enforced and can not possibly go out of date without giving an error. It documents everything the function is expecting.

An example:

    (defn push-sentence
      [current-database-connection permanent-id map-of-sentence]
      {:pre [
             (not (nil? permanent-id))
             (not (nil? map-of-sentence))
             (map? map-of-sentence)
             (:sentence map-of-sentence)
             (:belongs-to map-of-sentence)
             ]}
      (mc/update
       current-database-connection
       "reports"
       {:permanent-id permanent-id}
       {operators/$push {:sentences map-of-sentence}}
       {:upsert true}))

    (defn push-transcript-processed
      [current-database-connection transcript]
      {:pre [
             (not (nil? transcript))
             (map? transcript)
             (:permanent-id transcript)
             ]}
      (mc/update
       current-database-connection
       "reports"
       {:permanent-id (:permanent-id transcript)}
       {operators/$set {:processed :true}}
       {:upsert true}))
This also gives me good error messages which makes debugging easy. For instance, if this fails:

             (not (nil? map-of-sentence))
I get an error about it being nil, whereas if this fails:

             (:belongs-to map-of-sentence)
I get an error about the missing key. This helps me narrow down the errors very fast.

I keep the functions short, which feels natural in Clojure.

Again, this isnt' the One True Way. Realy Clojure programmers will tell you to use Spec, or maybe Typed Clojure.

But I will say, heavy use of pre-conditions gives me the right balance between the contract enforcement I want, while still giving me the flexibility of a dynamic language. I like Java, but often the verbose quality of a static typed language is just not something I need.


My team has been using Clojure for close to a decade now. We have apps that have been in production for many years, and are still under active development.

- refatoring

I don't really find it to be problematic with dynamic typing. I tend to rely on specification tests and spec to test functionality at the API level. If the API behaves according to the specification, then the code can be said to be working as intended.

- homoiconicity

Absolutely, my team gave a talk about one of our projects recently https://www.youtube.com/watch?v=aXBe6hoi-Mw

The project heavily relies on homoiconicity as the business logic for each form is encoded using EDN and stored in the database.

- static types

I've primarily worked with statically typed languages prior to Clojure. I've worked with Java for close to a decade, then used Haskell for about a year, and some Scala.

My experience is that dynamic typing is problematic in imperative/OO languages. One problem is that the data is mutable, and you pass things around by reference. Even if you knew the shape of the data originally, there's no way to tell whether it's been changed elsewhere via side effects.

What I find to be of highest importance is the ability to reason about parts of the application in isolation, and types don't provide much help in that regard. When you have shared mutable state, it becomes impossible to track it in your head as application size grows. Knowing the types of the data does not reduce the complexity of understanding how different parts of the application affect its overall state.

My experience is that immutability plays a far bigger role than types in addressing this problem. Immutability as the default makes it natural to structure applications using independent components. This indirectly helps with the problem of tracking types in large applications as well. You don't need to track types across your entire application, and you're able to do local reasoning within the scope of each component. Meanwhile, you make bigger components by composing smaller ones together, and you only need to know the types at the level of composition which is the public API for the components.

The other problem is that OO encourages proliferation of classes in your code. Each class is its own DSL with an ad hoc API, and knowing the behavior of one class doesn't tell you anything about that of the next. Keeping track of that quickly gets out of hand. With Clojure, you're working with a common set of data structures and using standard library functions to manipulate them.

REPL driven development also plays a big role in the workflow. Any code I write, I evaluate in the REPL straight from the editor. The REPL has the full application state, so I have access to things like database connections, queues, etc. I can even connect to the REPL in production. So, say I'm writing a function to get some data from the database, I'll write the code, and run it to see exactly the shape of the data that I have. Then I might write a function to transform it, and so on. At each step I know exactly what my data is and what my code is doing.


> What I find to be of highest importance is the ability to reason about parts of the application in isolation, and types don't provide much help in that regard. When you have shared mutable state, it becomes impossible to track it in your head as application size grows. Knowing the types of the data does not reduce the complexity of understanding how different parts of the application affect its overall state.

Ironically, I find having types allows me to reason more about an app in isolation. Pick any function in isolation with a typed language and I know the shape/contents of the data I'm dealing with. I know what these things "are". With something like Clojure and no Spec, I have no idea what I'm dealing with until I find the source of where the original data came from. Having worked on an application that used a Clojure library that read sql from a file and translated the queries into maps, I had to look at the sql file every time to make sure I had the data I thought I did. In my experience, worrying about what could be changing the data is overrated. In a typed language, I know exactly what's changing the data since I can statically guarantee what other parts of the codebase are mutating it. It can definitely become a problem with concurrent code, but that happens so rarely in the codebases I've worked on that isn't really a concern and there are ways to mitigate it when it is.


I don't find knowing the shape of the data to be problematic in the applications I work on. I find the REPL tends to play a big part for me here. Any time I develop a feature, I run the code to see what it's doing. So, say I'm adding a new feature that needs some data from the database. I'll run the query to get the data and see it, then I'll write a function to transform it in the desired way, run it, see the output, take the next step, and so on. When I'm updating or changing the feature, I do the same thing. I'll get the application in the desired state, find the spot where I want to make the change, run the current code, and work from there.

Meanwhile, without immutability the types do little to help with tracking side effects which create implicit coupling. To know whether it's safe to change data, you have to know how it's used in every location it's referenced. This completely precludes the ability to do local reasoning about your code. This is a completely separate problem from concurrency.


I've worked on numerous Clojure projects, both personal and professional. I started most my learning on Lisp, so I'm probably not the most qualified to discuss this outside of the thought that many people come to Lisp and Clojure with approaches that aren't entirely compatible with Lisp. I know my thoughts are going to be controversial, so take it for what it's worth.

During refactoring, do you feel more flexible, or less? I can see both sides to the argument. On one hand, static types are more rigid, and getting something to compile during a refactoring can be a pain. Often for the right reasons, but often it's due to some annoying technicality in the type system. But once you get there, "if it compiles, it runs" is a cliche that is pretty accurate in my experience. On the other hand, with a dynamic type system, you can slowly evolve a code base and see incremental progress during a refactoring, but I can never get over the dreadful feeling of "what did I forget?". Has clojure.spec worked out in terms finding bugs during refactoring/maintenance and general QA?

I think there is good and bad to this, but in my experience, using a lot of types seems to be an anti-pattern. Since most web-based projects really only deal with strings, basic numbers, lists and dictionaries, I really don't see the entire benefit, even in strongly-typed languages. I work with typed languages as well and generally feel ambivalent about types.

A bad refactor is a bad refactor and bad code is bad code no matter what language you are using. If you can't understand what the code is doing, don't touch it until you can test against it or test against what it's really supposed to be doing. Types don't alter this fact.

Do you make use of the Lispy-ness (Homoionicity, macros, code is data, etc ...) or is it generally an anti-pattern to use these language features in daily development?

Homoiconicity is the very definition of Lisp, so there is no way to work around that.

Macros are generally eschewed in Lisp. It's glaringly obvious when and if you need to use a macro, and honestly, most people can go their entire career without pushing a macro into production.

Code is data is pretty hand-wavey in my opinion. All it's really saying is that, if you call a function with a value, you get a value back. There is no mental quantum leap needed for this.

Do you wish you had static types? If so, have you tried gradual typing like Typed Clojure? And if you don't miss types, why? I remember someone saying of Typed Clojure "We went to Clojure to get away from types!". And I'm still trying to understand the mindset.

I don't think the community has any strong for-or-against opinions on types. In general, it's bad practice to use multiple-type functions. The perspective that's generally embraced is that each function does exactly one thing, which is why you see so many Lisp projects with many short functions, and if you pay attention, the functions aren't doing a lot of acrobatics based on possible types.

What the community doesn't like is someone coming along and demanding something that isn't compatible. This is a little more nuanced, but it boils down to: "Add types if you want me to use it" or "Create a RoR system for me to use it." The community would rather you take the time to embrace Lisp and really try to understand what it is attempting to do before pushing your judgment on it.

I often have the feeling that I'm not smart enough to use Lisp. If I had 2x the brain power, I would be able to use macros to conjure up some black magic in 100 lines what would take a mortal 5k in a lesser language. But even if I manage such a feat, I couldn't imagine bringing on another developer to that codebase.

Anyone can use Lisp. Most of what you do is very simple. The entirety of Clojure is written on a single webpage. https://clojure.org/api/cheatsheet

No one is building up systems with loads of macros and wild abstractions that cause a 5KLOC code base to shrink to 100 lines. This just isn't possible, and anyone that claims it is has no understanding of what a macro actually does.


> Macros are generally eschewed in Lisp. It's glaringly obvious when and if you need to use a macro, and honestly, most people can go their entire career without pushing a macro into production.

'generally' and 'mostly' wrong, I'd say.

> Code is data is pretty hand-wavey in my opinion. All it's really saying is that, if you call a function with a value, you get a value back. There is no mental quantum leap needed for this.

Does seem completely unrelated. Code is data means that source code has a non-trivial direct external and internal representation other than text strings (in Lisp typically s-expressions / nested lists of atoms) and operations over it: reading, printing, destructuring, constructing, evaluation, transforming. This enables programmers to easily write programs which write programs.


>If you can't understand what the code is doing, don't touch it until you can test against it or test against what it's really supposed to be doing. Types don't alter this fact.

Many developers in real projects don't have the opportunity to to work with good code, most of the time you have to work on projects started by others, with a language that is not the one you prefer, with a framework you probably dislike etc, having the language and tools help you figuring how a thing works and how can I fix a bug without having to understand the whole project, creating new unit tests from scratch is very important,

let me give you an example that happened to me, so I work on a SPA app , JS, angular, jQuery (project started by others, I had no choice in frameworks or languages) , I want to fix and improve things to make the users have a better experience(I don't do it for the pleasure or writing code). So I want to change a text that appears on an element, so I get the element ID, it's class, search the code, I find the element but I could not find where the element text was changed. It took me hours (honestly) to find it, dev used the feature in js where you can dinamicaly access object properties and did something like object["select_"+type+"_box"] you could use same feature to call functions obj("function"+"Name").apply... My point is that with languages that let devs do dinamic shit like this you are never 100% sure that your refactor will work, I can't be 100% sure if it is safe to remove this function that appears unused, or to remove this css rule that also appears unused, I prefer 1000 times more an environment that I can be 100% sure that I can remove or rename code. (I know about reflection and similar in static languages but normal projects do not use that)

I am wondering if you never worked with this kind of code bases and this experience is not familiar to you, also if someone will say that I shyould find a job that works with cool language or framework X , I don't , I care about the result of my work, I take pleasure when I fix some hard bug or when a nice feature or improvement lands and our users are happy.


I think a lot of Clojure developers have worked with gnarly codebases before, probably for the bulk of their careers. It's a common reason for using Clojure in personal projects, and seeking out Clojure work.

There are numerous ways to find satisfaction in work. I'd merely extend the reasons you list to include an enjoyment of the tooling and software you're working with. Might as well try to tick all the boxes.


My opinion is that for your personal project, that only you work on is fair to use whatever.

For work projects you can't just join a team and ask let's rewrite it in X. Some developers will try to get satisfaction with the latest and greatest but this is a moving target, a few years back C# was cool, then it was cool to know Scala because is functional, now is cool to uise Rust, GO or some compile to JS and react or similar frameworks, when this are no longer cool this developers leave and somebody else will have to take over and meintein their amateur code (by definition you can't be experienced in new thing since is new)

Anyway I got off-topic, my point is that refactoring your personal project is not the same as refactoring. cleaning up a big old project(in my case 4 year old angular1 project , that probably has a few deprecated dependencies that are no longer cool enough). It would be better a bit to work with a better code in TypeScript and latest React but I personally won't hunt for a new job because of the tools.


Scala is a great language for large software projects. The only downside really is compilation speed/tooling.


This is wonderful! Just what I was looking for in terms of real world Clojure.


I really like clojure as a language, but it seems like the bus factor is very problematic.

Observing the community I see rich hickey, as well as cognitect as a whole SPOFs, which hold me back from devoting more time to it.


There are many companies that have invested their core business in Clojure, and the language has been around for a decade. There is absolutely no risk here. If Rich disappeared tomorrow, there would be no lack of people ready to step in and continue working on Clojure. In fact you can already see that happening with ClojureScript where there is a lot of community involvement in the development.


It's open source so, if things go awry, anyone with a dictionary and a passion for pulling things apart can take over ;)


Interesting blend of raw clojure, java interop, os integration... very very nice


I still miss the proper Java interop. Calling SAMs is not fun and I could not figure out the proper way of make it work from Clojure. We ended up needing to write a Java wrapper.


Do you mean having to reify java.util.function.* classes?



For any project bigger than a certain threshold, I always prefer Scala over Clojure or even Java. For smaller projects, Clojure fits well.


Every large project can, and should, be broken down into small isolated projects that are developed and maintained independently.

Furthermore, I think that writing software using monolithic design is bad practice. It creates coupling within the project, and it makes it harder to reuse code. It's harder to onboard new people, and so on. However, the biggest problem is that it creates a lot of communication overhead for the team. The more people you have working directly with each other, the more emails, meetings, and other kind of communication you end up incurring.

Splitting things up into small components that are each maintained by separate teams of 5~6 people is a far more productive way to maintain large software projects than to create monoliths.


Your arguments hold true for a regular run of the mill software project. Many real world projects are hardly ideal.


Using static typing as a crutch to work around poor project architecture doesn't actually solve the problem. Also, there is a question whether static typing is what contributes to projects being written in a monolithic style in the first place. I often see this to be the case for projects written in statically typed languages, while ones written in dynamic ones tend to be broken up much more aggressively from the start.


> Interactive REPL based workflow (think of programming in Clojure like playing a difficult video game based on trial and error with emulator save states vs without).

What?

I'm not sure I understand what that's even trying to say?

Is it trying to say, think of programming as playing a difficult computer game, and using a REPL like having save states?

...because it sounds like you are saying programming in Clojure is like playing a difficult video game based on trial and error, which is a really rubbish endorsement.

> To touch on the video game analogy - imagine playing one of the old Mega Man games, Dark Souls, or Battletoads. Now, imagine that instead of getting feedback (a death) that results in a hard restart at the beginning, you instead just get taken back one action / event that caused an error (a single failed function call in REPL). That's like loading a save state, and if you like easy mode, you'll love the Clojure REPL.

?

mmm... but if your analogy is 'this is like doing something really hard, but you can timestep back to save state so it's not so bad', then you're still basically saying writing Clojure is really hard.

I don't think that's really a fair thing to say about Clojure, it's just a terrible analogy.

Expressing things in Clojure is what it excels at; if you want an anology the REPL is more like a minecraft sandbox that you just continually keep building in, rather, than planning out your structure on paper before you start playing.


It's saying programming is really hard, but Clojure gives you save states.


Well most of the people write code in small chunks that yields to the following workflow:

- write code

- compile & run, see if it works

Just keep repeating this until you achieved what you want.

In REPL this is exactly the same you just have a much faster way of trying out a concept.

I think this is pretty simple to conceptualise.


Obviously, but that's not the point I was making.

...but how does that (what you wrote) == "think of programming in Clojure like playing a difficult video game based on trial and error with emulator save states"?


- each iteration in the repl is a state that you can preserve ("save")

- playing the vide game is trying out new states

I guess this is what OP was thinking about.


I've had this same thought while working with immutable data structures. Maybe the author is referring to that.


As I understand the analogy: the (difficulty of the) video game is the problem domain.

Clojure + REPL is save states / time travel

(could be clearer though)




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

Search: