A library with various basic utilities for programming with Clojure.
active.clojure.monad/run-monadic-swiss-army
was renamed toactive.clojure.monad/run-monadic
.
- For an RTD record
MyRecord
,(MyRecord :meta)
will no longer return a meta data map. Use(meta #'MyRecord)
instead.
- Clojure version 1.9.0 or higher and Clojurescript version 1.9.542 or higher are required.
- The namespace of ClojureScript's
define-record-type
has changed fromactive.clojure.record
toactive.clojure.cljs.record
. - To make sure that the right
active-clojure
version gets picked up by Leiningen, you should exclude previousactive-clojure
that are included in the dependencies transitively by adding:exclusions [active-clojure]
to libraries that come with the dependency. When in doubt, checklein deps :why active-clojure
. - Since selectors are now lenses by default, the previously used "lens triples" are no longer valid. You need to remove the parens and the third element and use the selector instead of the name of the lens everywhere in your code.
The active.clojure.record
namespace implements a
define-record-type
form similar to Scheme's SRFI
9.
Example: A card consists of a number and a color
(ns namespace
(:require [active.clojure.record :as r]))
(r/define-record-type Card
(make-card number color)
card?
[number card-number
color card-color])
;; Creating a record with field values 3 and "hearts"
(def card-1 (make-card 3 "hearts"))
;; Get number of this card via selector
(card-number card-1)
;; => 3
;; Predicate test
(card? card-1)
;; => true
(card? "3 of Hearts")
;; => false
You can provide additional options in an option-map as second argument to define-record-type
.
By providing a value to the option key :spec
, a spec for the record type is created.
The fields of records can also be "spec'd" via meta information.
(spec/def ::color #{:diamonds :hearts :spades :clubs})
(defn is-valid-card-number?
[n]
(and (int? n)
(> n 0) (< n 14)))
(r/define-record-type Card
{:spec ::card}
(make-card number color)
card?
[^{:spec is-valid-card-number?} number card-number
^{:spec ::color} color card-color])
(spec/valid? ::card (make-card 5 :hearts))
;; => true
(spec/valid? ::card (make-card 5 "hearts"))
;; => false
(spec/explain ::card (make-card 5 "hearts"))
;; => val: #namespace.Card{:number 124, :color "hearts"} fails spec: :namespace/card
;; predicate: (valid? :namespace/color (card-color %))
To use spec/def
, spec/valid?
, and spec/explain
you have to require clojure.spec.alpha
in your ns
form.
You also get a spec for the constructor function. If instrumentation is enabled
(via clojure.spec.test.alpha/instrument
), the constructor is checked using the specs
provided for the selector functions:
;; Does not get checked without instrument.
(make-card 20 :hearts)
;; => #namespace.Card{:number 20 :color :hearts}
;; Now, with instrumentation.
(clojure.spec.test.alpha/instrument)
(make-card 20 :hearts)
;; => Spec assertion failed.
;;
;; Spec: #object[clojure.spec.alpha$regex_spec_impl$reify__2436 0x31346221
;; "clojure.spec.alpha$regex_spec_impl$reify__2436@31346221"]
;; Value: (20 :hearts)
;;
;; Problems:
;;
;; val: 20
;; in: [0]
;; failed: is-valid-card-number?
;; at: [:args :number]
If you provide a value (uid) to the nongenerative
option,
the record-creation operation is nongenerative i.e.,
a new record type is created only if no previous call to
define-record-type
was made with the uid.
Otherwise, an error is thrown.
If uid is true
, a uuid is created automatically.
If this option is not given (or value is falsy),
the record-creation operation is generative, i.e.,
a new record type is created even if a previous call
to define-record-type
was made with the same arguments.
Default is true
.
If you provide the key:val pair :arrow-constructor?
:false
,
the creation of the arrow-constructor of the defrecord
call is omitted,
i.e.
(define-record-type Test {:arrow-constructor? false} (make-test a) ...)
won't yield a function ->Test
.
Default is true
.
If you don't want your records to implement the Map-protocols (in Clojure
these are java.util.Map
and clojure.lang.IPersistentMap
, in ClojureScript
IMap
and IAssociative
), you can provide the key:val pair
:map-protocol?
:false
to the options map.
There are a number of interfaces, that our records defaultly implement (like
e.g. aforementioned java.util.Map
). Providing key:val pair
:remove-interfaces
:[interface1 interface2 ...]
will prevent the
implementations of the given interfaces.
You can implement protocols and interfaces with the
define-record-type
-statement:
(defprotocol SaySomething
(say [this]))
(r/define-record-type Card
(make-card number color)
card?
[number card-number
color card-color]
SaySomething
(say [this] (str "The card's color is " (card-color this))))
(say (make-card 3 :hearts))
You can also override the defaultly implemented interfaces/protocols by the same means. You don't have to provide every method of a default interface, those left out by you will remain the default ones.
By default define-record-type
generates new types in the host
language (Java for Clojure or JavaScript for ClojureScript), just
like defrecord
does. That can be changed by specifying either
:java-class? false
, or rtd-record? true
options like so:
(r/define-record-type Foo {:rtd-record? true}
...)
These records have the advantage, that a hot code reload of the same definition will not create a new type in the host language. So record values created before the code reload are still compatible with the record type, unless its fields have changed of course.
You cannot define protocol implementations for these kinds of record
types, but you can use multi methods. Use the defined type and the
result of r/record-type
as the dispatch value for that.
Above options may not work with RTD records:
- Arrow: RTD records don't provide an arrow constructor
- Map implementation: RTD records don't implement the map interface
- Interfaces: No interfaces are implemented, you cannot provide your own implementations for RTD records
You can provide meta data via (define-record-type ^{:foo "bar"} MyRecord)
. This meta data is then "inherited" to all created symobls
(like ->MyRecord
).
If you use an RTD record (:java-class?
, :rtd-record?
options), this data
is also retrievable via (meta #'MyRecord)
.
You can provide a binding name to the option key :projection-lens-constructor
to create a
[[active.clojure.lens/record-lens]] constructor for the record that is bound to the supplied
binding name. For example:
(define-record-type Pare
{:projection-lens-constructor pare-lens}
kons
pare?
[a kar
b kdr])
(let [data {:pare {:a "Foo" :b "Bar"}}
l (pare-lens (lens/>> :pare :a) (lens/>> :pare :b))]
(= (pare "Foo" "Bar") (lens/yank data l)))
The active.clojure.lens
namespace implements lenses. Lenses
provide a subtle way to access and update the elements of a structure
and are well-known in functional programming
languages.
If you want to update only one field in a record, it is cumbersome to write out the whole make-constructor expression:
(r/define-record-type Person
make-person
person?
[name person-name
age person-age
address person-address
job person-job])
(def mustermann (make-person "Max Mustermann" 35 "Hechinger Straße 12/1, 72072 Tübingen"
"Software Architect"))
(make-person "Max Maier"
(person-age mustermann)
(person-address mustermann)
(person-job mustermann))
With lenses you can set and update fields easily:
(lens/shove mustermann
person-name
"Max Maier")
(lens/overhaul mustermann
person-age
inc)
Note: The lens
functions don't alter the given record but create and return
a new one.
You can even combine lenses to update records inside records:
(r/define-record-type Address
make-address
adress?
[street address-street
number address-number
city address-city
postalcode address-postalcode])
(def mustermann (make-person "Max Mustermann" 35
(make-address "Hechinger Strasse" "12/1"
"Tübingen" 72072)
"Software Architect"))
(lens/shove mustermann
(lens/>> person-address address-street)
"Hechinger Straße")
The active.clojure.condition
namespace implements conditions,
specifically crafted exception objects that form a protocol for
communicating the cause of an exception, similar to the condition
system in R6RS Scheme.
The active.clojure.config
namespace implements application
configuration via a big map.
The active.clojure.debug
namespace implements some useful debugging
tools such as a macro pret
that prints and returns its argument.
The active.clojure.match
namespace provides some syntactic sugar
for map matching around core.match
.
The active.clojure.functions
namespace provides the same higher order
functions that clojure.core
does, but implemented via records and
IFn
, so that the returned "functions" are =
if created with =
arguments.
These can be very handy for using React-based libraries like Reacl, which can optimize work based on the equality of values.
An example usage of the active.clojure.monad
namespace can be found at https://github.com/active-group/active-clojure-monad-example
The active.clojure.validation
namespace provides utilities for
applicative data validation. It is useful to create validation
functions that collect all errors that occured (as opposed to finding
only specific or one error) in a purely functional way.
The main building-blocks are the validate-*
functions and
validate
.
An idiomatic example, hiding the actual record constructor and exposing only a validated record constructor:
(ns validation
(:require [active.clojure.record :as r]
[active.clojure.validation :as v]))
(r/define-record-type Config
^:private make-config config?
[host config-host
port config-port
mode config-mode
admin-users config-admin-users])
Here, we define the record-type Config
. We want to have the
following rules:
- The
host
is a non-empty string - The
port
must be an integer between 0 -- 65536 - The
mode
must be one of:dev
,:test
, and:prod
- the
admin-users
must be a sequence of non-empty strings.
First, we define a validator for ports which is not already included in the library:
(defn validate-port
"Given a `candidate` value and an optional `label`, validates that
`candidate` is in [1 65535]."
[candidate & [label]]
(v/make-validator candidate
(fn [candidate]
(and (< 0 candidate)
(> 65536 candidate)))
::port
label))
make-validator
returns a function that checks if the candidate is
valid and returns either a ValidationSuccess
or a
ValidationFailure
. This can then be combined with other validators
to create a validated constructor for Config
:
(defn create-config
"Creates a validated [[Config]], wrapped in a 'ValidationSuccess'. If
any arguments are invalid, returns a 'ValidationFailure' holding all
'ValidationError's."
[host port mode admin-users]
(v/validation make-config
(v/validate-non-empty-string host :host)
;; NOTE: We could also check for pos-int in
;; `validate-port`. This is intended to show the
;; `validate-all` combinator.
(v/validate-all [v/validate-pos-int validate-port] port :port)
(v/validate-one-of #{:dev :test :prod} mode :mode)
(v/sequence-of v/validate-non-empty-string admin-users :admin-users)))
We will go through the parts of this expression one by one.
(v/validation make-config <validations>)
means that, given all<validations>
areValidationSuccess
es, call the functionmake-config
with the validated candidate values.(v/validate-non-empty-string host :host)
usesvalidate-non-empty-string
from the validation library and checks ifhost
is a string and not empty. If it fails, it will keep:host
as a label to refer back to the argument. All labels are optional, but it is a good idea to state a label if you want to map back from error to cause.(v/validate-all [v/validate-pos-int validate-port] port :port)
uses thev/validate-all
combinator to say that 'the candidate must satisfy all of the following validations,v/validate-pos-int
andvalidate-port
'. It will use both validators on the candidate and combine both errors if there are any into oneValidationFailure
.(v/validate-one-of #{:dev :test :prod} mode :mode)
validates thatmode
is in the specified set of values.(v/sequence-of v/validate-non-empty-string admin-users :admin-users
) also pretty much does what it says on the label: It validates thatadmin-users
is a sequence of values, each of which satisfy thevalidate-non-empty-string
validation.
Lets look at some results:
;; Valid arguments, returns a ValidationSuccess holding the validated candidate.
(create-config "host" 8888 :dev ["user1" "user2"])
;; => #active.clojure.validation/ValidationSuccess{:candidate
;; #validation/Config{:host "host"
;; :port 8888
;; :dev-mode? true
;; :admin-users ["user1" "user"2]}}
Hopefully the most common case: All arguments are valid, therefore the
whole validation succeeds and returns the validated candidate value,
wrapped in a ValidationSuccess
.
;; Every argument is invalid, returns a ValidationFailure with all ValidationErrors.
(create-config "" -1 :staging ["user1" ""])
;; => #active.clojure.validation/ValidationFailure{:errors
;; [#active.clojure.validation/ValidationError{:candidate ""
;; :message :active.clojure.validation/non-empty-string
;; :label :host}
;; #active.clojure.validation/ValidationError{:candidate -1
;; :message :active.clojure.validation/pos-int
;; :label :port}
;; #active.clojure.validation/ValidationError{:candidate -1
;; :message :validation/port
;; :label :port}
;; #active.clojure.validation/ValidationError{:candidate :staging
;; :message [:active.clojure.validation/one-of #{:prod :test :dev}]
;; :label :port}
;; #active.clojure.validation/ValidationError{:candidate ""
;; :message :active.clojure.validation/one-of #{:prod :test :dev}
;; :label [:admin-users 1]}]}
The dire case in which each argument is invalid. Note that the result
is a ValidationFailure
that contains a sequence of
ValidationError
s. Each error tells us which candidate was causing
the error (:candidate
), which validation was violated (:message
)
and gives us a :label
to refer back to the cause of the error. Also
note that the -1
shows up twice. This is to be expected, because it
violates both validations of our validate-all
clause.
;; Only some arguments are invalid
(create-config "" 65537 :dev ["user1" ""])
;; => #active.clojure.validation/ValidationFailure{:errors
;; [#active.clojure.validation/ValidationError{:candidate ""
;; :message :active.clojure.validation/non-empty-string
;; :label :host}
;; #active.clojure.validation/ValidationError{:candidate 65537
;; :message :validation/port
;; :label :port}
;; #active.clojure.validation/ValidationError{:candidate ""
;; :message :active.clojure.validation/one-of #{:prod :test :dev}
;; :label [:admin-users 1]}]}
This case shows the result of only some validations failing. Take
note of the :port
validation again. This time it is only one error.
This is because it satisfies the validate-pos-int
validation but not
our custom validation specifying the legal port range.
The Clojure tests can be executed via
lein test
For auto-testing the ClojureScript code, we use figwheel-main. In a terminal, do
lein fig
which starts a CLJS REPL. Opening
http://localhost:9500/figwheel-extra-main/auto-testing
in a browser window will then run the tests and display the results. After every code change, it will automatically reload and re-run the tests, notifying you via the browser of the result.
Copyright © 2014-2023 Active Group GmbH
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.