Last week, I was looking to some old code I wrote in my last job and my spare time. Then, I’ve decided to publish two new libraries for Clojure and ClojureScript.
One is Paprika, available in Clojars at version 0.1.0-SNAPSHOT.
The other is Check, also available in Clojars (but at version 0.0.1-SNAPSHOT).
The reason for the early publishing is to push forward some simple libraries to fix a simple problem that I had while working with Clojure: the absence of abstractions.
Paprika
Let’s for example, look at the marvelous clj-time library: it wraps Joda Time, but it’s kinda difficult to understand which code generates UTC or Local Times, how to convert between then, or to coerce things… it’s mostly an API to work with Joda to make things “less clumsy”. But I’ve came from a Ruby background, where working with dates is incredibly simple. For instance, to pick up current time, in UTC, and add 3 days to it, we’ll use: Time.now.utc + 3.days
Now, with Paprika:
(require '[paprika.time :as time]) (-> (time/now) (time/plus (time/days 3)))
Ok, it’s a little more clumsy because of the lack of monkey-patch, but it’s not too far behind of ActiveRecord’s code. Also, because of the way Java works, we’ll probably have to transform to, and from SQL. This is also very simple, and if we :refer
some functions, some calls are identical to the Ruby version:
; Will convert to SQL and back: (-> (time/now) time/to-sql time/from-sql) ; 3 days ago (-> 3 time/days time/ago) ; Or you can `refer` time functions: (require '[paprika.time :refer [days ago]]) (-> 3 days ago) ; 3.days.ago.utc in Ruby ; Or, let's say that we're reading a date from ; a string, and we want to interpret it as UTC ;
Also, it eases uses with Prismatic Schemas:
(require '[paprika.schemas :as schemas]) (def PersonSchema {:name schemas/NonEmptyStr :age schemas/PositiveInt :account-amount BigDecimal :credit-card (schemas/digits-string 16)}) (def as-person (schemas/coercer-for PersonSchema)) ; Will coerce "90.29" to 90.29M. Also, will remove time-of-birthtime-of-birth (as-person {:name "Szabo" :time-of-birth #time/utc "1990-10-20T10:00:00Z" :age 30 :account-amount "90.29" :credit-card "1111222233334444"}) (def as-person2 (schemas/strict-coercer-for PersonSchema)) ; This will throw an exception: time-of-birth is not permitted (as-person2 {:name "Szabo" :time-of-birth #time/utc "1990-10-20T10:00:00Z" :age 30 :account-amount "90.29" :credit-card "1111222233334444"})
Check
Don’t you love Midje? Well, I do. But there are three problems: first of all, the infamous Midje could not understand something you wrote
. Also, the lots of magic that Midje uses (like, facts
will re-write almost all your tests so that arrows are in the right position…. this means problems when things don’t go the way you want). But the worse of all is that it’s Clojure-only, and there are no alternatives for ClojureScript except… clojure-test.
I’m not gonna measure my words: I really hate clojure-test. I think it’s wayyyy too simple, it’s really strange to work with async tests in ClojureScript (because it does not have a timeout – it just hangs forever), and finally their assertions – when there’s an error, they just don’t help you to understand what’s happening. I also like expectations library, but I think it’s too opinated… so, why not join the two?
(require '[check.core :refer [check]] '[clojure.test :refer :all] '[clojure.string :as str]) ; With Midje (facts "uppercases a string" (str/upper-case "foobar") => "FOOBAR" [:some :strange :vector] => (contains :vector)) ; With clojure-test (deftest upcase-string (is (= "FOOBAR" (str/upper-case "foobar"))) (is (some #{:vector} [:some :strange :vector]))) ; With check (deftest upcase-string-with-check (check (str/upper-case "foobar") => "FOOBAR") (check [:some :strange :vector] =includes=> :some))
But where it shines is that: first, arrows are extensible: we just need to implement check.core/assert-arrow
to define a new “arrow” that will be checkable too. This method will receive the “left” and “right” part of the arrow, quoted, and it’s your responsability to return a syntax tree that’ll return a clojure-test map:
(require '[check.core :as check :refer [check]] '[clojure.test :refer :all] '[clojure.string :as str]) (defmethod check/assert-arrow '=begins-with-foo=> [left _ right] `(let [to-check# (str "foo" ~right) unq-left# ~left] (cond (= unq-left# to-check#) {:type :pass} (str/starts-with? unq-left# "foo") {:type :fail :message "Begins with \"foo\", but doesn't matches the rest" :expected to-check# :actual unq-left#} :else {:type :fail :message "Doesn't begin with foo" :expected to-check# :actual unq-left#}))) (deftest not-foo (check "lolcat" =begins-with-foo=> "cat")) (deftest dont-match (check "foobar" =begins-with-foo=> "cat"))
If I (run-tests)
in the code above, it’ll fail with:
FAIL in (not-foo) (form-init1668407501325729845.clj:59) Doesn't begin with foo expected: "foocat" actual: "lolcat" FAIL in (dont-match) (form-init1668407501325729845.clj:62) Begins with "foo", but doesn't matches the rest expected: "foocat" actual: "foobar"
Second, that it runs on ClojureScript too!
Conclusion
These are just two minimal approaches to make Clojure a great and expressive language. I encourage everyone that works with Clojure to do the same, and also to open pull requests (or make suggestions) on these libraries.
We need better abstractions, ones that will made our life easier. We need to give Clojure a Ruby “feel”. It’s possible: Elixir does it, but I still think that Clojure have more potencial. Let’s make use of it!
1 Comment
Clojure does NOT need a “definite web framework” – Maurício Szabo · 2022-07-31 at 02:21
[…] this to say: you don’t need to wrap libraries. Most people I know, me included, said sometime that Clojure needs more libraries that wrap over other other things, and while I still would love to just stay […]
Comments are closed.