神刀安全网

Clojure, the Good Parts

Clojure, The Good Parts

I kid, somewhat. This title is of course based on Douglas Crockford’s seminal work, Javascript, The Good Parts , which demonstrated that Javascript isn’t a terrible language, if only you avoid the icky parts.

This is my heavily opinionated guide to what a "good" production Clojure app looks like in 2016. I love Clojure, I’ve been using it pretty much exclusively since 2009. In the 7 years I’ve been using it professionally, a consensus has started to form around what ‘the good parts’ of Clojure looks like. This article is my take on what a good app looks like.

In several places I’ll recommend specific libraries. While I recommend that specific library, there are often competitor libraries with similar functionality, and most of the time getting the functionality is more important than that exact library, so feel free to substitute, as long as you’re getting the same benefits.

To make my biases explicit, I mostly write webapps and data analysis on servers in the cloud. If you use Clojure in significantly different applications, these recommendations might not apply to you.

Recommendations are split into several categories: core language, libraries and deployment.

I’ll also try to avoid uncontroversial advice that’s been covered elsewhere, like ‘avoid flatten’ and ‘don’t (def) anywhere but the top-level’, etc.

Core

Avoid Binding

clojure.core/binding is a code-smell. Avoid it. In almost all cases, binding is used to sneak extra arguments into a function, reducing its referential transparency. Instead, pass all arguments explicitly into a function, which improves purity and readability, and avoids issues with multi-threaded behavior. If you’re dealing with optional arguments, consider an extra arity of the function with an optional map.

Avoid Agents

It’s rare to see a problem that can be precisely modeled by agents. Most of the instances I’ve used them have been as hacks to get the specific multi-threading properties I want. These days, core.async can be used to more explicitly model the desired behavior.

Avoid STM

Like agent , it’s rare to see a problem that can be properly modeled by STM, in production . Most production apps will need to persist data in a database. It’s rare that you can do ‘real’ work in an STM commit that shouldn’t also be stored in the DB. Coordinating commits to both STM and the DB is …error prone (the two generals problem).

Failing to commit to the DB is a Major Error, while failing to commit to the STM is typically less so, because the Clojure process can be restarted and load from the DB as the source of truth. Therefore, the easiest way to avoid coordination problems is to just have one source of truth, the database.

Use atoms, sparingly

By process of elimination, because I’ve just recommended avoiding binding, agent and STM, that leaves only one core mutable-state construct, the atom. Atoms are good, and should be used, but treat them like macros: only use them when they’re the only tool available.

Avoid global mutable state

In 2009, I would not have believed how little global mutable state is used in my applications. The vast majority of your state should be in the DB, or a queue, or Redis. I’m now at the point where

(def foo (atom ...)) 

is a code smell. Most of the time when using atoms, they should not be def d at the top level. They should be returned from constructor functions, or stored in Component. This means that you should end up with only one piece of global state, the system.

Avoid pmap

pmap has been subtly broken since chunked seqs were introduced to the language, and it’s parallelism is not as high as promised. Use reducers + fork/join, or core.async’s pipeline , or raw Java concurrency instead.

Avoid metadata

It’s not always obvious which functions will preserve metadata, and which won’t. As a Clojure user since pre-1.0, I’ve long stopped caring about "oh, assoc in 1.x didn’t preserve metadata, but it did in 1.(inc x)". Metadata is nice to have, for introspection and working at the repl. As a bright line though, metadata should never be used to control program behavior.

Exercise Caution with Futures

Futures are great, but they’re a potential footgun. There are a few things to watch out for.

First, always always always use java.lang.Thread/setDefaultUncaughtExceptionHandler. It looks something like:

(Thread/setDefaultUncaughtExceptionHandler   (reify     Thread$UncaughtExceptionHandler     (uncaughtException [this thread throwable]       (errorf throwable "Uncaught exception %s on thread %s" throwable thread)))) 

This guarantees that if your future throws an exception (and it will, eventually), that will be logged and recorded somewhere.

Second, always consider what you’re doing inside the future, and what would happen to the system if power was lost while the future was running. Imagine you’re running an e-commerce shop, and a customer buys something, and then we send them a confirmation email in a future. The pseudo-code would look like:

(charge-credit-card! user transaction) (future (send-confirmation-email transaction)) 

If power dies while the future is running, the customer might not get their email. Not ideal. In general, futures are a place where transactional guarantees are likely to be lost. In almost all cases, if you’re using a future for side effects (sending email, calling 3rd party APIs, etc), consider whether that action should go into a job queue. Use futures for querying multiple data-sources in parallel, and use durable queues for performing side-effects asynchronously.

Libraries

Some libraries I like, in no particular order. Note that these recommendations are about libraries to use in apps you write. I fully agree with Stuart Sierra’s position on library dependencies .

Use Component

Seriously, Component is the single biggest improvement you can make to a large Clojure codebase. It fixes testing. It fixes app startup. It fixes configuration. It fixes staging vs. production woes. CircleCI’s unit tests were a mess because of a huge amount of with-redefs and binding to get testing behavior right. If the component under test used Component, there would be no need for redefining at all. Literally thousands of lines of test fixtures would get dramatically simpler.

Use Schema

Schema All The Things . As Emacs tells me, "Code never lies, comments [and docstrings] sometimes do". Schema can reduce the amount of doc strings necessary on a function, and schema-as-doc-strings are more likely to be correct, because they’re executable (and therefore, verified). They completely eliminate that annoyance in doc strings where the doc states ‘this argument takes a Foo’, without every specifying what a Foo is. It can be used to handle input validation from 3rd parties. It can be used to prove your tests are valid (i.e. passing valid data to the function under test).

Use core.async

I’ve mentioned it several times in this post already, but core.async should be your default choice for most complex multi-threading tasks.

Use Timbre

Clojure programmers love to make fun of the horrors that are j.u.logging, logback and SLF4J. Just dump it all, and use timbre . Timbre is Clojure[script]-only logging, so it has no java vestigial tails, no XML, and no classpath weirdness.

Timbre plays well with Component, so when you have separate component systems, one for development, one for test, etc, they can use different logging policies, while both are running in the same process at the same time . Production systems log to Splunk or Loggly or what have you, while tests only log to stdout, etc.

Use clj-time

clj-time is essential for readable code that interacts with dates. Let’s say you have a DB query that takes a start and end date:

(query (-> 7 time/days time/ago) (time/now)) 

clj-time wraps Joda, which is excellent. Libraries for wrapping java 8 Instant are probably good too, I haven’t used them though.

Use Clojure.test

Your tests don’t need a cutesy DSL. Your tests especially do not need eight different cutesy DSL. .

Yes, clojure.test has warts. But it has well-known, immutable warts. Not having new releases is a feature of testing libraries.

New testing libraries are fun and exciting, until you find a bug in your test library. clojure.test works, and is battle tested in a way that no other clojure testing library is. I’ve shipped bugs to production on code that I thought was tested, because I didn’t understand the way the testing DSL works. I’ve seen infinite loops in macros shipped by the testing library, because the DSL was too cutesy and complex.

Never again, use the simplest thing that solves the problem.

Don’t wrap clj-http

This is kind of a meta point. Don’t use libraries for 3rd party APIs that provide no value on top of your HTTP client. For example, interacting with Stripe’s API is easy. Just write one helper function that merge s in whatever authentication the service needs, and then just use clj-http directly :

(def stripe-api-endpoint "https://api.stripe.com")  (defn stripe-api [auth path args]    (http/request (str stripe-api-endpoint path)       (merge {:basic-auth auth} args))) 

That is literally the entirety of what you need from a stripe API library. A library must provide significant value over just writing your own, and most libraries that wrap HTTP rest APIs don’t. A few provide nice features, like HTTP pagination, but that code isn’t that difficult to write. (Actually, it’d be interesting to see if there are REST patterns that can be abstracted into a single clj-http higher-order-function library).

Deployment

Build a single artifact

Whatever you’re building, releases should consist of a single artifact. A .jar or .war or docker container or AMI or whatever it is, it should be self-contained, and reproducible. Your process should not resolve or download dependencies at runtime. Starting a production server should be simple, reliable and repeatable. Reliable, meaning "very little chance of failing" and repeatable meaning "starting a server on one day and starting on another day should have bit-for-bit identical code".

You will need to rollback, because you will deploy bad code at some point. You need to know that the prior version still works, because you’re already rolling back production because of an error, you really don’t want your problems to get worse.

We improve repeatability by avoiding downloading or resolving anything mutable. Any scheme involving git or lein or apt-get when turning on a server is immediately suspect, and npm is right out! Downloading previously-resolved deps is better, but still not as good as baking the deps directly into your artifact. That guarantees that even there is an npm-style disaster, your old build still works.

Avoid writing lein plugins

During the lein 1.x days, plugins were the standard way of adding functionality to your build. The combination of lein run and :aliases has changed that. Whenever possible, write a standard clojure function, then add it to :aliases in your project.clj:

{:aliases "build-foo" ["run" "-m" "rasterize.build.foo/build-foo"]}

Typically, the only reason you’ll need a plugin is to control the classpath.

Standard functions are much easier to write, test, run, and chain together. Chaining clojure functions is just do . Chaining lein plugins is lein do , which is slower and awkward, and can’t (easily) be done from the repl or other functions.

Prefer Clojure Over Build Tools

In the last section, I said ‘prefer Clojure functions over lein plugins’. Now I’m also saying ‘prefer Clojure functions over bash and most CLI tools’. Obviously some allowance needs to be made for tools that are very hard to replace, but your asset fingerprinting probably isn’t one of them. Clojure is an incredibly powerful language. Most of the time, you’ll get more power, flexibility and insight into your build process if you use clojure code over command line tools.

For example, Rasterize’s asset compilation, fingerprinting and deploy to S3 are all standard Clojure functions, using less4j java.io and AWS libraries. The Rasterize static site generation ( .md to .html , etc) is all clojure. These are functions that I can run from the repl, and debug pusing standard clojure tools. I haven’t used boot in anger yet, but I’m supportive of its philosophy.

Conclusion

And there we have it. A haphazard collection of poorly justified opinions. Let me know if you agree or disagree on Hacker News.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Clojure, the Good Parts

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址