diff --git a/search/search_index.json b/search/search_index.json
index a2ae7211b..2171fb14e 100644
--- a/search/search_index.json
+++ b/search/search_index.json
@@ -1 +1 @@
-{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Practicalli Clojure","text":"Practicalli Clojure is a hands-on guide to using Clojure throughout all the software development stages. Live coding videos demonstrate the Clojure REPL workflow in action, showing how to get the most out of the unique approach the language provides.
Discover how to make the most of Clojure CLI and community tools, drawing from commercial experiences and community discussions.
Practical code examples are supported by discussions of the concepts behind Clojure, including functional programming, \"pure\" functions and a stateless approach with persistent data structures, changing state safely, Java interoperability and tooling around Clojure.
John Stevenson, Practical.li
Clojure - an elegant language for a more civilised development experience
"},{"location":"#clojure-repl-driven-development","title":"Clojure REPL Driven Development","text":"The Clojure REPL is interactive environment used to run Clojure code in both development and production. The REPL workflow provides an instant feedback loop so individual pieces of code (expressions) can be evaluatived, quickly growing confidence with Clojure and rapidly evolving effective designs.
"},{"location":"#clojure-language","title":"Clojure Language","text":"Clojure programming language has a strong dynamic type system and a simple syntax that is a joy to work with. Immutable values and a pragmatic approach to pure functional programming makes it easier to create simple and highly maintainable systems. A specification library ensures values are of the correct shape, especially valuable when receiving data from outside of Clojure.
Clojure has an open source license and a large number of open source libraries and tools. Simple host interoperability allows a even more libraries to be leveraged.
Adrian Cockcroft - formally Cloud Architect, Netflix
The most productive programmers I know are writing everything in Clojure ... producing ridiculously sophisticated things in a very short time. And that programmer productivity matters.
Clojure REPL Workflow overview Clojure REPL
"},{"location":"#navigate-the-book","title":"Navigate the book","text":"Use the mouse or built-in key bindings to navigate the pages of the book
- P , , : go to previous page
- N , . : go to next page
Use the search box to quickly find a specific topic
- F , S , / : open search dialog
- Down , Up : select next / previous result
- Esc , Tab : close search dialog
- Enter : follow selected result
"},{"location":"#resources","title":"Resources","text":" Practicalli Clojure CLI Config - additional tools via aliases Clojure Aware Editors Practicalli YouTube channel
"},{"location":"#sponsor-practicalli","title":"Sponsor Practicalli","text":"All sponsorship funds are used to support the continued development of Practicalli series of books and videos, although most work is done at personal cost and time.
Thanks to Cognitect, Nubank and a wide range of other sponsors from the Clojure community for your continued support
"},{"location":"#creative-commons-license","title":"Creative commons license","text":"This work is licensed under a Creative Commons Attribution 4.0 ShareAlike License (including images & stylesheets)."},{"location":"core-async/","title":"Core.async","text":""},{"location":"core-async/#discussion-from-the-clojurians-clojure-uk-slack-channel","title":"Discussion from the clojurians clojure-uk slack channel","text":"recommendation / intro tutorial
https://github.com/clojure/core.async/blob/master/examples/walk-through.clj
EuroClojure talk about clojure otp library that built on top of core.async because they felt it was too low level
At the REPL
- if you don\u2019t create channels with non-zero sized buffers, or don\u2019t perform operations in separate threads or go-blocks, then you can end up blocked quite easily
In production be sure to catch exceptions, or at least be logging them. It can be too easy to loose errors (and the processes that threw them)
if you make a note of the nrepl port when you start a repl, you can always connect a second repl to the same process to recover from accidental blocking
"},{"location":"core-async/#coreasync-and-transducers","title":"core.async and transducers","text":"core.async + transducers
"},{"location":"core-async/#manifold-alternative-to-coreasync","title":"Manifold - alternative to core.async","text":"another approach is to use manifold
it\u2019s use of deferred values makes it harder to block the REPL - but obviously the buffering still has to happen somewhere!
manifold
for general async stuff more than core.async
use core.async as a way of chaining together bits of data processing. manifold would be good for that too
put multiple calls to an external API on a channel and have them \u201cdo their thing\u201d in their own time in the background. This seems to be a good use for core.async\u2026 Is Manifold is as good / better?
If processing streams of data then core.async
is fine (as are manifold Stream
s) If calls are better suited to promises then consider manifold Deferred
If you are wanting streams of promises then manifold is a more complete solution (because the manifold stream
and deferred
work easily together
API calls are generally more promise-like than stream-like (unless your result is an SSE stream or websocket etc)
there are promise-chan
s in core.async too though
Manifold could be used to collect a number of remote resources together in a let
which isn't very stream like
manifold has two distinct core abstractions - the deferred
, which is a promise with callbacks and additional machinery, and the stream
which is a umm stream of values, and supports buffering, backpressure etc
First call to the API I will get a total back as part of the result and as there is no paging functionality \u201cbuilt in\u201d on the API in question I will need to take the total and figure out how many more calls I need to make to get the \u201crest\u201d of the data. I was going to do this by throwing the calls onto a channel using core.async\u2026 I am sensing that their promise-y nature would, you feel, be better suited to Manifold Deferred..? a stream or a channel works quite well for that sort of query in my data access lib we actually use a promise of a stream for that sort of access which is an improvement over just a plain stream because it allows easy mixing with other calls which return promises of a value
I was intending to stack up API operations in a channel as a queue so that a) I don\u2019t block execution and b) so that I don\u2019t have to use an actual queue (SQS / rabbitMQ etc) I am starting to think I may not have understood the implications of what I want to do\u2026
i'm not sure - are you just trying to get a stream of records from a paginated api ?
what are you trying to not block the execution of?
The API is not paginated, I need to figure out how many pages there are and stack up the calls.
can you pass an offset or something ?
What I am looking for is a way to stack work up asynchronously in the background so that my call(s) to the external API don\u2019t lock up the whole app / program for minutes at a time.
yes, but there is no \u201cnext\u201d call - the offset and page-size are arbitrary params every call, there is no way to ask for \u201cpage 2 of my last query to the API\u201d
a channel of results in core.async is perfectly reasonable, as is a manifold stream of deferred responses
so:
- Make call for first page
- Process first page (hopefully async)
- Use total to work out how many more ops I need to make
- Fill up channel with calls
- Consume channel \u201celsewhere\u201d.
is my thesis - does that make sense..?
what is the main thread in this context? the app that \u201cruns\u201d which in turn is a hybrid API / webapp
core.async itself uses a threadpool, so you might not need to funnel them all down one channel unless you wanted to limit the number of concurrent api calls I would like to do as much concurrently as possible, but it\u2019s not a deal-breaker, serial is fine as long as the work can be \u201ckicked off\u201d and left going. order of results being processed is not important, so concurrency (particularly if it makes the whole thing faster) would be great.
I need to make one call to find out how many results match the request, the vendor / curator of the AI in question is not prepared to produce a simplified response to figure out the size of results sets, so I am stuck with that.
I am assuming that I need to \u201cdef\u201d the channel(s) and then have a form that is in an evaluated namespace that is \u201cwaiting\u201d for the channel(s) to have something on them..?
a common idiom is to return channels/streams/promises as the result of a query fn
OK, but how would I consume them without tying up the main thread?
many options
- put the consuming code inside a core.async
go
block - create a new channel with a transducer and pipe your first channel to that
- have your api fn take the channel you want responses put on and pass in a channel with a transducer
similarly in manifold,
- chain a step onto a deferred https://github.com/ztellman/manifold/blob/master/docs/deferred.md#composing-with-deferreds
- map a fn over a stream https://github.com/ztellman/manifold/blob/master/docs/stream.md#stream-operators
need some code in an evaluated namespace that was effectively \u201clistening\u201d for there to be \u201cthings\u201d on the channel, just a form at the end of my namespace containing a go block that was consuming a named channel onto which my API function would place \u201cthings\u201d
most web or UI frameworks will already have an event loop to do that i\u2019d have expected?
order of ops:
- Make first query to API
- Process result, including calculation of how many more ops required
- Load up a channel with the other calls
It\u2019s an app that will periodically (every hour / day not sure yet) make calls to an API, stash the returned data in a database and an ElasticSearch cluster, and then do it all again the next time.
might want to add [4] concatenate results from each of the page queries into a single record stream
but what will consume the eventual record stream ?
This makes the API into a smaller, custom dataset that can be interrogated via Kibana
I am not saying I don\u2019t want to add \u201c[4] concatenate results from each of the page queries into a single record stream\u201d, but I can\u2019t think of why I would do that, and that is probably me being ignorant of the benefits etc. Please could you explain to me why I would add this step - I really am asking, not being a prick, I promise
do you want to expose your downstream consumers to an additional level of structure (pages) which is an implementation feature of the upstream API ?
No
I want to take each of the 100 / 1000 / 10000 results and store them as individual documents in ES and as JSONB fields in Postgres
The API I am \u201charvesting\u201d has a 90 day sliding window, so over time the queries I make will have different results. I don\u2019t want to keep track of the last article I harvested, nor do I want to have to \u201cfind\u201d it in the results to then get all the newer ones. It\u2019s easier to just \u201ceat\u201d the whole response every time and reply on ES refusing to re-import a document with an existing id (into the same index) and on postgres\u2019s ability to enforce a \u201cunique\u201d index on the id field.
but I can\u2019t \u201cget\u201d all of the results in one query, the API limits \u201cpages\u201d to 1000 results, so I need to be able to stack up calls and execute them in an async, non-blocking manner.
yep, so you can concatenate the pages into a single record-stream, and process each of the records individually
OK, I like the sound of this in principle, and I think I am sort of doing that already with the synchronous, manual approach, as I get 100 articles back and then I do a doseq over the vector of maps to do INSERT queries into postgres and PUT calls to ES
What do you mean by a \u201crecord-stream\u201d?
by \"record stream\" i mean a conceptual sequence of individual records... could be on a core.async chan
or a manifold stream
the benefit is just simplicity - a sequence of individual records is a simpler thing than a sequence of pages of individual records... but there are tradeoffs - sometimes you want to deal with pages of records
Oh I see! Right, yeah, I was just going to consume the channel of returned promises with the doseq I already have, so concatenating them together into one HUGE vector first seemed like a redundant step. i.e. one of the queries I am going to do returns (currently) a little over 13,000 records - I was expecting to grab the results of 14 promises off the channel and \u201cdoseq\u201d each one until the channel was empty
I suppose I could consume them off the channel into one big vector, or indeed another channel and then have another consumer running what currently runs inside the doseq on each map / JSON blob that comes out of the channel\u2026 Is that what you mean?
so:
channel of promises consumer turns vector of maps into another channel of individual maps consumer2 puts maps into DB and ES off second channel
i meant another channel, yes
possibly even:
consumer2 puts maps into DB and onto another channel consumer3 puts maps on third channel into ES
(as an aside @maleghast , doing any long-running or blocking processing in a vanilla core.async go
block isn't a good idea - there is a fixed-size core.async threadpool which you can exhaust, causing blocking - so you can use https://clojure.github.io/core.async/index.html#clojure.core.async/thread )
So if I use thread inside a go block, or instead of a go block..?
here is an example
(async/go (let [v (async/<! (async/thread (do-blocking-stuff)))] (do-non-blocking-stuff v))
something like that
"},{"location":"core-async/#long-running-processes","title":"Long running processes","text":"also, beware long-running processes in core.async that expand items with eg. mapcat
operations. You can break back pressure that way. (ie. pages on a channel being expanded into multiple events)
ooo i haven't come across that problem what happens ?
requires a very specific use case to be a problem, but it\u2019s caught a few people out: https://stackoverflow.com/questions/37953401/where-is-the-memory-leak-when-mapcat-breaks-backpressure-in-core-async
Where is the memory leak when mapcat breaks backpressure in core.async? I wrote some core.async code in Clojure and when I ran it it consumed all available memory and failed with an error. It appears that using mapcat in a core.async pipeline breaks back pressure. (Whi...
you\u2019re not likely to hit it unless you are using a lot of transforms on your channels, and then its easily worked around, but it can work fine in test, and then blow up in prod with more data/longer running processing
"},{"location":"explaining-macros/","title":"Explaining Macros","text":"The macro system allows you to extend the design of the Clojure language, without waiting for the language designers.
Expose as much of your API as possible as functions so that they can be stored in hash-maps, mapped over sequences of widgets, negated with complement, juxtaposed with juxt, and so on.
Only define your own macros when functions are insufficient or there is a very common abstraction that creates a simpler system.
"},{"location":"explaining-macros/#hintclojure-macros-are-quite-unique","title":"Hint::Clojure macros are quite unique","text":"Many languages have macros, although most are more akin to a template systems.
Clojure macros are a language within the Clojure language that generate Clojure code when the Clojure Reader parses a macro. In fact the Clojure Reader will pass the macro to the macro reader which does the expansion.
"},{"location":"explaining-macros/#hintevery-macro-adds-maintenance-cost","title":"Hint::Every Macro adds maintenance cost","text":"Adding custom macros adds a higher maintenance cost to a codebase and increased the amount of on-boarding of developers onto a project.
Custom macros are additional abstractions to learn when working with a project that are not common across projects in the same way as clojure.core or common libraries.
"},{"location":"explaining-macros/#functional-composition-and-macros","title":"Functional composition and Macros","text":"Macros do not compose functionally
Macros in Clojure aren't values. They can't be passed as arguments to other functions or macros, can't be returned as the result of computations, and can't be stored in data structures.
If macros were values and used as arguments to functions then the compile cycle of Clojure would need to change, otherwise the results of macro expansion wouldn't be known until runtime and could even vary between function calls.
"},{"location":"explaining-macros/#wrapping-macros-in-functions-for-composition","title":"Wrapping Macros in functions for composition","text":"It is possible to wrap a macro in a function and then that macro can be used as part of a functionally composed expression.
(reduce and [true true false true])\n;;=> RuntimeException\n\n(reduce #(and %1 %2) [true true false true])\n;;=> false\n
If you are doing this, then its more probably that a simple function would be a better approach.
"},{"location":"io/","title":"IO in Clojure","text":"From http://blog.isaachodes.io/p/clojure-io-p1/
The Ins and Outs of Clojure: Part I November 13, 2010
(Written about Clojure 1.2.)
It is a truth universally acknowledged, that a programmer using Clojure will want to perform IO. Let me help you out (put).
I\u2019ll go over some of the basics of IO, focusing on what you can use Clojure to do directly. I\u2019ll move on after the basic introduction, to some of the more interesting and generally useful classes that Java offers, giving a little context for each. In
Reading files in is generally one of the first things I want to do when playing with a new language, so I\u2019ll start there. Before I get started though, I should mentioned that in Clojure, strings are always encoded using UTF-16. Generally this saves time and worry, but it\u2019s something to keep in mind should you run into problems on the encoding front. slurp
Clojure comes with a handy little function called slurp that takes in a string representing a filename (or, really, pretty much anything; a File, a stream, a byte array, URL, etc) and returns a string containing the contents of your file. It\u2019s pretty handy if you just need to get some information from a file that\u2019s relatively small, and you\u2019ll be parsing it yourself.
(slurp \"/home/user/file.txt\") => \"A little bit\\nof information here.\"
A nice thing about slurp is that you can easily build up file paths with str. For example, say you want to output to a file based on information you find at runtime:
(slurp (str \"/home/\" username \"/projects/\" filename))
But slurp is pretty basic, and once your files get large enough, totally impractical. Nonetheless, it\u2019s a handy function to know about.
As a useful and comical aside, the function spit is the counterpart to slurp, except that instead of reading input, spit does output. More on this in a future article, though. line-seq
One of my favorite IO functions has got to be line-seq; line-seq takes a reader object (which must implement BufferedReader) and returns a lazy sequence of the lines of the text the reader supplies. This is handy when you\u2019re dealing with files (if this offends you, taking a Unix approach here for now and say that everything is a file) that are too big to merely slurp, but that are \\newline delimited (or CR/LF delimited, if you\u2019re of the Windows persuasion).
(use '[clojure.java.io '(reader)]) (take 2 (line-seq (reader \"bobby.txt\"))) => (\"Bobby was a good boy,\" \"and didn't complain too much\")
Notice how we take 2 from the sequence we get from using line-seq. We can take as much or as little as we need; we won\u2019t be reading much (Clojure will read a bit more than you tell it to in order to get more IO performance, but let\u2019s not worry about that) more than we specify. We can do anything we want with the resulting seq; that\u2019s the beauty of line-seq and the ubiquitous sequence abstraction.
Back in the day, Clojurists had to sink a little lower than the clojure.java.io namespace to use line-seq; two Java classes were needed. One of these Java classes is the most wondrous and amazing thing just below the surface of the more elegant and beautiful Clojure code; BufferedReader. Here\u2019s how we used to do it;
(import '(java.io FileReader BufferedReader)) (take 2 (line-seq (BufferedReader. (FileReader. \"bobby.txt\"))) => (\"Bobby was a good boy,\" \"and didn't complain too much\")
This might give you a better sense of what\u2019s going on when you use reader, though in reality reader is far more complicated than just that: you can trust it to handle a variety of \u201creadable things\u201d and return to you a BufferedReader if possible.
FileReader will return a Reader on a file, and BufferedReader takes and buffers a Reader, as you might have extrapolated from the name. Readers are basically just objects upon which a few methods (like read, skip and close) may be enacted and expected to return reasonable results. line-seq essentially reads up until a line-delimiter and returns the read chunk as an element in the sequence it is generating.
While on the subject of files, I should probably mentioned the file function, from clojure.java.io. file takes in an arbitrary number of string arguments, and pieces them together into a file hierarchy, returning a File instance. This can come in handy. Rivers? inputStreams? Brooks?
Streams are an especially useful class of readers. Oftentimes you\u2019re reading in text; that\u2019s what Readers do. But often you need to read in a stream of bytes; that\u2019s where you need to use clojure.java.io\u2019s input-stream.
(use '[clojure.java.io '(reader)]) (def g (input-stream \"t.txt\")) (.read g) => 105 (.read g) => 115 (char (.read g)) => \\space
As you can see, instead of getting characters from this file (like we get when we use a reader), we\u2019re getting integer byte values. This can be useful when reading, for example, a media file.
In general, strings are always UTF-16, which are 16-bit pieces of data, whereas byte-streams are 8-bit pieces of data. It bears repeating that the stream operators should be used when you\u2019re not dealing with strings: they are not trivially interchangeable, as they might be in other languages where strings are syntactic sugar for byte arrays. RandomAccessFile
Finally, let me introduce to you a spectacularly useful Java class. RandomAccessFile is a class which allows you to quickly jump around in a large file, and read bytes from it.
(import '(java.io RandomAccessFile)) (def f (RandomAccessFile. \"stuff.txt\" \"r\"))
Note the second argument of the constructor, \u201cr\u201d; this indicates that we\u2019re opening the file just for reading. Now that we have f, we can use it to navigate and read the file:
(.read f) => 105 (.length f) => 2015 ;; this is the number of bytes this file is in length (.skipBytes f 20) (.getFilePointer h) => 21 ;; the position we're at in the file (.read f) => 89
As you can see, you can jump around (quickly!) through a file, and read from the parts you want, and skip the parts you do not want. The key methods/functions here (among many others that can also be useful; be sure to check the documentation) are read, length, skipBytes, seek and getFilePointer. Closing
Every file that is opened should be closed, and what we\u2019ve been doing is a little unsafe. In order to close an open reader/file, we should use the close method on it; in the above example, when you\u2019re done with f, simply execute (.close f) to tell the file system that you\u2019re done with the file. Alternatively, and more idiomatically, you can open your files with the handy with-open binder:
(with-open [f (RandomAccessFile. \"stuff.txt\" \"r\")] (.read f))
When you\u2019re done with f, Clojure will close it, and you won\u2019t have to worry one iota about it. Digging Deeper
Should slurp and line-seq not be enough for your reading needs (and chances are that, should you code enough in Clojure, they won\u2019t always been), you might want to explore clojure.java.io some more, as well as some of the Java classes (namely, those stemming from Reader and BufferedReader, as well as InputStream and BufferedInputStream) mentioned above. See my previous article on using Java if you\u2019re unfamiliar with using Java.
Next up is an introduction to the \u201couts\u201d of Clojure and Java. Stay tuned!
I owe a big thank you to Phil Hagelberg for reading over this essay and offering advice. If you don\u2019t already, you should be using his Leiningen for both dependency management and a stress-free development environment.
The Ins and Outs of Clojure: Part I November 13, 2010
(Written about Clojure 1.2.)
It is a truth universally acknowledged, that a programmer using Clojure will want to perform IO. Let me help you out (put).
I\u2019ll go over some of the basics of IO, focusing on what you can use Clojure to do directly. I\u2019ll move on after the basic introduction, to some of the more interesting and generally useful classes that Java offers, giving a little context for each. In
Reading files in is generally one of the first things I want to do when playing with a new language, so I\u2019ll start there. Before I get started though, I should mentioned that in Clojure, strings are always encoded using UTF-16. Generally this saves time and worry, but it\u2019s something to keep in mind should you run into problems on the encoding front. slurp
Clojure comes with a handy little function called slurp that takes in a string representing a filename (or, really, pretty much anything; a File, a stream, a byte array, URL, etc) and returns a string containing the contents of your file. It\u2019s pretty handy if you just need to get some information from a file that\u2019s relatively small, and you\u2019ll be parsing it yourself.
(slurp \"/home/account/projects/config.txt\") => \"A little bit\\nof information here.\"
A nice thing about slurp is that you can easily build up file paths with str. For example, say you want to output to a file based on information you find at runtime:
(slurp (str \"/home/\" username \"/projects/\" filename))
But slurp is pretty basic, and once your files get large enough, totally impractical. Nonetheless, it\u2019s a handy function to know about.
As a useful and comical aside, the function spit is the counterpart to slurp, except that instead of reading input, spit does output. More on this in a future article, though. line-seq
One of my favorite IO functions has got to be line-seq; line-seq takes a reader object (which must implement BufferedReader) and returns a lazy sequence of the lines of the text the reader supplies. This is handy when you\u2019re dealing with files (if this offends you, taking a Unix approach here for now and say that everything is a file) that are too big to merely slurp, but that are \\newline delimited (or CR/LF delimited, if you\u2019re of the Windows persuasion).
(use '[clojure.java.io '(reader)]) (take 2 (line-seq (reader \"bobby.txt\"))) => (\"Bobby was a good boy,\" \"and didn't complain too much\")
Notice how we take 2 from the sequence we get from using line-seq. We can take as much or as little as we need; we won\u2019t be reading much (Clojure will read a bit more than you tell it to in order to get more IO performance, but let\u2019s not worry about that) more than we specify. We can do anything we want with the resulting seq; that\u2019s the beauty of line-seq and the ubiquitous sequence abstraction.
Back in the day, Clojurists had to sink a little lower than the clojure.java.io namespace to use line-seq; two Java classes were needed. One of these Java classes is the most wondrous and amazing thing just below the surface of the more elegant and beautiful Clojure code; BufferedReader. Here\u2019s how we used to do it;
(import '(java.io FileReader BufferedReader)) (take 2 (line-seq (BufferedReader. (FileReader. \"bobby.txt\"))) => (\"Bobby was a good boy,\" \"and didn't complain too much\")
This might give you a better sense of what\u2019s going on when you use reader, though in reality reader is far more complicated than just that: you can trust it to handle a variety of \u201creadable things\u201d and return to you a BufferedReader if possible.
FileReader will return a Reader on a file, and BufferedReader takes and buffers a Reader, as you might have extrapolated from the name. Readers are basically just objects upon which a few methods (like read, skip and close) may be enacted and expected to return reasonable results. line-seq essentially reads up until a line-delimiter and returns the read chunk as an element in the sequence it is generating.
While on the subject of files, I should probably mentioned the file function, from clojure.java.io. file takes in an arbitrary number of string arguments, and pieces them together into a file hierarchy, returning a File instance. This can come in handy. Rivers? inputStreams? Brooks?
Streams are an especially useful class of readers. Oftentimes you\u2019re reading in text; that\u2019s what Readers do. But often you need to read in a stream of bytes; that\u2019s where you need to use clojure.java.io\u2019s input-stream.
(use '[clojure.java.io '(reader)]) (def g (input-stream \"t.txt\")) (.read g) => 105 (.read g) => 115 (char (.read g)) => \\space
As you can see, instead of getting characters from this file (like we get when we use a reader), we\u2019re getting integer byte values. This can be useful when reading, for example, a media file.
In general, strings are always UTF-16, which are 16-bit pieces of data, whereas byte-streams are 8-bit pieces of data. It bears repeating that the stream operators should be used when you\u2019re not dealing with strings: they are not trivially interchangeable, as they might be in other languages where strings are syntactic sugar for byte arrays. RandomAccessFile
Finally, let me introduce to you a spectacularly useful Java class. RandomAccessFile is a class which allows you to quickly jump around in a large file, and read bytes from it.
(import '(java.io RandomAccessFile)) (def f (RandomAccessFile. \"stuff.txt\" \"r\"))
Note the second argument of the constructor, \u201cr\u201d; this indicates that we\u2019re opening the file just for reading. Now that we have f, we can use it to navigate and read the file:
(.read f) => 105 (.length f) => 2015 ;; this is the number of bytes this file is in length (.skipBytes f 20) (.getFilePointer h) => 21 ;; the position we're at in the file (.read f) => 89
As you can see, you can jump around (quickly!) through a file, and read from the parts you want, and skip the parts you do not want. The key methods/functions here (among many others that can also be useful; be sure to check the documentation) are read, length, skipBytes, seek and getFilePointer. Closing
Every file that is opened should be closed, and what we\u2019ve been doing is a little unsafe. In order to close an open reader/file, we should use the close method on it; in the above example, when you\u2019re done with f, simply execute (.close f) to tell the file system that you\u2019re done with the file. Alternatively, and more idiomatically, you can open your files with the handy with-open binder:
(with-open [f (RandomAccessFile. \"stuff.txt\" \"r\")] (.read f))
When you\u2019re done with f, Clojure will close it, and you won\u2019t have to worry one iota about it. Digging Deeper
Should slurp and line-seq not be enough for your reading needs (and chances are that, should you code enough in Clojure, they won\u2019t always been), you might want to explore clojure.java.io some more, as well as some of the Java classes (namely, those stemming from Reader and BufferedReader, as well as InputStream and BufferedInputStream) mentioned above. See my previous article on using Java if you\u2019re unfamiliar with using Java.
Next up is an introduction to the \u201couts\u201d of Clojure and Java. Stay tuned!
I owe a big thank you to Phil Hagelberg for reading over this essay and offering advice. If you don\u2019t already, you should be using his Leiningen for both dependency management and a stress-free development environment.
"},{"location":"lazy-evaluation/","title":"Lazy evaluation","text":"repeat
"},{"location":"testing-in-clojure/","title":"Testing in clojure","text":"My recommended Clojure testing setup
kaocha test runner in watch mode
Occasionally, either on Stack Overflow or in the Clojurians Slack group, someone will ask what tools they should use to test Clojure code. Below is what I would currently recommend. I\u2019ve come to this recommendation through observing teams using a variety of testing tools and through my own use them.
Use clojure.test with humane-test-output and lein-test-refresh.
Use clojure.test
clojure.test is ubiquitous and not a big departure from other languages' testing libraries. It has its warts but your team will be able to understand it quickly and will be able to write maintainable tests.
Use humane-test-output
You should use clojure.test with humane-test-output. Together they provide a testing library that has minimal additional syntax and good test failure reporting.
Use lein-test-refresh
If you\u2019re not using a tool that reloads and reruns your tests on file changes then you are wasting your time. The delay between changing code and seeing test results is drastically reduced by using a tool like lein-test-refresh. Nearly everyone I know who tries adding lein-test-refresh to their testing toolbox continues to use it. Many of these converts were not newcomers to Clojure either, they had years of experience and had already developed workflows that worked for them.
Use lein-test-refresh\u2019s advanced features
lein-test-refresh makes development better even if you don\u2019t change any of its settings. It gets even better if you use some of its advanced features.
Below is a stripped down version of my ~/.lein/profiles.clj. The :test-refresh key points towards my recommended lein-test-refresh settings.
{:user {:dependencies [[pjstadig/humane-test-output \"0.8.0\"]] :injections [(require 'pjstadig.humane-test-output) (pjstadig.humane-test-output/activate!)] :plugins [[[com.jakemccrary/lein-test-refresh \"0.16.0\"]]] :test-refresh {:notify-command [\"terminal-notifier\" \"-title\" \"Tests\" \"-message\"] :quiet true :changes-only true}}}
These settings turn on notifications when my tests finish running (:notify-command setting), make clojure.test\u2019s output less verbose (:quiet true), and only run tests in namespaces affected by the previous code change (:changes-only true). These three settings give me the quickest feedback possible and free me from having the terminal running lein test-refresh visible.
Quick feedback lets you make changes faster. If you\u2019re going to write tests, and you should write tests, having them run quickly is powerful. After years of writing Clojure, this is my current go-to for testing Clojure code and getting extremely fast feedback.
I use test-refresh every day. Frankly I'm not sure how one can program effectively without it. I especially like being able to control the notifications
"},{"location":"alternative-tools/","title":"Alternative Tools","text":""},{"location":"alternative-tools/clojure-cli/basic-repl/","title":"Basic Terminal REPL UI","text":"The clojure
command will start a REPL by default or if given the --repl
or -r
argument. The basic repl does not provide history of commands.
clj
is a script that wraps the clojure
command and requires rlwrap
, an external readline command, to navigate REPL history via the Up and Down keys.
Use clj
when you want to run a repl (or preferably use rebel readline instead) and clojure
for everything else.
Rebel Rich Terminal UI
rebel readline is a terminal REPL UI that provides interactive help, function autocomplete, signature prompts and many other features to provide a very rich REPL experience.
Practicalli Clojure CLI Config includes the -M:repl/rebel
alias to run rebel readline REPL.
clj
command in a terminal window starts a Clojure REPL and shows the version of Clojure used. The command does not need to be in a directory containing a Clojure project.
clj\n
Type in a Clojure expression at the => user
REPL prompt and press Enter to see the result
Ctrl+d to exit the REPL
"},{"location":"alternative-tools/clojure-cli/compare-with-leiningen/","title":"Clojure Automation Aproaches","text":"Leiningen, Clojure CLI tools and Boot are different approaches to Clojure project configuration. Regardless of the tool used, the Clojure code in the project remains the same.
"},{"location":"alternative-tools/clojure-cli/compare-with-leiningen/#clojure-cli-tools-overview","title":"Clojure CLI tools overview","text":"Clojure CLI tools takes a very simple approach, focusing on running a REPL process, Clojure programs via clojure main and specific functions via clojure exec. With the Clojure exec approach any function can be called as the entry point to running an application or tool written in Clojure.
CLI tools can use both Maven and Git repositories for dependency management. Code from a Git repository dependency can be used without packaging it into a library (Java jar file), simplifying the use of libraries under active development.
Clojure CLI tools provides a comprehensive set of features via community tools. These community tools are provided via aliases in the user level configuration for all projects (e.g. practicalli/clojure-deps-edn) or an alias for a specific project deps.edn
configuration.
"},{"location":"alternative-tools/clojure-cli/compare-with-leiningen/#leiningen-overview","title":"Leiningen overview","text":"Leiningen is a feature rich tool which is simple to get started with a plugin extension to add more functionality. Leiningen does use more resources as it starts a Java Virtual machine to run itself and another to run the application.
"},{"location":"alternative-tools/clojure-cli/compare-with-leiningen/#boot-overview","title":"Boot overview","text":"Boot runs tasks written in Clojure on the command line, providing a flexible way to work with projects. However, this approach does require expertise with Clojure and Clojure scripts to work with projects.
Clojure code is the same which ever approach used
The Clojure code for the project will be the same regardless of which tool is used to configure and manage the Clojure project.
"},{"location":"alternative-tools/clojure-cli/compare-with-leiningen/#configuration","title":"Configuration","text":"Tool Project Config User Config Extension Clojure CLI tools deps.edn hash-map merged with user config $XDG_HOME_CONFIG/clojure/deps.edn
or $HOME/.clojure/deps.edn
aliases in project and user deps.edn Leiningen project.clj and defproject
macro ~/.lein/profiles.clj Leiningen specific plugin Boot build.boot with deftask
, task-options!
, set-env!
Write the tasks required in Clojure Clojure CLILeiningenBoot Clojure CLI tools are configured with an EDN data structure, i.e. a hash-map of key-value pairs. As this is a Clojure data structure its much easier to parse and should be very familiar to Clojure developers.
Leiningen projects are configured with a project.clj
file which contains a defproject
macro with a great many options. The Leiningen tutorial explains the options in detail. A sample project.clj contains examples of using each of this options.
For most projects all the configuration resides in the project.clj file. Exceptions to this include figwheel-main, which also adds it own EDN configuration and EDN build configuration files.
Leiningen also has a user level configuration
build.boot
is a file containing Clojure code that defines the tasks for using your project.
"},{"location":"alternative-tools/clojure-cli/compare-with-leiningen/#extending-the-tools","title":"Extending the tools","text":"Clojure CLILeiningenBoot Clojure CLI tools has been designed for a very specific role, to provide a lightweight wrapper over running Clojure programs and via tools.deps managing dependencies from Maven and Git repositories.
The projects that extend Clojure CLI tools are self-contained libraries and tools, so are not tied to any one particular tool. Any general tools written for Clojure should work with Clojure CLI tools by calling their main function (clojure main) or a specifically named function (clojure exec)
Leiningen plugin extension was the main way to extend the functionality of Leiningen (or getting pull requests accepted to the Leiningen projects).
Although there are several plugins that were widely adopted, some plugins eventually caused more confusion than benefit or were simply trivial and in the main plugins seem to have become less important to the Clojure community.
One of the limitations of Leiningnen plugin mechanism was not being able to exclude any configuration in a users .lein/profiles.clj
file, so there was greater potential for conflict.
The recommended way to extend Leiningen is to not write plugins, but to include aliases that define a qualified function to run when that alias is used with the Leiningen command.
Write clojure to define scripts to run with boot projects.
Babashka for Clojure scripting
Babashka is an approach to writing bash-style scripts using the Clojure language. Babashka bundles additional libraries to support common tasks
"},{"location":"alternative-tools/clojure-cli/evaluate-an-expression/","title":"Evaluating an expression with Clojure CLI tools","text":"An expression is a piece of Clojure code that can be evaluated and return a result
This expression calls the +
function with the arguments 1 2 3 4 5
. As this code works, we get a result.
(+ 1 2 3 4 5)\n
Using the -e
option an expression can be passed to the Clojure CLI tools and a value returned
clojure -e (+ 1 2 3 4 5)\n
"},{"location":"alternative-tools/clojure-cli/evaluate-an-expression/#expressions-returning-nil","title":"Expressions returning nil
","text":"If the expressing used returns a value of nil
, a legal value in Clojure, then no result is printed out.
"},{"location":"alternative-tools/clojure-cli/evaluate-an-expression/#when-to-use-this","title":"When to use this?","text":"clojure -e
is a quick way to see what an expression does without having to set anything up (although starting a REPL is very little extra effort).
Using the -e
option is useful for running simple scripts written in Clojure, especially on servers and remote environments.
The lack of return value for nil is very useful when using Clojure CLI tools to evaluate Clojure code within another script.
"},{"location":"alternative-tools/clojure-cli/set-namespace-on-repl-startup/","title":"Set namespace on REPL startup","text":"The REPL process does not evaluate project code on start-up. If it did and that code had a error, it could prevent the REPL from starting.
The common approach is to require the main namespace for the project, making the functions in that namespace available. This will also make available functions from those namespaces.
Switching to a specific namespace in the REPL allows calling functions by name, without the fully qualified name.
"},{"location":"alternative-tools/clojure-cli/set-namespace-on-repl-startup/#set-namespace-via-the-command-line","title":"Set namespace via the command line","text":"To require and switch to a namespace on startup, use the clojure
or clj
commands with the --eval option to run the specific commands. The --repl option will ensure the repl starts.
clj --eval \"(require 'practicalli.random-clojure-core-function)\" --eval \"(in-ns 'practicalli.random-clojure-core-function)\" --repl\n
-r
or (clojure.main/repl)
are the same as using the --repl
option
clj -e \"(ns foo.bar) (alter-var-root #'*ns* (constantly 'foo.bar))\" -r\nclj -e \"(ns foo.bar) (alter-var-root #'*ns* (constantly 'foo.bar)) (clojure.main/repl)\"\n
"},{"location":"alternative-tools/clojure-cli/set-namespace-on-repl-startup/#set-namespace-with-rebel-readline","title":"Set namespace with Rebel Readline","text":"Set the namespace using Rebel Readline alias from Practicalli Clojure CLI Config
clj -M:lib/rebel -e \"(ns foo.bar) (alter-var-root #'*ns* (constantly (find-ns 'foo.bar)))\" -m rebel-readline.main\n\n#object[clojure.lang.Namespace 0x46cf05f7 \"foo.bar\"]\n[Rebel readline] Type :repl/help for online help info\nfoo.bar=>\n
The :lib/rebel
alias adds the rebel library as a dependency without calling clojure main on the rebel namespace. alter-var-root
sets the namespace. The -m
flag defines the namespace which Clojure main will run the -main
function from, starting the rebel UI on the command line.
The --eval
approach will be blocked if used with aliases that set the main namespace, such as :repl/rebel
.
"},{"location":"alternative-tools/clojure-cli/set-namespace-on-repl-startup/#set-namespace-using-an-editor","title":"Set namespace using an editor","text":"It is not necessary to set the namespace when evaluating code in a Clojure aware editor. Expressions are evaluated within the scope of the namespace in which they are defined.
Using an editor to evaluate Clojure is much simpler and quicker than using a command line REPL, especially when working with Clojure projects with more than one namespace.
"},{"location":"alternative-tools/data-inspector/reveal/","title":"Reveal REPL with visual data browser","text":"Reveal provides a REPL with a connected visual data explorer. Reveal can also be used as a tap>
target, visualizing data added to a tap>
(an elegant approach compared to print statements).
Reveal describes itself as a read evaluate visualize loop tool proving extra tools to explore data in Clojure visually. The extensive documentation shows the many ways to use Reveal.
Use Reveal with a terminal REPL or a Clojure editor that uses nrepl such as Emacs Cider and Spacemacs. Reveal can be added as a tap source to any running REPL, eg. using Reveal with Rebel Readline.
REPL Terminal UInrepl Editors Practicalli Clojure CLI Config contains the :inspect/reveal
alias to run Reveal REPL in a terminal with the Reveal UI along-side.
Open a terminal and run the command:
clojure -M:inspect/reveal\n
Use the clj
command if the rlwrap
binary is available
Write Clojure code as normal in the REPL and result are also sent to the Reveal data browser window.
The extensive documentation shows the many ways to use Reveal.
Start reveal with an nREPL server to allow connection from a Clojure aware editor
Practicalli Clojure CLI ConfigAlias Definition :inspect/reveal-nrepl
alias provided by Practicalli Clojure CLI Config runs a Reveal repl with data browser and nrepl server, allowing connections from Clojure aware editors such as Emacs CIDER and VSCode Calva.
:inspect/reveal-light-nrepl
does the same and uses a light them with Ubuntu Mono 32 point font (good for demos, HiDPI screens)
Add the following aliases to your user level ~/.clojure/deps.edn
configuration to make reveal available to all projects.
Plain nrepl server with Reveal.
:inspect/reveal-nrepl\n {:extra-deps {vlaaad/reveal {:mvn/version \"1.1.159\"}\n nrepl/nrepl {:mvn/version \"0.8.3\"}}\n :main-opts [\"-m\" \"nrepl.cmdline\"\n \"--middleware\" \"[vlaaad.reveal.nrepl/middleware]\"]}\n
CIDER specific nrepl connection with the Cider middleware :inspect/reveal-nrepl-cider\n {:extra-deps {vlaaad/reveal {:mvn/version \"1.1.159\"}\n nrepl/nrepl {:mvn/version \"0.8.3\"}\n cider/cider-nrepl {:mvn/version \"0.25.4\"}\n refactor-nrepl/refactor-nrepl {:mvn/version \"2.5.0\"}}\n :main-opts [\"-m\" \"nrepl.cmdline\"\n \"--middleware\" \"[vlaaad.reveal.nrepl/middleware,refactor-nrepl.middleware/wrap-refactor,cider.nrepl/cider-middleware]\"]}\n
Start a Reveal REPL with nrepl server
clojure -M:inspect/reveal-nrepl\n
Connect to the Reveal repl from a Clojure aware editor, e.g cider-connect
"},{"location":"alternative-tools/data-inspector/reveal/#using-reveal-with-cider-jack-in","title":"Using Reveal with Cider Jack-in","text":"C-u cider-jack-in-clj
in CIDER to start a reveal REPL (SPC u , '
in Spacemacs)
Edit the jack-in command by deleting the all the configuration after the clojure
command and add the alias
clojure -M:inspect/reveal-nrepl-cider\n
:inspect/reveal-nrepl-cider
is a light version of the above.
"},{"location":"alternative-tools/data-inspector/reveal/#cider-jack-in-with-reveal-using-a-dir-localsel","title":"Cider jack-in with reveal using a .dir-locals.el","text":"Add a .dir-locals.el
file to the root of the Clojure project. The .dir-locals.el
configuration adds the :inspect/reveal-nrepl-cider
via cider-clojure-cli-aliases
and all other automatically injected configuration is disabled to prevent those dependencies over-riding the alias.
((clojure-mode . ((cider-preferred-build-tool . clojure-cli)\n (cider-clojure-cli-aliases . \":inspect/reveal-nrepl-cider\")\n (cider-jack-in-dependencies . nil)\n (cider-jack-in-nrepl-middlewares . nil)\n (cider-jack-in-lein-plugins . nil)\n (cider-clojure-cli-parameters . \"\"))))\n
Ensure the .dir-locals.el
file is loaded by using revert-buffer
on any open Clojure file from the project.
cider-jack-in-clj
should now start the Reveal REPL and send evaluations from Cider to the Reveal visualization UI.
"},{"location":"alternative-tools/data-inspector/reveal/#reveal-with-rebel-and-tap","title":"Reveal with Rebel and tap>
","text":"Reveal can be used as a tap>
target with the Rebel REPL, launching the Reveal data browser when added as a tap> target.
Start Rebel REPL with Reveal library as a dependency
clojure -M:repl-reveal:repl/rebel\n
Add reveal as a tap target that will receive all data from tap>
function calls
(add-tap ((requiring-resolve 'vlaaad.reveal/ui)))\n
A reveal window opens and receives all tap>
values while the REPL is running.
(tap> [1 2 3 4 5])\n(tap> {:name \"Jenny\" :skill \"Clojure\"})\n(tap> (zipmap [:a :b :c] [1 2 3 4]))\n
"},{"location":"alternative-tools/data-inspector/reveal/#atom-and-tapped-value","title":"Atom and Tapped value","text":"Create an atom for debugging purposes
(def debug-state (atom nil))\n(add-tap #(reset! debug-state %))\n
Use tap>
to capture intermediate values in the middle of code that requires debugging, inspecting or watching the atom value (deref the atom and watch:latest or watch:all). This approach complements an editor debugging process
"},{"location":"alternative-tools/data-inspector/reveal/#tracking-state-with-an-atom","title":"Tracking state with an atom","text":"Define the state as an atom, the state being a simple value in this case
(def state (atom 24))\n
Select the reference created for the state in the Revel browser.
SPACE
or ENTER
to open the menu and select Deref
. A tab opens with the value of the atom.
SPACE
or ENTER
with the value selected and select Watch:all
.
In the REPL, update the value of the state atom.
(swap! state * 12)\n
The new value of the state atom is shown in the Reveal data browser. Each Clojure expressions evaluated that affects the state atom will be displayed in the Reveal browser.
"},{"location":"alternative-tools/data-inspector/reveal/#running-different-types-of-repl","title":"Running different types of repl","text":"Using Clojure exec -X
flag, the default repl function can be over-ridden on the command line, supplying the io-prepl
or remote-prepl
functions.
clojure -X:inspect/reveal io-prepl :title '\"I am a prepl repl\"
clojure -X:inspect/reveal remote-prepl :title '\"I am a remote prepl repl\"'
"},{"location":"alternative-tools/data-inspector/reveal/#configure-theme-font","title":"Configure theme & font","text":"Add a custom theme and font via the -J
command line option or create an alias using :inspect/reveal-light
as an example.
clojure -M:inspect/reveal -J-Dvlaaad.reveal.prefs='{:theme :light :font-family \"Ubuntu Mono\" :font-size 32}'\n
"},{"location":"alternative-tools/data-inspector/reveal/#rebel-readline-reveal-add-reveal-as-tap-source","title":"Rebel Readline & Reveal: Add Reveal as tap> source","text":"Evaluate (add-tap ((requiring-resolve 'vlaaad.reveal/ui)))
when using Rebel Readline to add Reveal as a tap source, showing (tap> ,,,)
expressions in the reveal window, eg. (tap> (map inc [1 2 3 4 5]))
.
"},{"location":"alternative-tools/leiningen/","title":"Leiningen build automation tool","text":"Leiningen will help you create, build and deploy your Clojure projects.
Practicalli recommends using Clojure CLI
For new project, Clojure CLI is recommended as it requires fewer resources and enables a more customisable approach to configuring project and running tools to support Clojure development.
"},{"location":"alternative-tools/leiningen/#install-leiningen","title":"Install Leiningen","text":"Install the Leiningen tool using the specific instructions for your Operating System
{% tabs first=\"Linux\", second=\"Homebrew\", third=\"GitBash\", forth=\"Chocolatey\", fifth=\"Windows Manual\" %}
LinuxHomebrewGitBashChocolateyWindows Download the lein script to your local bin
directory. Then make the lein
script executable and run lein
to download the full version.
mkdir ~/bin\ncurl https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein > ~/bin/lein\nchmod u+x ~/bin/lein\nlein\n
If the command lein
is not found, run source ~/.bashrc
to ensure your bin
directory is on the path. If you have Homebrew installed, run the following command in a terminal window.
brew install leiningen\n
GitBash allows you to use the Linux lein
script, which may have less issues when installing.
Create a directory called C:\\Users\\your-user-name\\AppData\\Local\\Programs\\Leiningen
Download the lein
file and save it to the above directory
Open Environment variables for your account
and add the directory to your path
Open a command window and run the command: lein
The full version of Leiningen will be downloaded and Leiningen is ready to use.
If you have Chocolatey installed, add the Leiningen package by running the following command in a terminal window.
choco install lein\n
Create a directory called C:\\Users\\your-user-name\\AppData\\Local\\Programs\\Leiningen
Download the lein.bat
file and save it to the above directory
Open Environment variables for your account
and add the directory to your path
Open a command window and run the command: lein.bat
The full version of Leiningen will be downloaded and Leiningen is ready to use.
"},{"location":"alternative-tools/leiningen/#run-leiningen","title":"Run Leiningen","text":"Check Leiningen is working by running lein
command in a terminal
lein help\n
If a list of Leiningen commands is shown then it is working correctly.
"},{"location":"alternative-tools/leiningen/#create-project","title":"Create project","text":"Create a new Clojure project with Leiningen using the new
task
lein new template-name domain/project-name\n
Built-in templates include app
and lib
Create a new project called playground
Open a terminal window and in a directory where you usually keep your projects, run the following command
lein new app practicalli/playground\n
A new directory will be created called playground
with a src/practicalli/playground.clj
"},{"location":"alternative-tools/leiningen/#unit-testing","title":"Unit Testing","text":"Leiningen automatically includes the test
directory when running, so no additional configuration is required if all tests reside inside the test
directory.
Run all the tests saved to file:
lein test\n
Run just the unit tests in a specific namepsace.
lein test :only domain.namespace-test\n
"},{"location":"alternative-tools/leiningen/#test-plugins","title":"Test Plugins","text":"The following Leiningen plugins watch the file system and will run tests when a file change is detected in the project files.
- lein-test-refresh
- lein-auto
"},{"location":"alternative-tools/leiningen/#configure-test-paths","title":"Configure test paths","text":":test-paths
added as a top level key to the defproject
configuration in the project.clj
file will configure specific paths for tests
For example, if the tests are defined under project-name/clj/tests
then the project.clj file would look as follows:
(defproject my-project \"0.5.0-SNAPSHOT\"\n :description \"A project for doing things.\"\n :license \"Creative Commons Zero\"\n :url \"http://github.com/practicalli/my-project\"\n\n :dependencies [[org.clojure/clojure \"1.10.1\"]]\n :test-paths [\"clj/test\" \"src/test/clojure\"]\n :plugins [[lein-auto \"0.1.3\"]])\n
:source-paths
can also be used to define the location of the source code files in the same manner.
"},{"location":"assets/images/social/","title":"Social Cards","text":"Social Cards are visual previews of the website that are included when sending links via social media platforms.
Material for MkDocs is configured to generate beautiful social cards automatically, using the colors, fonts and logos defined in mkdocs.yml
Generated images are stored in this directory.
"},{"location":"automation/","title":"Automation","text":"Automation tools can provide a consistent command line interface across a wide range of projects.
Whilst the Clojure CLI is a very extensible tool that flexibility can also add some complexity to its command line interface.
Automation tools abstract the command line to provide a consistent and simple user experience whilst keeping underlying flexibility.
Practicalli recommends make
A Makefile is not reliant on programming language knowledge so has no barrier to those who are unfamiliar with the Clojure language.
Make is useful when working with mixed language teams to create a unified tool and command line across a wide range of projects.
"},{"location":"automation/#automation-tooling","title":"Automation tooling","text":" - Make - ubiquitous task automation tool, programming language agnostic
- Shell Scripts
- Babashka - create task automation tool with Clojure
"},{"location":"automation/#make","title":"Make","text":"Make is very simple to use and has a long history as a build tool and wider task automation tool.
Task are defined in a Makefile
and task can depend on each other. Any commands or combination of commands that run on the command line can be used as make tasks.
make provides tab completion of tasks defined in the Makefile without additional configuration.
make
is available for all operating systems.
Practicalli Project Templates include Makefile
Creating new projects with :project/create
and Practicalli Project Templates provide a Makefile with a wide range of common tasks for Clojure development.
"},{"location":"automation/#shell-scripts","title":"Shell Scripts","text":"Shell scripts provide a very common way to create a relatively ubiquitous approach to running tools, even across multiple Shell implementations (Bash, Zsh, fish, etc.) and operating systems.
Shell scripting language is very powerful especially for manipulation of the operating system, although scripts require development and maintenance.
"},{"location":"automation/#babashka","title":"Babashka","text":"Write automation scripts with Clojure code using the Babashka task runner
Babashka can use a wide range of Clojure functions and libraries, although as a general script tool then additional coding and maintenance may be reqiured compared to a dedicated tool.
Babashka task runner
"},{"location":"automation/make/","title":"Make","text":" practicalli/dotfiles Makefile
GNU Make provide a simple and consistent way to run any development task for Clojure & ClojureScript projects (or any other languages).
Wrap any combination of tools (building, linting, formatting, testing) with Make targets for a simple command line interface.
Make supports tab completion making tasks discoverable.
All that is required is a Makefile
in the root of the project
"},{"location":"automation/make/#gnu-make-overview","title":"GNU Make overview","text":"GNU Make is a language agnostic build automation tool which has been an integral part of building Linux/Unix operating system code and applications for decades, providing a consistent way to configure, compile and deploy code for all projects.
A Makefile
defines targets called via the make
command. Each target can run one or more commands. Targets can be dependent on other targets, e.g the dist
target that builds a project can be dependent on deps
& test
targets.
GNU Make is available on all Linux/Unix based operating systems (and Windows via chocolatey or nmake).
Practicalli also uses make
to configure and build the latest versions of Emacs and other Linux open source software
"},{"location":"automation/make/#defining-tasks","title":"Defining tasks","text":"Create a Makefile
in the root of a project and define a target by typing a suitable name followed by a :
character, e.g. test:
Insert a tab on the next line and type a command to be called. Further commands can be added on new lines so long as each line is tab indented.
The repl
target prints out an information message and then uses the Clojure CLI with aliases from practicalli/clojure-deps-edn to run a Clojure REPL process with a rich terminal UI (Rebel Readline)
repl: ## Run Clojure REPL with rich terminal UI (Rebel Readline)\n $(info --------- Run Rebel REPL ---------)\n clojure -M:dev/env:test/env:repl/rebel\n
"},{"location":"automation/make/#common-target-naming","title":"Common target naming","text":"Targets used across Practicalli projects follow the make standard targets for users
all
, test-ci
, deps
and dist
targets are recommended for use with a CI deployment pipeline and builder stage when using Docker.
all
calling all targets to prepare the application to be run. e.g. all: deps test-ci dist clean deps
download library dependencies (depend on deps.edn
file) dist
create a distribution tar file for this program or zip deployment package for AWS Lambda lint
run lint tools to check code quality - e.g MegaLinter which provides a wide range of tools format-check
report format and style issues for a specific programming language format-fix
update source code files if there are format and style issues for a specific programming language pre-commit
run unit tests and code quality targets before considering a Git commit repl
run an interactive run-time environment for the programming language test-unit
run all unit tests test-ci
test running in CI build (optionally focus on integration testing) clean
remove files created by any of the commands from other targets (i.e. ensure a clean build each time)
practicalli/dotfiles/Makefile also defines docker targets to build and compose images locally, inspect images and prune containers and images.
"},{"location":"automation/make/#target-dependencies","title":"Target dependencies","text":"A Makefile
target can depend on either a file name or another target in the Makefile
.
The all target typically depends on several Makefile
targets to test, compile and package a service. Add the names of the targets this target depends upon
all: deps test-ci dist clean\n
Add the filename of a file after the name of the target, to depend on if that file has changed. If the file has not changed since make was last run then the task will not run again.
Clojure CLI Example: If the deps
target depends on deps.edn
and the file has not changed since last run, the deps target will not run again.
"},{"location":"automation/make/#deps-target-depend-on-a-file","title":"deps target - depend on a file","text":"The deps target would use Clojure CLI or Leiningen to download dependencies.
Configuring the deps
target to depend on deps.edn
or project.clj
file, then if the file has not changed the deps will not run again.
A Clojure CLI example depends on the deps.edn
file that defines all the library dependencies for the project, tools for testing and packaging the Clojure service. The -P
flag is the prepare option, a dry run that only downloads the dependencies for the given tasks.
deps: deps.edn ## Prepare dependencies for test and dist targets\n $(info --------- Download libraries to test and build the service ---------)\n clojure -P -X:test/env:package/uberjar\n
:test/env
adds libraries to run Kaocha and libraries used to run unit tests. :package/uberjar
runs a tool that creates an uberjar.
"},{"location":"automation/make/#clean-target-hiding-command-errors","title":"Clean target - hiding command errors","text":"The clean target should remove files and directories created by the build (compile) process, to ensure a consistent approach to building each time.
On Linux / Unix systems files can be deleted with the rm
command using -r
for recursively deleting a directory and its contents. -f
forces the deleting of files and directories, otherwise a prompt for confirmation of the delete may be shown.
-
before a command instructs make
to ignore an error code, useful if the files to be deleted did not exist (i.e. the build failed part way through and not all files were created).
# `-` before the command ignores any errors returned\nclean:\n $(info --------- Clean Clojure classpath cache ---------)\n - rm -rf ./.cpcache\n
"},{"location":"automation/make/#megalinter-target-simplifying-a-command","title":"MegaLinter target - simplifying a command","text":"The lint
target is an example of how the Makefile
simplifies the command line interface.
lint:
target is used to call the MegaLinter runner, avoiding the need to remember the common options passed when calling MegaLinter command line tool, mega-linter-runner
The Java flavor of MegaLinter is less than 2Gb image (full MegaLinter image is 8Gb) and contains all the tools for a Clojure project. The flavor can only be set via a command line option, so the make file ensures that option is always used and the full MegaLinter docker image is not downloaded by mistake.
When MegaLinter is configured to generate reports (default), lint-clean:
target is used to clean those reports.
# Run MegaLinter with custom configuration\nlint:\n $(info --------- MegaLinter Runner ---------)\n mega-linter-runner --flavor java --env 'MEGALINTER_CONFIG=.github/linters/mega-linter.yml'\n\nlint-clean:\n $(info --------- MegaLinter Clean Reports ---------)\n - rm -rf ./megalinter-reports\n
"},{"location":"automation/make/#enhancing-make-output","title":"Enhancing make output","text":"The info
message is used with each target to enhances the readability of the make output, especially when multiple targets and commands are involved, or if commands are generating excessive output to standard out.
test:\n $(info --------- Runner for unit tests ---------)\n ./bin/test\n
"},{"location":"automation/make/#avoiding-file-name-collisions","title":"Avoiding file name collisions","text":"Although unlikely, if a filename in the root of a project has the same name as a Makefile
target, it can be used instead of running the targets command
.PHONY:
defines the names of targets in the Makefile
to avoid name clashes
.PHONY: all lint deps test test-ci dist clean\n
phony - MakefileTutorial
"},{"location":"automation/make/#halt-on-error","title":"Halt on error","text":".DELETE_ON_ERROR:
halts any further commands if a command returns non-zero exit status. Useful as short-circuit to stop tasks when further work is not valuable, e.g. if tests fail then it may not be valuable to build the Clojure project.
.DELETE_ON_ERROR:\nall: deps test-ci dist clean\n
delete_on_error - MakefileTutorial
"},{"location":"automation/make/#references","title":"References","text":"Makefile Tutorial by Example practicalli/dotfiles Makefile
"},{"location":"automation/make/#summary","title":"Summary","text":"A Makefile
can simplify the command line interface for any task with a Clojure project (or any other language and tooling).
Using the same target names across all projects reduces the cognitive load for driving any project.
"},{"location":"clojure-cli/","title":"Clojure CLI","text":"Learn by doing To learn Clojure CLI by doing, jump to Terminal REPL or Clojure project sections to start using the Clojure CLI
Clojure CLI (command line interface) is the latest approach to working with Clojure projects, libraries and tools. Clojure CLI focuses on:
- running Clojure code (applications and tools)
- starting a REPL process (Read-Eval-Print Loop) for interactive development with a Clojure editor or a command line REPL UI.
- managing dependencies (tools.deps) -downloading from Maven and Git repositories
- building Clojure projects (using tools.build) to create deployable Clojure services
Practicalli Clojure CLI Config extends the feautres of Clojure CLI, defining aliases that add community libraries and tools.
Video commands dated but concepts remain valid"},{"location":"clojure-cli/#common-tasks-for-clojure-development","title":"Common tasks for Clojure development","text":"The Clojure CLI has several built-in tasks. Additional tasks are provided via aliases that include libraries and tools from the Clojure community, e.g. Practicalli Clojure CLI Config
REPL Reloaded
clojure -M:repl/reloaded
runs a rich terminal UI REPL prompt that includes portal data inspector, namespace reloading and library hotload tools. test
path is included to support editor test runners and dev
path for custom user namespace for custom REPL startup
Task Command Defined In Basic terminal UI REPL clojure
(or clj
if rlwrap
binary installed) Clojure CLI Enhanced terminal UI REPL (Rebel & nREPL) clojure -M:repl/rebel
or clojure -M:repl/reloaded
Practicalli Create project clojure -T:project/new :template app :name domain/appname :args '[\"+h2\"]'
Practicalli Run unit tests / watch for changes clojure -X:test/run
or clojure -X:test/watch
Practicalli Run the project (clojure.main) clojure -M -m domain.main-namespace
No Alias Find libraries (maven & git) clojure -M:search/library library-name
Practicalli Find library versions (maven) clojure -X:deps find-versions domain/library-name
CLojure CLI Download dependencies clojure -P
(plus optional execution flags with aliases) CLojure CLI Check for new dependency versions clojure -T:search/outdated
Practicalli Package library clojure -X:build/jar
and clojure -X:build/uberjar
Practicalli Deploy library locally clojure -X:deps mvn-install
Clojure CLI Check code for unused vars clojure -X:search/unused
Practicalli tools.build is recommended for packaging projects
Package with tools.build is the recommended approach to create jar and Uberjar packages of a Clojure project.
"},{"location":"clojure-cli/#execution-option-flags","title":"Execution option flags","text":"The execution option flags for the clojure
command define how to run Clojure code. The most commonly used options are:
-M
uses clojure.main and calls the -main
function of the given namespace, passing positional string arguments. -X
uses clojure.exec to call a fully qualified function which has a map argument, passing key/value pair arguments. -T
is the same as -X
execpt setting the classpath to .
, ignoring project dependencies not defined in a given alias.
Flag Purpose -A
Pass alias to built-in terminal UI REPL (clojure
or clj
) -M
Run Clojure with clojure.main -P
Prepare / dry run (Build scripts, CI servers, Containers) -X
Execute a fully qualified function, optional default arguments -T
Run a tool independently from a project configuration -J
Java Virtual Machine specific options (heap size, etc) Examples of execution option flags
Execution option page expands on flag usage with numerous examples
"},{"location":"clojure-cli/#configure-clojure-cli","title":"Configure Clojure CLI","text":"A deps.edn
file configures the Clojure CLI, using extensible data notation (EDN), the underlying syntax of Clojure itself.
Configuration is defined using a hash-map with the following top-level keys:
:deps
- library dependencies :paths
- directories to search for code and resources (Java classpath) :aliases
- named configuration defining extra paths, extra deps and configuration to run Clojure :mvn/repos
- library dependency sources, remote and local (e.g. Clojars, Maven, Artifactory, etc).
:aliases
configuration is only included when using the alias name with the Clojure CLI, e.g. :repl/rebel
alias in Practicalli Clojure CLI Config adds library dependencies only used during development to run a rich terminal UI REPL.
clojure -M:repl/rebel\n
Add a wide range of aliases by installing Practicalli Clojure CLI Config Practicalli Clojure CLI Config provides aliases for a wide range of tools for use with Clojure CLI to support Clojure software development.
"},{"location":"clojure-cli/#precedence-order","title":"Precedence Order","text":"Clojure CLI Configuration can be used from several different sources.
Configuration Description Command line arguments string or edn (key value) arguments passed to the clojure
command project deps.edn
Project specific configuration: paths, dependencies, aliases $XDG_CONFIG_HOME/clojure/deps.edn
/ $HOME/.clojure/deps.edn
User level configuration for use with all projects Clojure CLI install Includes Clojure standard library, src
path and built-in :deps
aliases Command line arguments take preceedence over the other configurations. When running the clojure
command the configurations are merged, with key/values being added or replaces following the precedence order.
"},{"location":"clojure-cli/#install-configuration","title":"Install configuration","text":"Clojure CLI install has a built-in configuration:
org.clojure/clojure
library dependency, setting the default version of Clojure for the Clojure CLI src
set as the default path
Clojure CLI Install deps.edn The Clojure CLI install includes a deps.edn
configuration, e.g. /usr/local/lib/clojure/deps.edn
{\n :paths [\"src\"]\n\n :deps {\n org.clojure/clojure {:mvn/version \"1.11.1\"}\n }\n\n :aliases {\n :deps {:replace-paths []\n :replace-deps {org.clojure/tools.deps.cli {:mvn/version \"0.9.10\"}}\n :ns-default clojure.tools.deps.cli.api\n :ns-aliases {help clojure.tools.deps.cli.help}}\n :test {:extra-paths [\"test\"]}\n }\n\n :mvn/repos {\n \"central\" {:url \"https://repo1.maven.org/maven2/\"}\n \"clojars\" {:url \"https://repo.clojars.org/\"}\n }\n}\n
Check version of Clojure Evaluate *clojure-version*
in a REPL shows which version of the Clojure language is currently being used.
Including org.clojure/clojure
as a dependency the project deps.edn
file specifies a version of the Clojure language. The Clojure CLI version is used if no other dependency is specified.
"},{"location":"clojure-cli/#user-configuration","title":"User configuration","text":"A Clojure CLI user configuration is available to all projects by the operating system user account. Practicalli Clojure CLI Config is a user configuration that contains a set of well-formed aliases that add common tools for all Clojure project.
Clojure CLI User Configuration Location Clojure CLI tools creates a configuration directory called .clojure
, which by default is placed in the root of the operating system user account directory, e.g. $HOME/.clojure
.
XDG_CONFIG_HOME
may be set by your operating system and over-rides the default location, e.g. $HOME/.config/.clojure
CLJ_CONFIG
can be used to over-ride all other location settings
Run clojure -Sdescribe
in a terminal and checking the :config-user
value to see the location of your Clojure configuration directory
A basic example of a user configuration for Clojure CLI
{\n :aliases {\n :test/env {:extra-paths [\"test\"]}\n\n :project/new\n {:extra-deps {seancorfield/clj-new {:mvn/version \"1.0.199\"}}\n :main-opts [\"-m\" \"clj-new.create\"]}\n }\n\n :mvn/repos {\n \"central\" {:url \"https://repo1.maven.org/maven2/\"}\n \"clojars\" {:url \"https://repo.clojars.org/\"}\n }\n}\n
Clojure Tools install sets Clojure version
A default version of Clojure is set by the Clojure tools install, enabling the clojure
command to know what version of Clojure library to use. This version will be over-ridden by the user or project specific deps.edn configuration files if set.
"},{"location":"clojure-cli/#references","title":"References","text":"tools.deps and cli guide clojure.main API Reference tools.deps.alpha API Reference
"},{"location":"clojure-cli/built-in-commands/","title":"Clojure CLI Built-in commands","text":"clojure
without any other arguments will run a REPL with a basic terminal prompt.
clj
is a wrapper script for the clojure
command (rlwrap
required) to add command history to the basic REPL prompt.
clojure -T:deps
to run one of several built-in commands to help work with Clojure CLI projects and libraries.
clojure --help
list the available commands.
"},{"location":"clojure-cli/built-in-commands/#deps-tool","title":":deps tool","text":"The :deps
aliases provides tools for managing library dependencies.
The Clojure CLI -X
flag is used to call these tools via clojure.exec
and take key value pairs as arguments
aliases Description clojure -X:deps list
List full transitive deps set and licenses clojure -X:deps tree
download dependencies & print dependency tree, indenting libraries that are dependencies of dependencies clojure -X:deps find-versions
Find available versions of a given library (domain/name) clojure -X:deps prep
Prepare all unprepped libs in the dep tree clojure -X:deps mvn-pom
Generate or update pom.xml with deps and paths clojure -X:deps mvn-install :jar '\"/path/to.jar\"'
install a given jar file into the local maven repository, eg. ~/.m2/repository
clojure -X:deps git-resolve-tags
update deps.edn
git based dependencies that used tags with the equivalent SHA commit values tools.deps.alpha API Reference
Libraries are downloaded if they are not in the local Maven cache, e.g. $HOME/.m2/repository
, so on first run a command may take a little time.
Use Clojure CLI -P flag to download libraries clojure -P
is the recommended way to download library dependencies as it does not run any other commands
The -P
flag can be used with aliases to download libraries for building and testing a Clojure project, especially useful for a cached environment like Docker.
clojure -P -M:dev/reloaded:project/build\n
"},{"location":"clojure-cli/built-in-commands/#list-library-dependencies","title":"List Library dependencies","text":"List library depenencies of a project, including the library version and software licence for each library.
The list includes transitive dependencies, library dependencies of the project library dependencies.
The :aliases '[:alias/name(s)]'
argument will also list library dependencies from a given alias, either project alias or user alias.
clojure -X:deps list\n
Dependency list from Practicalli Service project template \u276f clojure -X:deps list\naero/aero 1.1.6 (MIT)\namalloy/ring-buffer 1.3.1 (EPL-1.0)\naysylu/loom 1.0.2 (EPL-1.0)\nborkdude/dynaload 0.2.2 (EPL-1.0)\nborkdude/edamame 0.0.18 (EPL-1.0)\ncom.bhauman/spell-spec 0.1.2 (EPL-1.0)\ncom.brunobonacci/mulog 0.9.0 (Apache-2.0)\ncom.brunobonacci/mulog-adv-console 0.9.0 (Apache-2.0)\ncom.brunobonacci/mulog-json 0.9.0 (Apache-2.0)\ncom.cnuernber/charred 1.010\ncom.cognitect/transit-clj 1.0.324 (Apache-2.0)\ncom.cognitect/transit-java 1.0.343 (Apache-2.0)\ncom.fasterxml.jackson.core/jackson-annotations 2.12.1 (Apache-2.0)\ncom.fasterxml.jackson.core/jackson-core 2.12.1 (Apache-2.0)\ncom.fasterxml.jackson.core/jackson-databind 2.12.1 (Apache-2.0)\ncom.fasterxml.jackson.datatype/jackson-datatype-jsr310 2.12.0 (Apache-2.0)\ncom.google.javascript/closure-compiler v20151015 (Apache-2.0)\ncom.googlecode.json-simple/json-simple 1.1.1 (Apache-2.0)\ncommons-codec/commons-codec 1.15 (Apache-2.0)\ncommons-fileupload/commons-fileupload 1.4 (Apache-2.0)\ncommons-io/commons-io 2.6 (Apache-2.0)\ncrypto-equality/crypto-equality 1.0.0 (EPL-1.0)\ncrypto-random/crypto-random 1.2.0 (EPL-1.0)\nexpound/expound 0.9.0 (EPL-1.0)\nfipp/fipp 0.6.25 (EPL-1.0)\nhttp-kit/http-kit 2.6.0 (Apache-2.0)\njavax.xml.bind/jaxb-api 2.3.0 (CDDL 1.1)\nlambdaisland/deep-diff 0.0-47 (EPL-1.0)\nmeta-merge/meta-merge 1.0.0 (EPL-1.0)\nmetosin/jsonista 0.3.1 (EPL-1.0)\nmetosin/malli 0.7.5 (EPL-2.0)\nmetosin/muuntaja 0.6.8 (EPL-1.0)\nmetosin/reitit 0.5.13 (EPL-1.0)\nmetosin/reitit-core 0.5.18 (EPL-1.0)\nmetosin/reitit-dev 0.5.18 (EPL-1.0)\nmetosin/reitit-frontend 0.5.13 (EPL-1.0)\nmetosin/reitit-http 0.5.13 (EPL-1.0)\nmetosin/reitit-interceptors 0.5.13 (EPL-1.0)\nmetosin/reitit-malli 0.5.13 (EPL-1.0)\nmetosin/reitit-middleware 0.5.13 (EPL-1.0)\nmetosin/reitit-ring 0.5.13 (EPL-1.0)\nmetosin/reitit-schema 0.5.13 (EPL-1.0)\nmetosin/reitit-sieppari 0.5.13 (EPL-1.0)\nmetosin/reitit-spec 0.5.13 (EPL-1.0)\nmetosin/reitit-swagger 0.5.13 (EPL-1.0)\nmetosin/reitit-swagger-ui 0.5.13 (EPL-1.0)\nmetosin/ring-swagger-ui 3.36.0 (EPL-1.0)\nmetosin/schema-tools 0.12.3 (EPL-1.0)\nmetosin/sieppari 0.0.0-alpha13 (EPL-1.0)\nmetosin/spec-tools 0.10.5 (EPL-1.0)\nmvxcvi/arrangement 2.0.0 (Public Domain)\nmvxcvi/puget 1.1.2 (Public Domain)\norg.clojure/clojure 1.11.1 (EPL-1.0)\norg.clojure/clojurescript 1.7.170 (EPL-1.0)\norg.clojure/core.rrb-vector 0.0.14 (EPL-1.0)\norg.clojure/core.specs.alpha 0.2.62 (EPL-1.0)\norg.clojure/data.json 0.2.6 (EPL-1.0)\norg.clojure/data.priority-map 0.0.5 (EPL-1.0)\norg.clojure/google-closure-library 0.0-20151016-61277aea (Apache-2.0)\norg.clojure/google-closure-library-third-party 0.0-20151016-61277aea (Apache-2.0)\norg.clojure/java.classpath 1.0.0 (EPL-1.0)\norg.clojure/spec.alpha 0.3.218 (EPL-1.0)\norg.clojure/test.check 1.1.1 (EPL-1.0)\norg.clojure/tools.namespace 1.3.0 (EPL-1.0)\norg.clojure/tools.reader 1.3.6 (EPL-1.0)\norg.javassist/javassist 3.18.1-GA (MPL 1.1)\norg.mozilla/rhino 1.7R5 (Mozilla Public License, Version 2.0)\norg.msgpack/msgpack 0.6.12 (Apache-2.0)\nparty.donut/system 0.0.202 \nprismatic/schema 1.1.12 (EPL-1.0)\nring/ring-codec 1.1.3 (MIT)\nring/ring-core 1.9.1 (MIT)\ntailrecursion/cljs-priority-map 1.2.1 (EPL-1.0)\ntech.droit/clj-diff 1.0.1 (EPL-1.0)\n
Use the :aliases '[:alias/name(s)]'
option to also include the dependencies from a project or user alias. Showing the dependencies from an aliase can useful to identify conflicting dependencies when using an alias (unlikely but it could happen).
clojure -X:deps list :aliases '[:dev/reloaded]'\n
Dependency list from project and Practicalli :dev/reloaded alias \u276f clojure -X:deps list :aliases '[:dev/reloaded]'\naero/aero 1.1.6 (MIT)\namalloy/ring-buffer 1.3.1 (EPL-1.0)\nclj-commons/clj-yaml 1.0.27 (EPL-1.0)\ncom.brunobonacci/mulog 0.9.0 (Apache-2.0)\ncom.cognitect/transit-clj 1.0.333 (Apache-2.0)\ncom.cognitect/transit-cljs 0.8.280 (Apache-2.0)\ncom.cognitect/transit-java 1.0.371 (Apache-2.0)\ncom.cognitect/transit-js 0.8.874 (Apache-2.0)\ncom.fasterxml.jackson.core/jackson-core 2.14.2 (Apache-2.0)\ncom.google.code.gson/gson 2.10.1 (Apache-2.0)\ncom.googlecode.json-simple/json-simple 1.1.1 (Apache-2.0)\ncom.nextjournal/beholder 1.0.2 \ncriterium/criterium 0.4.6 (EPL-1.0)\ndjblue/portal 0.49.0 (MIT)\nexpound/expound 0.9.0 (EPL-1.0)\nfipp/fipp 0.6.26 (EPL-1.0)\nhawk/hawk 0.2.11 (EPL-1.0)\nhttp-kit/http-kit 2.7.0 (Apache-2.0)\nio.methvin/directory-watcher 0.17.3 (Apache-2.0)\njavax.activation/javax.activation-api 1.2.0 (CDDL/GPLv2+CE)\njavax.xml.bind/jaxb-api 2.4.0-b180830.0359 (CDDL 1.1)\nlambdaisland/clj-diff 1.4.78 (EPL-1.0)\nlambdaisland/deep-diff2 2.10.211 (EPL-1.0)\nlambdaisland/kaocha 1.87.1366 (EPL-1.0)\nlambdaisland/tools.namespace 0.3.256 (EPL-1.0)\nmeta-merge/meta-merge 1.0.0 (EPL-1.0)\nmvxcvi/arrangement 2.1.0 (Public Domain)\nnet.incongru.watchservice/barbary-watchservice 1.0 (GPLv2 + Classpath Exception)\nnet.java.dev.jna/jna 5.12.1 (LGPL-2.1-or-later)\norg.clojure/clojure 1.11.1 (EPL-1.0)\norg.clojure/core.rrb-vector 0.1.2 (EPL-1.0)\norg.clojure/core.specs.alpha 0.2.62 (EPL-1.0)\norg.clojure/data.json 2.4.0 (EPL-1.0)\norg.clojure/java.classpath 1.0.0 (EPL-1.0)\norg.clojure/spec.alpha 0.3.218 (EPL-1.0)\norg.clojure/test.check 1.1.1 (EPL-1.0)\norg.clojure/tools.cli 1.0.219 (EPL-1.0)\norg.clojure/tools.namespace 1.4.4 (EPL-1.0)\norg.clojure/tools.reader 1.3.6 (EPL-1.0)\norg.clojure/tools.trace 0.7.11 (EPL-1.0)\norg.flatland/ordered 1.15.11 (EPL-1.0)\norg.javassist/javassist 3.18.1-GA (MPL 1.1)\norg.msgpack/msgpack 0.6.12 (Apache-2.0)\norg.slf4j/slf4j-api 2.0.9 (MIT)\norg.slf4j/slf4j-nop 2.0.9 (MIT)\norg.tcrawley/dynapath 1.1.0 (EPL-1.0)\norg.yaml/snakeyaml 2.1 (Apache-2.0)\nprogrock/progrock 0.1.2 (EPL-1.0)\nslingshot/slingshot 0.12.2 (EPL-1.0)\n
The :aliases
option can be used to inspect the dependencies of a user alias, listing only dependencies from the specified aliases when run outside of a Clojure project.
Dependency list from Practicalli :repl/rebel alias only \u276f clojure -X:deps list :aliases '[:repl/rebel]'\ncider/cider-nrepl 0.42.1 (EPL-1.0)\ncider/orchard 0.18.0 (EPL-1.0)\ncljfmt/cljfmt 0.5.7 (EPL-1.0)\ncom.bhauman/rebel-readline 0.1.4 (EPL-1.0)\ncom.google.javascript/closure-compiler v20151216 (Apache-2.0)\ncompliment/compliment 0.3.6 (EPL-1.0)\nmx.cider/logjam 0.1.1 (EPL-1.0)\nnrepl/nrepl 1.1.0 (EPL-1.0)\norg.clojure/clojure 1.11.1 (EPL-1.0)\norg.clojure/clojurescript 1.7.228 (EPL-1.0)\norg.clojure/core.specs.alpha 0.2.62 (EPL-1.0)\norg.clojure/data.json 0.2.6 (EPL-1.0)\norg.clojure/google-closure-library 0.0-20151016-61277aea (Apache-2.0)\norg.clojure/google-closure-library-third-party 0.0-20151016-61277aea (Apache-2.0)\norg.clojure/spec.alpha 0.3.218 (EPL-1.0)\norg.clojure/tools.reader 1.0.0-alpha4 (EPL-1.0)\norg.fusesource.jansi/jansi 1.16 (Apache-2.0)\norg.jline/jline-reader 3.5.1 (The BSD License)\norg.jline/jline-terminal 3.5.1 (The BSD License)\norg.jline/jline-terminal-jansi 3.5.1 (The BSD License)\norg.mozilla/rhino 1.7R5 (Mozilla Public License, Version 2.0)\nrewrite-clj/rewrite-clj 0.5.2 (MIT)\nrewrite-cljs/rewrite-cljs 0.4.3 (MIT)\n
"},{"location":"clojure-cli/built-in-commands/#dependency-tree","title":"Dependency tree","text":"Show a tree hieracthy of project library dependencies, including the library name and version used.
The tree includes transitive dependencies, library dependencies of the project library dependencies.
The :aliases '[:alias/name(s)]'
argument will also list library dependencies from a given alias, either project alias or user alias.
clojure -X:deps tree\n
Dependency list from Practicalli Service project template \u276f clojure -X:deps tree\norg.clojure/clojure 1.11.1\n . org.clojure/spec.alpha 0.3.218\n . org.clojure/core.specs.alpha 0.2.62\nhttp-kit/http-kit 2.6.0\nmetosin/reitit 0.5.13\n X metosin/reitit-core 0.5.13 :superseded\n X meta-merge/meta-merge 1.0.0 :parent-omitted\n X metosin/reitit-dev 0.5.13 :use-top\n . metosin/reitit-spec 0.5.13\n X metosin/reitit-core 0.5.13 :older-version\n . metosin/spec-tools 0.10.5\n X org.clojure/spec.alpha 0.2.187 :older-version\n . metosin/reitit-malli 0.5.13\n X metosin/reitit-core 0.5.13 :older-version\n X metosin/malli 0.3.0 :older-version\n . metosin/reitit-schema 0.5.13\n X metosin/reitit-core 0.5.13 :older-version\n . metosin/schema-tools 0.12.3\n . prismatic/schema 1.1.12\n . metosin/reitit-ring 0.5.13\n X metosin/reitit-core 0.5.13 :older-version\n . ring/ring-core 1.9.1\n . ring/ring-codec 1.1.3\n . commons-codec/commons-codec 1.15\n . commons-io/commons-io 2.6\n . commons-fileupload/commons-fileupload 1.4\n X commons-io/commons-io 2.2 :older-version\n . crypto-random/crypto-random 1.2.0\n X commons-codec/commons-codec 1.6 :older-version\n . crypto-equality/crypto-equality 1.0.0\n . metosin/reitit-middleware 0.5.13\n . metosin/reitit-ring 0.5.13\n . lambdaisland/deep-diff 0.0-47\n . mvxcvi/puget 1.1.2\n X mvxcvi/arrangement 1.2.0 :older-version\n X fipp/fipp 0.6.17 :older-version\n X fipp/fipp 0.6.17 :older-version\n . org.clojure/core.rrb-vector 0.0.14\n . tech.droit/clj-diff 1.0.1\n X mvxcvi/arrangement 1.2.0 :older-version\n . metosin/muuntaja 0.6.8\n . metosin/jsonista 0.3.1\n . com.cognitect/transit-clj 1.0.324\n . com.cognitect/transit-java 1.0.343\n X com.fasterxml.jackson.core/jackson-core 2.8.7 :older-version\n . org.msgpack/msgpack 0.6.12\n . com.googlecode.json-simple/json-simple 1.1.1\n . org.javassist/javassist 3.18.1-GA\n X commons-codec/commons-codec 1.10 :older-version\n . javax.xml.bind/jaxb-api 2.3.0\n . metosin/spec-tools 0.10.5\n . metosin/reitit-http 0.5.13\n X metosin/reitit-core 0.5.13 :older-version\n . metosin/reitit-ring 0.5.13\n . metosin/reitit-interceptors 0.5.13\n . metosin/reitit-ring 0.5.13\n . lambdaisland/deep-diff 0.0-47\n . metosin/muuntaja 0.6.8\n . metosin/reitit-swagger 0.5.13\n X metosin/reitit-core 0.5.13 :older-version\n . metosin/reitit-swagger-ui 0.5.13\n . metosin/reitit-ring 0.5.13\n . metosin/jsonista 0.3.1\n X com.fasterxml.jackson.core/jackson-core 2.12.0 :older-version\n X com.fasterxml.jackson.core/jackson-databind 2.12.0 :older-version\n . com.fasterxml.jackson.datatype/jackson-datatype-jsr310 2.12.0\n X com.fasterxml.jackson.core/jackson-annotations 2.12.0 :older-version\n X com.fasterxml.jackson.core/jackson-core 2.12.0 :older-version\n X com.fasterxml.jackson.core/jackson-databind 2.12.0 :older-version\n . metosin/ring-swagger-ui 3.36.0\n . metosin/reitit-frontend 0.5.13\n X metosin/reitit-core 0.5.13 :older-version\n . metosin/reitit-sieppari 0.5.13\n X metosin/reitit-core 0.5.13 :older-version\n . metosin/sieppari 0.0.0-alpha13\n . com.fasterxml.jackson.core/jackson-core 2.12.1\n . com.fasterxml.jackson.core/jackson-databind 2.12.1\n . com.fasterxml.jackson.core/jackson-annotations 2.12.1\n . com.fasterxml.jackson.core/jackson-core 2.12.1\nmetosin/reitit-dev 0.5.18\n . metosin/reitit-core 0.5.18 :newer-version\n . meta-merge/meta-merge 1.0.0\n . com.bhauman/spell-spec 0.1.2\n . expound/expound 0.9.0\n . fipp/fipp 0.6.25\ncom.brunobonacci/mulog 0.9.0\n . amalloy/ring-buffer 1.3.1\ncom.brunobonacci/mulog-adv-console 0.9.0\n X com.brunobonacci/mulog 0.9.0 :use-top\n . com.brunobonacci/mulog-json 0.9.0\n X com.brunobonacci/mulog 0.9.0 :use-top\n . com.cnuernber/charred 1.010\naero/aero 1.1.6\nparty.donut/system 0.0.202\n . aysylu/loom 1.0.2\n . org.clojure/data.priority-map 0.0.5\n . tailrecursion/cljs-priority-map 1.2.1\n . org.clojure/clojurescript 1.7.170\n . com.google.javascript/closure-compiler v20151015\n . org.clojure/google-closure-library 0.0-20151016-61277aea\n . org.clojure/google-closure-library-third-party 0.0-20151016-61277aea\n . org.clojure/data.json 0.2.6\n . org.mozilla/rhino 1.7R5\n X org.clojure/tools.reader 0.10.0-alpha3 :older-version\n . org.clojure/tools.namespace 1.3.0\n . org.clojure/java.classpath 1.0.0\n . org.clojure/tools.reader 1.3.6\n . metosin/malli 0.7.5\n . borkdude/dynaload 0.2.2\n . borkdude/edamame 0.0.18\n X org.clojure/tools.reader 1.3.4 :older-version\n . org.clojure/test.check 1.1.1\n X fipp/fipp 0.6.24 :older-version\n . mvxcvi/arrangement 2.0.0\n
Use the :aliases
option with the Clojure CLI in the root of a project to show library dependencies for the project and the :dev/reloaded
alias which could be useful if there are library conflicts when using an alias (unlikely but it could happen).
clojure -X:deps tree :aliases '[:dev/reloaded]'\n
The :aliases
option can be used to inspect the dependencies of a user alias, listing only dependencies from the specified aliases when run outside of a Clojure project.
Dependency list from Practicalli :repl/rebel alias only \u276f clojure -X:deps tree :aliases '[:repl/rebel]'\norg.clojure/clojure 1.11.1\n . org.clojure/spec.alpha 0.3.218\n . org.clojure/core.specs.alpha 0.2.62\nnrepl/nrepl 1.1.0\ncider/cider-nrepl 0.42.1\n X nrepl/nrepl 1.0.0 :use-top\n . cider/orchard 0.18.0\n . mx.cider/logjam 0.1.1\ncom.bhauman/rebel-readline 0.1.4\n . org.jline/jline-reader 3.5.1\n . org.jline/jline-terminal 3.5.1\n . org.jline/jline-terminal 3.5.1\n . org.jline/jline-terminal-jansi 3.5.1\n . org.fusesource.jansi/jansi 1.16\n . org.jline/jline-terminal 3.5.1\n . cljfmt/cljfmt 0.5.7\n . org.clojure/tools.reader 1.0.0-alpha4\n . rewrite-clj/rewrite-clj 0.5.2\n X org.clojure/tools.reader 0.10.0 :older-version\n . rewrite-cljs/rewrite-cljs 0.4.3\n . org.clojure/clojurescript 1.7.228\n . com.google.javascript/closure-compiler v20151216\n . org.clojure/google-closure-library 0.0-20151016-61277aea\n . org.clojure/google-closure-library-third-party 0.0-20151016-61277aea\n . org.clojure/data.json 0.2.6\n . org.mozilla/rhino 1.7R5\n X org.clojure/tools.reader 1.0.0-alpha1 :older-version\n X org.clojure/tools.reader 1.0.0-alpha3 :older-version\n . compliment/compliment 0.3.6\n
"},{"location":"clojure-cli/built-in-commands/#local-library-install","title":"Local library install","text":"Add a jar file for a library to the local Maven repository, e.g. ~/.m2/repository
, making that library accessible to all other local projects.
clojure -X:deps mvn-install :jar '\"/path/to.jar\"'`\n
"},{"location":"clojure-cli/built-in-commands/#find-library-versions","title":"Find Library Versions","text":"Find the available versions of a given library in the form domain/library-name (domain is typically the company name or Git Service user or organisation name).
clojure -X:deps find-versions :lib clojure.java-time/clojure.java-time\n
"},{"location":"clojure-cli/built-in-commands/#prepare-source-dependencies","title":"Prepare Source dependencies","text":"Some dependencies will require a preparation step before they can be used on the classpath.
Projects that require preparation would have a configuration of the form:
Example
{:paths [\"src\" \"target/classes\"]\n :deps/prep-lib {:alias :build\n :fn compile\n :ensure \"target/classes\"}}\n
Including the top-level key :deps/prep-lib
tells the tools.deps classpath construction that something extra is needed to prepare this lib and that can be performed by invoking the compile function in the :build alias. Once the prepare step has been done, it should create the path \"target/classes\" and that can be checked for completion.
Add a library dependency as with any other library (git or local/root):
Example
{:deps {practicalli/library-name {:local/root \"../needs-prep\"}\n practicalli/library-name {:git/sha \"../needs-prep\"}}}\n
:deps prep
will built the library of any dependency that requires it
clojure -X:deps prep\n
"},{"location":"clojure-cli/built-in-commands/#pom-file","title":"POM file","text":"Generate or update pom.xml with deps and paths
clojure -X:deps mvn-pom\n
"},{"location":"clojure-cli/built-in-commands/#resolve-git-tags","title":"Resolve Git tags","text":"-X:deps git-resolve-tags
updates git based dependencies in the project deps.edn
file which use :git/tags key to the equivalent SHA commit values in the :git/sha
key
clojure -X:deps git-resolve-tags\n
"},{"location":"clojure-cli/built-in-commands/#references","title":"References","text":"tools.deps and cli guide clojure.main guide clojure.main API Reference tools.deps.alpha API Reference
"},{"location":"clojure-cli/clojure-style/","title":"Clojure Style","text":"Code is easier to read and work with when it is consistent format that follows common rules.
Clojure community style guide provides a common style for Clojure code. While most style recommendations are widely used, others are more contentious. Ultimately the development team for the project should define a workable set of style rules that makes them productions, ideally using much of those rules from the style guide.
A consistent format between editors also minimises version control changes not related to code design. The following format tools for clojure can all be configured to be consistent with each other (although zprint defaults will require more customisation):
- cljfmt - library, also included in Clojure LSP
- cljstyle - binary and library (re-write of cljfmt)
- zprint - binary & library
Tooling that uses the Clojure Style Guide Emacs clojure-mode
and Clojure LSP (via cljfmt) format code following the most common Clojure style guide rules, although cljfmt rules are quite strick so Practicalli disables many of them.
cljstyle default configuration follows the majority of styles and has the same defaults as cljfmt. Practicalli Clojure CLI Config tweaks a few rules to make code more readable and allow for repl design experiments.
"},{"location":"clojure-cli/clojure-style/#cljstyle","title":"cljstyle","text":"Cljstyle is a rewrite of cljfmt, designed to be easier to configure. The default rules implement many of the style rules from the Clojure community style guide and is compatible with cljfmt.
Call with the check
option to report formatting issues, providing a coloured diff view of the format changes
Call with fix
option to automatically update all Clojure files with fixes, indicating which files have changed.
Cljstyle will examine all files in the current directory and any sub-directories.
.cljstyle
configuration file in the root of the project can override the default customisation, including indentation rules.
cljstyle config used by Practicalli
Clojure App template repository contains the .cljstyle
configuration file used for all Practicalli projects
Binary Practicalli Clojure CLI ConfigMakefile Install the latest binary release from the cljstyle GitHub repository onto the operating system path, e.g. $HOME/.local/bin
cljstyle check\n
fix
option automatically updates all source code files that have format issues.
cljstyle fix\n
cljstyle can be used as a library without installing the cljstyle binary. Practicalli Clojure CLI Config defines the :format/cljstyle
alias which should be passed wither the check
or format
option
Check all the Clojure files (.clj .cljc .edn .cljs) in the current project
clojure -M:format/cljstyle\n
clojure -M:format/cljstyle!\n
Clojure Alias for cljstyle
:format/cljstyle\n{:extra-deps\n {mvxcvi/cljstyle {:git/url \"https://github.com/greglook/cljstyle.git\"\n :git/sha \"14c18e5b593c39bc59f10df1b894c31a0020dc49\"}}\n :main-opts [\"-m\" \"cljstyle.main\" \"check\"]}\n\n:format/cljstyle!\n{:extra-deps\n {mvxcvi/cljstyle {:git/url \"https://github.com/greglook/cljstyle.git\"\n :git/sha \"14c18e5b593c39bc59f10df1b894c31a0020dc49\"}}\n :main-opts [\"-m\" \"cljstyle.main\" \"fix\"]}\n
Use a Makefile to run common commands such as checking style, running tests, building uberjars, etc.
Practicalli Clojure App template repository contains an example Makefile that contains common tasks for Clojure development
This example calls the cljstyle binary, but could be changed to call the clojure -M:format/cljstyle check
and clojure -M:format/cljstyle fix
aliases instead.
# ------- Code Quality --------------- #\nformat-check: ## Run cljstyle to check the formatting of Clojure code\n $(info --------- cljstyle Runner ---------)\n cljstyle check\n\nformat-fix: ## Run cljstyle and fix the formatting of Clojure code\n $(info --------- cljstyle Runner ---------)\n cljstyle fix\n# ------------------------------------ #\n
Stage changes before automatically fixing format
Practicalli suggests staging (or committing) changes before running cljstyle fix
to easily undo undesired changes or simply confirm what changes have been made
"},{"location":"clojure-cli/clojure-style/#recommended-configuration","title":"Recommended configuration","text":"Practicalli updated the default cljstyle configuration with the following changes
Configure list indent to one character
.cljstyle :indentation\n {:enabled? true,\n :list-indent 1,\n\n }\n
Do not warn about duplicate var names (def, defn names) - excluded to stop warning about REPL experiments and design journal rich comments that contain alternative designs.
.cljstyle :vars\n {:enabled? false}\n
"},{"location":"clojure-cli/clojure-style/#cljfmt","title":"cljfmt","text":"cljfmt is not available as a separate binary, although it a fixed part of the Clojure LSP server implementation.
whist typing Clojure code, Clojure LSP will format using cljfmt rules
Define a cljfmt configuration via Clojure LSP to define rules and indentation settings for all projects.
.config/clojure-lsp/config.edn :cljfmt-config-path \"cljfmt.edn\"\n
Or specify cljfmt configuration within the Clojure LSP configuration file
.config/clojure-lsp/config.edn :cljfmt {}\n
Practicalli Clojure LSP config - LSP and cljfmt Practicalli Clojure LSP config provides an example config.edn configuration file for Clojure LSP that uses a cljfmt.edn configuration file for a minimum set of Clojure format rules
The default cljfmt rules feel overly strict and Practicalli configuration disables the more draconian rules to make code far more readable
"},{"location":"clojure-cli/clojure-style/#zprint","title":"zprint","text":"zprint is a highly configurable format tool for both Clojure code and Clojure/EDN structures, available as a library and command line tool
zprint has advanced features over cljstyle and cljfmt, although may require some additional configuration work especially to format consistently with these tools.
zprint available styles
No built-in diff option zprint requires an external diff tool to see the format changes made, as zprint only reports on the files changed and not the content of those files that has changed.
zprint can write changes to a new file and a file comparison made. Or files can be staged / committed in a local Git repository before running zprint and a Git client used to see the diff.
Once the desirable styles and configuration are established there is less need for an external diff tool, although its always useful to have a quick way to check what format tools are doing.
Binary Practicalli Clojure CLI ConfigNode.js Download zprint for Linux or MacOSX using the latest binary released on the GitHub repository
Move the binary to the executable path for the operating system, updating the name to zprint
(or use a symbolic link)
mv ~/Downloads/zprintl-1.2.5 ~/.local/bin/zprint\n
Make the binary executable chmod a+x ~/.local/bin/zprint\n
Ensure the zprint binary is working and examine the default configuration for zprint, including all default values and highlighting where non-default values are set zprint --explain-all\n
Using zprint to check the Clojure files in the current directory and list which files require formatting zprint --formatted-check *.clj\n
A more detailed zprint report checking all the Clojure files a project, including files in the route directory and all sub-directories (i.e. **/*.cjl
pattern)
zprint --list-formatted-summary-check **/*.clj **/*.edn\n
Or using short form flags zprint -lfsc **/*.clj **/*.edn\n
Update formatting for all the files in a projects, showing details of the files processed and changed
zprint -lfsw **/*.clj *.edn *.clj\n
zprint can be used as a library without installing the binary. Practicalli Clojure CLI Config defines the :format/zprint
alias which checks the format of a file and reports which files required
clojure -M:format/zprint deps.edn\n
clojure -M:format/zprint filename\n
Clojure Alias for zprint Add :format/zprint
alias to check format and :format/zprint!
to write format changes to a given file or filename pattern User or project deps.edn file
:format/zprint\n{:extra-deps {zprint/zprint {:mvn/version \"1.2.4\"}}\n :main-opts [\"-m\" \"zprint.main\"\n \"{:style :indent-only}\"\n \"--list-formatted-summary-check\"]}\n\n:format/zprint!\n{:extra-deps {zprint/zprint {:mvn/version \"1.2.4\"}}\n :main-opts [\"-m\" \"zprint.main\"\n \"{:style :indent-only}\"\n \"--list-formatted-summary-write\"]}\n
Use the alise zprint is available as an NPM package
sudo --install --global zprint-clj\n
Run zprint-clj over all Clojure files zprint-clj **/*.{clj,cljs,cljc,edn}\n
"},{"location":"clojure-cli/clojure-style/#configure-zprint","title":"Configure zprint","text":"It is assumed that the majority of format needs are met by one of the following style rule sets
{:style :indent-only}
only formats indentation, less likely to change the general style of code {:style :community}
a quite strict adhearence to the Clojure Community Guide (which Practicalli finds a little to strict)
Unless the code is really messy (e.g. not written in a clojure aware editor with live linting) then {:style :indent-only}
is a simple starting point.
If the team have adopted most if not all community styles, then {:style :community}
may be a more appropriate start point. Use --explain-all flag with the zprint command to see all the rules that are applied with a partiular style and modify as appropriate
$HOME/.zprintrc
is used for the configuration applied to all files, although this can be overridden in each project (or even as zprint comments in particular files)
zprint - GitHub repo zprint - clojars zprint - cljdoc
"},{"location":"clojure-cli/defining-aliases/","title":"Defining aliases","text":"Aliases extend the built-in functionality of Clojure CLI via community libraries and tools, either in a project specific or a user deps.edn
configuration file.
Aliases are explicitly added to the clojure
command, e.g. clojure -M:repl/rebel
to start a repl with rebel rich terminal UI.
Aliases are optional configuration that supports the development workflow.
Aliases can be used to :
- add libraries and directories to the class path
- configure how to run community tools and provide default options
Understanding Clojure CLI Execution Options Understand the execution options (exec-opts) on the command line options ensures an effective use of Clojure CLI tools.
"},{"location":"clojure-cli/defining-aliases/#configuration-file","title":"Configuration file","text":"deps.edn
is an EDN configuration file containing a single hash-map with several top-level keywords. All keywords are optional.
:paths
- directories included by default as a vector of directory names, e.g. [\"src\" \"resources\"]
:deps
- library dependencies included by default as a map (practicalli/banking-on-clojure example) :mvn/repos
- a map of repositories to download Maven dependencies, Maven Central and Clojars included by default :mvn/local-repo
to specify an alternative location for the Maven cache :aliases
- a map of optional libraries and tools, the key being the alias name and its value the configuration
The installation of Clojure CLI contains a configuration
- adds
src
and org.clojure/clojure
library - Maven Central & Clojars.org repository sources.
Configuration available to all projects (or stand-alone tools) is defined in a user deps.edn
configuration in either $XDG_CONFIG_HOME/clojure
or $HOME/.clojure
.
Project specific configuration is defined in a deps.edn
file in the root of the Clojure project.
User configuration locations If XDG_CONFIG_HOME
environment variable is set, then the user configuration is $XDG_CONFIG_HOME/clojure/deps.edn
Otherwide the user configuration is $HOME/.clojure/deps.edn
.
The CLJ_CONFIG
environment variable will be used if set.
"},{"location":"clojure-cli/defining-aliases/#alias-keys","title":"Alias keys","text":"An alias name is a keyword in Clojure, e.g. :test/env
, so the :
is an intrinsic part of the alias name.
Keys used to define an alias are:
:extra-paths
- a vector of directory names added to the project class path, e.g. [\"env/dev\" \"env/test\"]
:extra-deps
- a map of additional library dependencies, as a Maven library, Git repository or local directory {domain/library-name {:mvn/version \"1.2.33\"}}
maven library {domain/name {:git/url \"https://github.com/account-name/repository-name\" :git/sha 'ab3de67'}}
{io.github.account/repository-name {:git/tag \"2023-01-10\" :git/sha 'ab3de67'}}
{}
:main-opts
- a vector of command line options passed to clojure.main
:exec-fn
- the fully qualified name of a function to be run by clojure.exec
:exec-args
- default arguments passed to the function, over-ridden by matching argument keys specified on the command line
Keys used when defining an alias for a standalone tool which exclude the paths and dependencies defined in top-level keys.
:replace-paths
- use only the paths specified as the class path :replace-deps
- use only the libraries specified, defined as a Maven library or Git repository
alias :paths and :deps short-cuts Using :paths
and :deps
keys in an alias are short-cuts for their respective replace-paths
and :replace-deps
keywords
Using :paths
and :deps
in an alias can be very confusing and Practialli recommends using the explicit names for greater clarity
Clojure CLI -T option -T
execution option will exclude the top-level :paths
and :deps
keys
-T
sets \".\" as the path, adding only paths and libraries defined in aliases used with the execution flag
"},{"location":"clojure-cli/defining-aliases/#clojuremain-alias","title":"clojure.main alias","text":":main-opts
specifies the options passed to a clojure.main alias, using the clojure -M
execution option flag.
The value is a vector containing individual string values that represent each option, i.e. option flag and value.
-m
is used to define the fully qualified namespace in which clojure.main
should look for the -main
function.
The :main-opts
vector defines arguments that are passed to the -main
function, the same kind of arguments that would be passed via the command line.
The \"--middleware\"
argument adds cider-nrepl middleware to the nREPL server, allowing Cider and other editors complete control over the REPL. The syntax uses values wrapped in a vector.
The \"-interactive\"
argument runs an interactive REPL prompt. A headless process is run without this option.
:repl/cider\n {:extra-deps {nrepl/nrepl {:mvn/version \"0.9.0\"}\n cider/cider-nrepl {:mvn/version \"0.27.4\"}}\n :main-opts [\"-m\" \"nrepl.cmdline\"\n \"--middleware\" \"[cider.nrepl/cider-middleware]\"\n \"-interactive\"]}\n
This alias is called using the command clojure -M:repl/cider
"},{"location":"clojure-cli/defining-aliases/#clojureexec-alias","title":"clojure.exec alias","text":":exec-fn
specifies the fully qualified name of the function, using the clojure -X
execution option flag .
:exec-args
specifies a hash-map of default key/value pairs passed to the :exec-fn
function. The defaults can be overridden on the command line with respective key/value pairs.
Arguments can be nested within the :exec-args
map, especially useful on projects with several component configurations (server, database, logging) and managed by a component system (i.e Integrant)
{:aliases\n {:project/run\n {:exec-fn practicalli.service/start\n :exec-args {:http/server {:port 8080\n :join? fale}\n :log/mulog :elastic-search}}}}\n
To run with the default arguments:
clojure -X:project/run\n
Over-ride the default arguments by passing them on the command line
clojure -X:project/run '[:http/server :port]' 8888 :log/mulog :console :profile :dev\n
In this command the vector defines the path to the :port
key and over-rides the default value. :log/mulog is a top-level key which changes the log publisher type. :profile
is another top-level key that sets the environment to :dev
(e.g. to configure Integrant / Aero).
Arguments in a nested map within the alias can be traversed (as with get-in
and update-in
functions) to override the default values in the alias. So to set a different port value :
Argument keys should either be a top-level key or a vector of keys to refer to a key in a nested hash-map of arguments.
An alias can contain configuration to run both clojure.main
and clojure.exec
(useful if steadily migrating users from -M to -X approach without breaking the user experience)
"},{"location":"clojure-cli/defining-aliases/#examples","title":"Examples","text":"Practicalli Clojure CLI Config provides a wide range of aliases Practicalli Clojure CLI Config is a configuration designed to work across all Clojure projects, containing unique and meaningful alias names for ease of understanding.
"},{"location":"clojure-cli/defining-aliases/#simple-project","title":"Simple Project","text":"A new Clojure project can be made by creating a deps.edn
file and respective src
& test
directory trees.
A project deps.edn
file typically contains :path
, :deps
and :aliases
sections, although deps.edn
could start with a simple {}
empty hash-map.
{:paths [\"src\" \"resources\"]\n\n :deps\n {org.clojure/clojure {:mvn/version \"1.11.1\"}}\n\n :aliases\n {:test/env {:extra-paths [\"test\"]}}}\n
The test
path and associated libraries are added as an alias as they are not required when packaging or running a Clojure application. :path
and :deps
keys are always included by default, :aliases
are optional and only included when specified with the clojure
command, e.g. clojure -M:test/env
"},{"location":"clojure-cli/defining-aliases/#clojuremain-tool","title":"clojure.main tool","text":"The Cognitect Lab test runner included the test
directory in the class path, so test code will be included when run with this alias.
The test runner dependency is pulled from a specific commit shared on GitHub (defined as a Git SHA).
The main namespace is set to that library and the -main
function is called when using this alias.
{:aliases\n\n :test/cognitect\n {:extra-paths [\"test\"]\n :extra-deps {com.cognitect/test-runner\n {:git/url \"https://github.com/cognitect-labs/test-runner.git\"\n :git/sha \"f7ef16dc3b8332b0d77bc0274578ad5270fbfedd\"}}\n :main-opts [\"-m\" \"cognitect.test-runner\"]}\n}\n
"},{"location":"clojure-cli/defining-aliases/#clojure-exec-tool","title":"Clojure Exec tool","text":"With Clojure CLI tools version 1.10.1.697 the -X
flag was introduced using aliases with Clojure exec.
The configuration should define a fully qualified function that runs the tool.
The function should take arguments as key/value pairs as with an Edn hash-map, rather than relying on positional arguments as strings.
In this example, :exec-fn
defines the fully qualified function name that will be called. :exec-args
is a hash-map of the default key values pairs that are passed to the function as an argument.
:project/new\n {:replace-deps {seancorfield/clj-new {:mvn/version \"1.1.23\"}}\n :main-opts [\"-m\" \"clj-new.create\"] ;; deprecated\n :exec-fn clj-new/create\n :exec-args {:template lib :name practicalli/playground}\n }\n
Default arguments can be over-ridden in the command, e.g. clojure -X:project/new :template app :name practicalli/simple-application
uses a different template
Additional arguments can be sent when running the command, e.g. clojure -X:project/new :template figwheel-main :name practicalli/landing-page :args '[\"--reagent\"]'
uses the figwheel-main
template, specifies a name and :args
arguments sent to
:ns-default
can also be used to qualify the function that will be executed in an alias. :ns-default
is especially useful when there are several functions that could be called from the specific namespace.
The command line can over-ride the :exec-fn
function configuration, allowing for a default configuration that can be easily over-ridden.
Example
:project/new\n {:replace-deps {seancorfield/clj-new {:mvn/version \"1.1.226\"}}\n :main-opts [\"-m\" \"clj-new.create\"] ;; deprecated\n :ns-default clj-new\n :exec-fn create\n :exec-args {:template lib :name practicalli/playground}\n }\n
Keyword naming
Alias names are a Clojure keyword, which can be qualified to provide context, e.g. :project/create
.
Aliases are composable (chained together) and their path and deps configurations merged:
clojure -M:task/path:task/deps:build/options\n
When multiple aliases contain a :main-opts
configurations they are not merged, the configuration in the last alias is used
"},{"location":"clojure-cli/defining-aliases/#resources","title":"Resources","text":"clj-exec: insideclojure.org clj-exec update: insideclojure.org Clojure CLI execution options Tips for designing aliases
"},{"location":"clojure-cli/design-journal/","title":"Design Journal","text":"A design journal captures with code and comments the decisions taken for a project, invaluable to anyone trying to get up to speed with a project.
"},{"location":"clojure-cli/design-journal/#using-a-design-journal-namespace","title":"Using a design-journal namespace","text":"A single namespace encourages the journal to flow as the design of the application flows. So onboarding onto a project is essentially reading and evaluating code from top to bottom.
"},{"location":"clojure-cli/design-journal/#using-comment-blocks","title":"Using comment blocks","text":"It is useful to include examples of how you expect key parts of the system to be called and the kind of arguments they receive. Placing these examples in a (comment ,,,) expression ensures they are not accidentally evaluated whilst showing the most important function calls in a particular namespace.
"},{"location":"clojure-cli/execution-options/","title":"Clojure CLI Execution options","text":"Execution options (-A
-M
-P
-T
-X
) define how aliases are used with the Clojure CLI. Aliases are included via one of these execution options and each option can affect how the alias is used.
Clojure CLI design evolution The first documented released used the -A
execution option to include aliases.
The design has evolved to provide specific execution options to run code via clojure.main (-M
) and clojure.exec (-X
).
In July 2021 the ability to run tools (-T
) independent from the Clojure project classpath was introduced.
"},{"location":"clojure-cli/execution-options/#quick-summary","title":"Quick summary","text":"-M
uses clojure.main
to call the -main
function of the specified namespace, passing string-based arguments.
-X
uses clojure.exec
to call a fully qualified function, passing arguments as key and value pairs
-T
runs a tool independent from project dependencies. Only the libraries in the alias are included in the Class Path. The path is defined as \".\"
by default.
-P
downloads library dependencies, including those from specified aliases
-A
in the specific case of running a basic terminal UI REPL with the clojure
command or clj
wrapper.
"},{"location":"clojure-cli/execution-options/#clojuremain","title":"clojure.main","text":"-M
flag instructs Clojure CLI tools to use clojure.main
to run Clojure code.
The --main
or -m
flag is an argument to clojure.main
which specifies the namespace to search for a -main
function.
clojure.main/main
function searches for a -main
function in the given namespace, e.g. --main pracicalli.gameboard.service
If the -main function is not found or the namespace is not specified, then the clojure
command will run a REPL session.
Run a project with the main namespace practicalli.sudoku-solver
, without any additional aliases on the command line
clojure -M -m practicalli.sudoku-solver\n
Add :project/run
alias to the project deps.edn
file to provide a simpler way to run the project on the command line
:project/run {:main-opts [\"--main\" \"practicalli.sudoku-solver\"]}\n
Now the project code can be run using the simple command line form
clojure -M:project/run\n
"},{"location":"clojure-cli/execution-options/#using-clojuremain","title":"Using clojure.main","text":"clojure.main
namespace has been the way Clojure code was run (including a REPL) for most of its history. This is now evolving with the addition of clojure.exec. clojure.main has other features, as covered in the REPL and main entrypoints article) on clojure.org.
"},{"location":"clojure-cli/execution-options/#rebel-rich-terminal-ui","title":"Rebel rich terminal UI","text":"Rebel readline provides a terminal UI REPL, providing auto-completion, function signatures, documentation, etc.
:repl/rebel
is an alias that includes nrepl, cider-nrepl and rebel-readline libraries, with a :main-opts
to run the rebel-readline.main/-main
function via clojure.main
.
:repl/rebel\n{:extra-deps {nrepl/nrepl {:mvn/version \"0.9.0\"}\n cider/cider-nrepl {:mvn/version \"0.28.2\"}\n com.bhauman/rebel-readline {:mvn/version \"0.1.4\"}}\n :main-opts [\"-m\" \"nrepl.cmdline\"\n \"--middleware\" \"[cider.nrepl/cider-middleware]\"\n \"--interactive\"\n \"-f\" \"rebel-readline.main/-main\"]}\n
Use the :repl/rebel
alias with the -M
execution option
clojure -M:repl/rebel\n
Multiple aliases can be specified to include additional paths and libraries. Aliases chained together have their configuration merged
:env/dev
adds \"dev\" as an extra path, with the dev/user.clj
file automatically loading its code into the user
namespace when the REPL starts
:lib/hotload
alias adds the org.clojure/tools.deps.alpha
library to provide hotloading of dependencies into the running REPL
Start a REPL process with this alias
clojure -M:env/dev:lib/hotload:repl/rebel\n
The Rebel REPL UI will start, include the dev directory on the class path and the org.clojure/tools.deps.alpha
library loaded into the REPL
"},{"location":"clojure-cli/execution-options/#chaining-aliases","title":"Chaining aliases","text":"Alises can be used together by chaining their names on the command line
clojure -M:env/dev:lib/hotload:repl/rebel\n
The clojure
command will merge the :extra-paths
and :extra-deps
values from each alias in the chain.
The :main-opts
values from the aliases are not merged. Only the :main-opts
value from the last alias in the chain is used with clojure.main
to run the Clojure code.
If the command line includes the -m
flag with a namespace, then that namespace is passed to clojure.main
, ignoring all :main-opts
values from the aliases. The -i
and -e
flags for clojure.main also replace :main-opts
values.
"},{"location":"clojure-cli/execution-options/#clojureexec","title":"clojure.exec","text":"-X
flag provides the flexibility to call any fully qualified function, so Clojure code is no longer tied to -main
Any function on the class path can be called and is passed a hash-map as an argument. The argument hash-map is either specified in an alias using :exec-args
or assembled into a hash-map from key/value pairs on the command line. Key/values from the command line are merged into the :exec-args
map if it exists, with the command line key/values taking precedence.
"},{"location":"clojure-cli/execution-options/#clojureexec-arguments","title":"clojure.exec arguments","text":"Clojure.exec command takes key value pairs read as EDN values (extensible data notation that is the base syntax of Clojure).
Number values and keywords can be parsed from the command line
Arguments that are vectors and hash maps should be wrapped in single quotes to avoid the command line shell splitting arguments at spaces, e.g. '[:a :b]'
, '{:c 1}'
.
The double quotes in an EDN string must be wrapped by single quotes, along with vectors and hash-maps
'\"strings in double quotes surround by single quotes\"'
'[:vectors :with-single-quotes]'
'{:hash-maps :with-single-quotes}'
"},{"location":"clojure-cli/execution-options/#clojureexec-examples","title":"clojure.exec examples","text":"Call the status
function from the namespace practicalli.service
, which is on the classpath in the practicalli.service project
clojure -X practicalli.service/status\n
Pass arguments to a start
function in the practicalli.service
namespace
clojure -X practicalli.service/start :port 8080 :join? false\n
As the arguments are key/value pairs, it does not matter in which order the pairs are used in the command line.
"},{"location":"clojure-cli/execution-options/#built-in-functions","title":"Built in functions","text":"Clojure CLI tools has some built in tools under the special :deps
alias (not to be confused with the :deps
configuration in a deps.edn
file)
-X:deps mvn-install
- install a maven jar to the local repository cache -X:deps find-versions
- Find available versions of a library -X:deps prep
- prepare source code libraries in the dependency tree
See clojure --help
for an overview or man clojure
for detailed descriptions
"},{"location":"clojure-cli/execution-options/#run-a-tool","title":"Run a Tool","text":"-T
install, run and remove a tool, by the tool name or an alias.
The -T
execution option also uses the clojure.exec
approach, although the :deps
and :path
values from a project deps.edn
file are ignored. This isolates the tool from the dependencies in a Clojure project.
Calling Tools on the command line has the general form:
clojure -Ttool-name function-name :key \"value\" ,,,\n
A tool may provide many functions, so the specific function name is provided when calling the tool.
key/value pairs can be passed as arguments to that function (as with the -X execution option)
-Ttools
is a built-in tool to install
and remove
other tools, with the :as
directive providing a specific name for the tool.
In this example, the antq tool is installed using the name antq
clojure -Ttools install com.github.liquidz/antq '{:git/tag \"1.3.1\"}' :as antq\n
Installing a tool adds an EDN configuration file using the name of the tool in $XDG_HOME/.clojure/tools/
or $HOME/.clojure/tools/
directory.
Once a tool is installed, run by using the name of the tool.
clojure -Tantq outdated\n
Options to the tool are passed as key/value pairs (as the tool is called by clojure.exec)
clojure -Tantq outdated :upgrade true\n
-Ttools remove
will remove the configuration of the tool of the given name
clojure -Ttools remove :tool antq\n
"},{"location":"clojure-cli/execution-options/#tools-install-or-aliases","title":"Tools install or aliases","text":"Tools can also be defined in an alias with :exec-fn
can be run via -T:alias-name
as they are both executed using clojure.exec
.
-X
execution option can emulate -T
behaviour when an alias uses :replace-paths
and :replace-deps
keys, instead of :extra-paths
and :extra-deps
, so project paths and dependencies are not included loaded by the alias.
Using an alias for a tool has the advantage allowing a use to define their preferred default arguments that are passed to the :exec-fn
, using the :exec-args
key.
Default arguments could be included in the deps.edn
of the installed tool itself, although this is controlled by the developer of that tool project.
The :search/outdated
alias defined in the practicalli/clojure-deps-edn
user level configuration is an example of a tool alias with default arguments
:search/outdated\n {:replace-paths [\".\"]\n :replace-deps {com.github.liquidz/antq {:mvn/version \"1.3.1\"}\n org.slf4j/slf4j-nop {:mvn/version \"1.7.32\"}}\n :main-opts [\"-m\" \"antq.core\"]\n :exec-fn antq.tool/outdated\n :exec-args {:directory [\".\"] ; default\n :exclude [\"com.cognitect/rebl\"\n \"org.openjfx/javafx-base\"\n \"org.openjfx/javafx-controls\"\n \"org.openjfx/javafx-fxml\"\n \"org.openjfx/javafx-swing\"\n \"org.openjfx/javafx-web\"]\n ;; :focus [\"com.github.liquidz/antq\"]\n :skip [\"boot\" \"leiningen\"]\n :reporter \"table\" ; json edn format\n :verbose false\n :upgrade false\n :force false}}\n
This alias is called using clojure -T:search/outdated
and is the same as calling clojure -Tantq outdated ,,, ,,,
with a long list of key value options that represent the arguments in the alias.
As the output is a table of results, the command output is typically pushed to a file: clojure -T:search/outdated > outdated-2021-12-24.txt
Example tools include
- liquidz/antq - search dependencies for newer library versions
- seancorfield/deps-new - create new projects using templates
- clojure-nvd - check dependencies against National Vunerability Database
"},{"location":"clojure-cli/execution-options/#prepare-dependencies","title":"Prepare dependencies","text":"-P
flag instructs the clojure
command to download all library dependencies to the local cache and then stop without executing a function call.
The -P
flag is often used with Continuous Integration workflows and to create pre-populated Container images, to avoid repeatedly downloading the same library jar files.
If used with just a project, then the Maven dependencies defined in the project deps.edn
file will be downloaded, if not already in the users local cache (~/.m2/repository/
).
If :git
or :local/root
dependencies are defined, the respective code will be downloaded and added to the classpath.
Prepare flag by itself download dependencies defined in the :deps
section of the deps.edn
file of the current project.
clojure -P\n
Including one or more aliases will preparing all the dependencies from every alias specified
clojure -P -M:env/dev:lib/hotload:repl/cider\n
-P
flag must be used before any subsequent arguments, i.e. before -M
, -X
, -T
As prepare is essentially a dry run, then the clojure
command does not call :main-opts
or :exec-fn
functions, even if they exist in an alias or on the command line.
-P
will warn if a project has dependencies that require building from source (i.e Java code) or resource file manipulation. If so then clojure -X:deps prep
will prepare these source based dependencies.
"},{"location":"clojure-cli/execution-options/#built-in-terminal-ui-repl","title":"Built-in terminal UI REPL","text":"-A
is stated as the official way to include an alias when running a REPL terminal UI clojure
or clj
.
Practicalli recommends using Rebel Readline which uses -M execution option, so -A execution option is rarely used by Practicalli.
The :env/dev
alias adds \"dev\" directory to the class path, typically used to add a user.clj
that will automatically load code from the user
namespace defined in that file.
clojure -A:env/dev\n
The alias definition is :env/dev {:extra-paths [\"dev\"]}
Aliases can be chained together and their configuration will be merged
:lib/hotload
adds a dependency to provide hotloading of other dependencies
:lib/hotload\n{:extra-deps {org.clojure/tools.deps.alpha\n {:git/url \"https://github.com/clojure/tools.deps.alpha\"\n :sha \"d77476f3d5f624249462e275ae62d26da89f320b\"}\n org.slf4j/slf4j-nop {:mvn/version \"1.7.32\"}}}\n
Start a REPL process with this alias
clojure -A:env/dev:lib/hotload\n
Use -M for alias definitions including :main-opts
Using an alias that contains a :main-opts
key with -A
will fail to run a REPL and print a warning to use -M
execution option The :main-opts
configuration for -A
execution option is deprecated (although currently works in 1.10.x). To run Clojure code via clojure.main
the -M
option should be with aliases that includes :main-opts
.
"},{"location":"clojure-cli/execution-options/#summary","title":"Summary","text":"There are many options when it comes to running Clojure CLI tools that are not covered here, however, this guide gives you the most common options used so far.
Practicalli recommends using the -X
execution option where possible, as arguments follow the data approach of Clojure design.
The -J
and :jvm-opts
are useful to configure the Java Virtual machine and deserve an article to themselves as there are many possible options.
The -T
tools is an exciting and evolving approach and it will be interesting to see how the Clojure community adopt this model.
See the Deps and CLI Reference Rationale for more details and description of these options.
"},{"location":"clojure-cli/execution-options/#references","title":"References","text":" - Inside Clojure - clj exec
- Inside Clojure - clj exec update
"},{"location":"clojure-cli/practicalli-config/","title":"Practicalli Clojure CLI Configuration","text":" Practicalli Clojure CLI Config
Practicalli Clojure CLI Config is a user configuration for Clojure CLI tools providing a range of community tools via meaningful aliases, supporting Clojure and ClojureScript development.
Alias names are designed with qualified keywords that provide context for the use of an alias (env
, inspect
, project
, repl
, search
test
). These keywords help with discovery and reduce cognitive load required to remember their purpose.
Commonly used arguments are included in many alias via :main-opts
or :exec-args
which can be overridden on the command line.
Minimum Clojure CLI Version - 1.10.3.1040 Clojure CLI version 1.10.3.1040 is the minimum version, although the latest available version is recommended.
Check the version of Clojure CLI currently installed via clojure --version
or clojure -Sdescribe
Remote Environments or Continuous Integration For remote environments or Continuous Integration services, include Practicalli Clojure CLI Config) in the environment build or copy specific aliases to the Clojure project deps.edn
configuration.
"},{"location":"clojure-cli/practicalli-config/#install","title":"Install","text":"Fork or clone Practicalli Clojure CLI Config GitHub repository, first removing the $XDG_CONFIG_HOME/clojure
or $HOME/.clojure
directory if they exist.
Check Clojure CLI configuration location Check the location of your Clojure configuration directory by running clojure -Sdescribe
and checking the :user-config
value.
Free Desktop XDG CONFIGClassic Config If XDG_CONFIG_HOME
environment variable is set, clone the repository to $XDG_CONFIG_HOME/clojure
git clone https://github.com/practicalli/clojure-deps-edn.git $XDG_CONFIG_HOME/clojure\n
Clojure CLI will look for its configuration in $HOME/.clojure
directory if $XDG_CONFIG_HOME
and CLJ_CONFIG
environment variables not set.
git clone https://github.com/practicalli/clojure-deps-edn.git $HOME/.clojure\n
"},{"location":"clojure-cli/practicalli-config/#community-tools","title":"Community Tools","text":"The Clojure configuration directory contains a deps.edn
file containing a substantial :aliases
section with a long list of aliases. These aliases are described in the README of the project.
All tools are provided via libraries and are only installed on first use. Unused aliases will therefore not install their libraries.
Aliases to start with
Start with the following aliases to keep things simple
clojure -T:project/create :name domain/project-name
to create a new clojure project
clojure -M:repl/reloaded
to run a fully loaded REPL and rich terminal UI (which can be connected to from Clojure editors)
clojure -X:test/watch
to run tests on file save (or :test/run
to manually run tests once)
Use Clojure tools.build to create jar and uberjar packages of the project.
"},{"location":"clojure-cli/practicalli-config/#repl-experience","title":"REPL experience","text":"Rebel REPL terminal UI provides a feature rich REPL prompt experience, far beyond the basic clj
command.
Command Description clojure -M:repl/rebel
Rebel terminal UI clojure -M:env/dev:repl/rebel
Rebel including deps & path from :env/dev
alias to configure REPL start clojure -M:repl/reloaded
Rebel with dev
& test
paths, library hotload, namespace reload, portal data inspector clojure -M:repl/rebel-cljs
Run a ClojureScript REPL using Rebel Readline :repl/help
in the REPL for help and available commands. :repl/quit
to close the REPL.
"},{"location":"clojure-cli/practicalli-config/#clojure-projects","title":"Clojure Projects","text":" - Create Clojure CLI specific projects using deps-new
- Create projects from deps, leiningen and boot templates with clj-new
Command Description clojure -T:project/create
library project called playground clojure -T:project/create :template app :name practialli/service
Clojure CLI project from app template clojure -T:project/new :template luminus :name practicalli/full-stack-app +http-kit +h2
Luminus project with given name and template options"},{"location":"clojure-cli/practicalli-config/#run-projects","title":"Run projects","text":"Run project with or without an alias:
clojure -M:alias -m domain.app-name\nclojure -M -m domain.app-name\n
The -M
flag is required even if an alias is not included in the running of the application. A warning will be displayed if the -M
option is missing.
In the project deps.edn file it could be useful to define an alias to run the project, specifying the main namespace, the function to run and optionally any default arguments that are passed to that function.
:project/run\n{:ns-default domain.main-namespace\n :exec-fn -main\n :exec-args {:port 8888}}\n
Then the project can be run using clojure -X:project/run
and arguments can optionally be included in this command line, to complement or replace any default arguments in exec-args
.
"},{"location":"clojure-cli/practicalli-config/#project-dependencies","title":"Project dependencies","text":"Command Description clojure -M:project/errors
detailed report of compilation errors for a project clojure -M:search/libraries library-name
fuzzy search Maven & Clojars clojure -M:search/libraries -F:merge library-name
fuzzy search Maven & Clojars and save to project deps.edn clojure -M:search/outdated
report newer versions for maven and git dependencies clojure -M:search/unused-vars
search and remove unused vars"},{"location":"clojure-cli/practicalli-config/#project-deployment","title":"Project Deployment","text":"Deploy a project archive file locally or to Clojars.org
Package projects into jars using tools.build
Clojure tools.build is the recommended way to create library jar files and application Uberjar files.
Command Description clojure -X:deps mvn-install project.jar
[NEW] deploy jar file to local maven repository, i.e. ~/.m2/repository
clojure -M:project/clojars project.jar
deploy jar file to Clojars clojure -M:project/clojars-signed project.jar
deploy signed jar file to Clojars Set Clojars username/token in CLOJARS_USERNAME
and CLOJARS_PASSWORD
environment variables.
Set fully qualified artifact-name and version in project pom.xml
file
Path to project.jar can also be set in alias to simplify the Clojure command.
clojure -X:deps mvn-install project.jar
for local deployment of jars is part of the 1.10.1.697 release of the Clojure CLI tools in September 2020.
"},{"location":"clojure-cli/practicalli-config/#java-sources","title":"Java Sources","text":"Include Java source on the classpath to look up Java Class and method definitions, e.g. cider-find-var
in Emacs Requires: Java sources installed locally (e.g. \"/usr/lib/jvm/openjdk-11/lib/src.zip\")
Use the aliases with either -M
or -X
flags on the Clojure command line.
"},{"location":"clojure-cli/practicalli-config/#format-tools","title":"Format tools","text":"Use formatting tools to support a consistent code style across all Clojure projects
Command Description clojure -M:format/cljstyle check / fix
Check or fix code style (cljstyle) clojure -M:format/cljfmt check / fix
Check or fix code style (cljfmt) clojure -M:format/zprint filename
Format file using zprint Include :lib/pprint-sorted
when starting a REPL to pretty print data with sorted keys and set values
"},{"location":"clojure-cli/practicalli-config/#databases-and-drivers","title":"Databases and drivers","text":"Databases and drivers, typically for development time inclusion such as embedded databases
:database/h2
- H2 embedded database library and next.jdbc lib/next.jdbc
- include the next.jdbc library
clojure -M:database/h2
- run a REPL with an embedded H2 database and next.jdbc libraries
https://cljdoc.org/d/seancorfield/next.jdbc/CURRENT/doc/getting-started#create--populate-a-database
Use the aliases with either -M
or -X
flags on the Clojure command line.
"},{"location":"clojure-cli/practicalli-config/#data-science","title":"Data Science","text":" lib/clerk
- Clerk Notebooks
"},{"location":"clojure-cli/practicalli-config/#visualizing-projects","title":"Visualizing projects","text":"Create Graphviz graphs of project and library dependencies
Morpheus creates grahps of project vars and their relationships
:graph/vars
- generate graph of vars in a project as a .dot file :graph/vars-png
- generate graph of vars in a project as a .png file using src
and test
paths :graph/vars-svg
- generate graph of vars in a project as a .svg file using src
and test
paths
Install Graphviz to generate PNG and SVG images. Or use the Edotor website to convert .dot files to PNG or SVG images and select different graph layout engines.
Vizns creates graphs of relationships between library dependencies and project namespaces
:graph/deps
:graph/deps-png
- generate a single deps-graph png image
Other options: - clojure -M:graph/deps navigate
# navigable folder of SVGs - clojure -M:graph/deps single
# deps-graph.dot file - clojure -M:graph/deps single -o deps-graph.png -f png
- clojure -M:graph/deps single -o deps-graph.svg -f svg
- clojure -M:graph/deps single --show
# View graph without saving
"},{"location":"clojure-cli/practicalli-config/#data-inspector","title":"Data Inspector","text":"Portal Navigate data in the form of edn, json and transit
Practicalli Clojure - data browsers section - portal
Command Description clojure -M:inspect/portal-cli
Clojure REPL with Portal dependency clojure -M:inspect/portal-web
ClojureScript web browser REPL with Portal dependency clojure -M:inspect/portal-node
ClojureScript node.js REPL with Portal dependency Using Portal once running
(require '[portal.api :as portal])
once the REPL starts. For inspect/portal-web
use (require '[portal.web :as portal])
instead
(portal/open)
to open the web based inspector window in a browser.
(portal/tap)
to add portal as a tap target (add-tap)
(tap> {:accounts [{:name \"jen\" :email \"jen@jen.com\"} {:name \"sara\" :email \"sara@sara.com\"}]})
to send data to the portal inspector window (or any other data you wish to send)
(portal/clear)
to clear all values from the portal inspector window.
(portal/close)
to close the inspector window.
"},{"location":"clojure-cli/practicalli-config/#clojure-specification","title":"Clojure Specification","text":"Clojure spec, generators and test.check
:lib/spec-test
- generative testing with Clojure test.check :lib/spec2
- experiment with the next version of Clojure spec - alpha: design may change
"},{"location":"clojure-cli/practicalli-config/#unit-testing-frameworks","title":"Unit Testing frameworks","text":"Unit test libraries and configuration. The Clojure standard library includes the clojure.test
namespace, so no alias is required.
:test/env
- add test
directory to classpath :lib/expectations
- clojure.test
with expectations :lib/expectations-classic
- expectations framework
Use expectations in a project clojure -M:test:expectations
or from the command line with a test runner, e.g. clojure -M:lib/expectations:test/runner
"},{"location":"clojure-cli/practicalli-config/#test-runners-and-test-coverage","title":"Test runners and Test Coverage","text":"Tools to run unit tests in a project which are defined under test
path.
Run clojure with the specific test runner alias: clojure -M:test-runner-alias
Command Description clojure -M:test/run
Kaocha test runner for Clojure clojure -M:test/watch
Kaocha: watch for changes clojure -M:test/cljs
Kaocha test runner for ClojureScript"},{"location":"clojure-cli/practicalli-config/#lint-tools","title":"Lint tools","text":"Static analysis tools to help maintain code quality and suggest Clojure idioms.
Command Description clojure -M:lint/clj-kondo
comprehensive and fast static analysis lint tool clojure -M:lint/eastwood
classic lint tool for Clojure clojure -M:lint/idiom
Suggest idiomatic Clojure code"},{"location":"clojure-cli/practicalli-config/#performance-testing","title":"Performance testing","text":":performance/benchmark
alias includes the Criterium library for performance testing of Clojure expressions.
Use the aliases with either -M
or -X
flags on the Clojure command line.
:dev/reloaded
and :repl/reloaded
both include criterium library
Start a REPL using the :repl/reloaded
alias, or by including the :performance/benchmark
in a Clojure command to start a REPL.
Repl Reloaded ```shell clojure -M:repl/reloaded
```
Clojure command ```shell clojure -M:performance/benchmark:repl/basic
```
REPL Require the Criterium quick-bench
function
(require '[criterium.core :refer [quick-bench]])\n
(quick-bench (adhoc-expression))\n
Performance test a project in the REPL
clojure -M:performance/benchmark:repl/rebel\n\n(require '[practicalli/namespace-name]) ; require project code\n(in-ns 'practicalli/namespace-name)\n(quick-bench (project-function args))\n
Use the aliases with either -M
or -X
flags on the Clojure command line.
In the REPL:
(require '[clj-memory-meter.core :as memory-meter])\n (memory-meter/measure (your-expression))\n
"},{"location":"clojure-cli/repl-reloaded/","title":"Practicalli REPL Reloaded Workflow","text":"An effective REPL workflow is central to Clojure development. Practicalli REPL Reloaded workflow provides a rich set of tools and minimises the need to restart the REPL
- custom REPL startup using
dev/user.clj
- continually run unit tests with Kaocha
- event log and publisher with mulog
- visualise & navigate evaluation data and logs with Portal
- hotload libraries without restarting the REPL with
clojure.repl.deps
(Clojure 1.12) - reload changed namespaces to manage large code refactor with
tools.namespace
- performance testing code expressions with time & Criterium
"},{"location":"clojure-cli/repl-reloaded/#start-the-repl","title":"Start the REPL","text":"Start a Clojure REPL with the :repl/reloaded
alias (or include :dev/reloaded
alias in an Editor jack-in command or other REPL startup command).
Aliases are defined in Practicalli Clojure CLI Config
Start a rich terminal UI repl and the REPL Reloaded tools
clojure -M:repl/reloaded\n
A Rebel rich terminal UI REPL prompt provides direct evaluation in the REPL (with autocomplete, documentation, signature hints and multi-line editing)
An nREPL server is started to allow connections from a range of Clojure editors.
Portal Inspector window opens and is connected to all evaluation results and Mulog events that occur.
Rebel REPL Teminal UI
Example Alias Definitions Start a REPL process with an nREPL server to connect Clojure editors. Providing a Rebel rich terminal UI with tools to hotload libraries, reload namespaces and run Portal data inspector. The alias also includes a path for custom REPL startup and a path to access unit test code, along with a test runner.
:repl/reloaded\n{:extra-paths [\"dev\" \"test\"]\n :extra-deps {nrepl/nrepl {:mvn/version \"1.0.0\"}\n cider/cider-nrepl {:mvn/version \"0.30.0\"}\n com.bhauman/rebel-readline {:mvn/version \"0.1.4\"}\n djblue/portal {:mvn/version \"0.35.1\"}\n org.clojure/tools.namespace {:mvn/version \"1.4.1\"}\n org.slf4j/slf4j-nop {:mvn/version \"2.0.6\"}\n com.brunobonacci/mulog {:mvn/version \"0.9.0\"}\n lambdaisland/kaocha {:mvn/version \"1.77.1236\"}\n org.clojure/test.check {:mvn/version \"1.1.1\"}\n ring/ring-mock {:mvn/version \"0.4.0\"}\n criterium/criterium {:mvn/version \"0.4.6\"}}\n :main-opts [\"-m\" \"nrepl.cmdline\"\n \"--middleware\" \"[cider.nrepl/cider-middleware,portal.nrepl/wrap-portal]\"\n \"--interactive\"\n \"-f\" \"rebel-readline.main/-main\"]}\n\n:dev/reloaded\n{:extra-paths [\"dev\" \"test\"]\n :extra-deps {djblue/portal {:mvn/version \"0.35.1\"}\n org.clojure/tools.namespace {:mvn/version \"1.4.1\"}\n org.slf4j/slf4j-nop {:mvn/version \"2.0.6\"}\n com.brunobonacci/mulog {:mvn/version \"0.9.0\"}\n lambdaisland/kaocha {:mvn/version \"1.77.1236\"}\n org.clojure/test.check {:mvn/version \"1.1.1\"}\n ring/ring-mock {:mvn/version \"0.4.0\"}\n criterium/criterium {:mvn/version \"0.4.6\"}}}\n
Include the :dev/reloaded
or :lib/hotload
aliases when starting the REPL with other aliases, using any of the available Clojure CLI execution options (-A
,-M
,-X
,-T
).
Alias example from Practicalli Clojure CLI Config
Clojure 1.11 Hotload Support To support Clojure 1.11.x, add an :lib/hotload
alias for the clojure.tools.deps.alpha.repl
library using the latest SHA commit from the add-libs3 branch of clojure.tools.deps.alpha
library as an extra dependency.
The add-libs
code is on a separate , so requires the SHA from the head of add-libs3 branch
:lib/hotload\n {:extra-deps {org.clojure/tools.deps.alpha\n {:git/url \"https://github.com/clojure/tools.deps.alpha\"\n :git/sha \"e4fb92eef724fa39e29b39cc2b1a850567d490dd\"}}}\n
Include the :dev/reloaded
or :lib/hotload
aliases when starting the REPL with other aliases, using any of the available Clojure CLI execution options (-A
,-M
,-X
,-T
). Alias example from Practicalli Clojure CLI Config
"},{"location":"clojure-cli/repl-reloaded/#custom-repl-startup","title":"Custom REPL startup","text":"A Clojure REPL starts in the user
namespace. When a user.clj
file is on the classpath its code is loaded (evaluated) into the REPL during startup.
Create a dev/user.clj
file with libraries and tools to support development and add the dev
directory to the classpath.
Create a custom REPL Startup with dev/user.clj
"},{"location":"clojure-cli/repl-reloaded/#reload-namespaces","title":"Reload namespaces","text":"As code and design evolves, expressions evaluated in the REPL may become stale especially when the names (symbols, vars) bound to function definitions are renamed or deleted from the source code. Rather than restart the REPL process and loose all the state, one or more namespaces can be refreshed.
Remove function definitions before renaming To minimise the need to reload namespaces, undefine function definitions (unbind their name to the function) before changing the names of the function.
Remove a symbol from the namespace, using *ns*
which is dynamically bound to the current namespace
(ns-unmap *ns* 'function-or-def-name)\n
Remove a specific namespace (any functions defined in the namespace are no longer accessible - illegalStateException - Attempting to call unbound function) (remove-ns 'practicalli.service.utils)\n
Remove an alias for a specific namespace (ns-unalias 'practicalli.service.utils 'utils)\n
Clojure editors may provide commands to undefine a function definition, e.g. Emacs CIDER includes cider-undef
to remove the current symbol via nREPL commands
clojure.tools.namespace.repl contains the refresh
function that compares source code files with the definitions in the REPL, removing and re-evaluating those namespaces containing changes.
refresh will manage loading of namespaces with respect to their dependencies, ensuring each namespace can be loaded without error.
Require the clojure.tools.namespace.repl refresh function
REPLProject (require '[clojure.tools.namespace.repl :refer [refresh]])\n
Use an ns form for the namespace (often added to a custom user
namespace)
(ns user\n (:require [clojure.tools.namespace.repl :refer [refresh]]))\n
Or in a rich comment expression
(comment\n (require '[clojure.tools.namespace.repl :refer [refresh]]))\n
Refresh the namespaces that have saved changes
(refresh)\n
A list of refreshed namespaces are printed. If there are errors in the Clojure code, then a namespace cannot be loaded and error messages are printed. Check the individual code expressions in the namespace to ensure they are correctly formed.
Reload namespaces with dev/user.clj
Handling Errors If an exception is thrown while loading a namespace, refresh stops and prints the namespace that caused the exception. (clojure.repl/pst) prints the rest of the stack trace
*e
is bound to the exeception so will print the exeception when evaluated
tools.namespace refactor - documentation can be misleading refresh
and other functions were moved to the clojure.tools.namespace.repl
namespace. The original clojure.tools.namespace
functions are deprecated, although the new clojure.tools.namespace.repl
namespace is not deprecated.
Clojure tools.namespace API reference Namespaces Reference - Clojure.org
"},{"location":"clojure-cli/repl-reloaded/#hotload-libraries","title":"Hotload Libraries","text":"clojure.repl.deps
provides functions to hotload libraries into a running REPL, avoiding the need to restart the REPL and loose state just to use a new library with the project.
add-lib
finds a library by name and adds it to the REPL add-libs
takes a hash-map of one or more library name and version key/value pairs and adds them to the REPL sync-deps
reads the project deps.edn
file and adds :deps
dependencies to the REPL that are not already loaded
Hotload functions are typically called from a rich comment block in a separate dev/user.clj
file to avoid being automatically loaded.
Once hot-loaded, a library namespace can be required as if the dependency had been added to the project configuration before the REPL started.
practicalli/clojure-webapp-hotload-libraries is an example project that uses REPL driven development and hot loading of libraries to build a very simple web server using http-kit and hiccup.
Hotload requires Clojure 1.12 & latest Clojure CLI Install the latest Clojure CLI version and use Clojure 1.12 onward to use the officially released hotload library.
add-libs
is an unofficial feature for Clojure 1.11.x and available only in the add-libs3 branch of the now deprecated clojure.tools.deps.alpha
library.
Hotload simple web server and build a page (comment\n ;; Require if not automatically loaded by the REPL tooling, ie. Rebel Readline\n #_(require '[clojure..deps.repl :refer [add-lib add-libs sync-deps]])\n\n ;; hotload the libraries required for the server\n (add-libs '{http-kit/http-kit {:mvn/version \"2.5.1\"}})\n\n (require '[org.httpkit.server :as app-server])\n\n ;; Discover which http-kit functions are available\n (ns-publics (find-ns 'org.httpkit.server))\n\n ;; Define an entry point for the application\n (defn welcome-page\n [request]\n {:status 200\n :body \"Welcome to the world of Clojure CLI hotloading\"\n :headers {}})\n\n ;; Start the application server\n (app-server/run-server #'welcome-page {:port (or (System/getenv \"PORT\") 8888)})\n\n ;; Visit http://localhost:8888/ to see the welcome-page\n\n ;; Hotload Hiccup to generate html for the welcome page\n (add-libs '{hiccup/hiccup {:mvn/version \"2.0.0-alpha2\"}})\n\n (require '[hiccup.core :as hiccup])\n (require '[hiccup.page :as hiccup-page])\n\n (defn page-template [content]\n (hiccup-page/html5\n {:lang \"en\"}\n [:head (hiccup-page/include-css \"https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css\")]\n [:body\n [:section {:class \"hero is-info\"}\n [:div {:class \"hero-body\"}\n [:div {:class \"container\"}\n [:h1 {:class \"title\"} (:title content) ]\n [:p {:class \"subtitle\"} (:sub-title content)]]]]]))\n\n ;; Check the page template returns HTML\n (page-template {:title \"Hotload Libraries in the REPL\"\n :sub-title \"REPL driven development enables experimentation with designs\"})\n\n\n ;; redefine the welcome page to call the page template\n (defn welcome-page\n [request]\n {:status 200\n :body (page-template {:title \"Hotload Libraries in the REPL\"\n :sub-title \"REPL driven development enables experimentation with designs\"})\n :headers {}})\n\n ) ; End of rich comment block\n
There are several approaches to hotload libraries, including via a terminal REPL UI or in a project with a Clojure editor:
- Rich terminal UI REPL
- Hotload in a Project
"},{"location":"clojure-cli/repl-reloaded/#unit-test-runner","title":"Unit test runner","text":"Unit tests are written with clojure.test
and reside in a parallel test
directory, creating matching _test.clj
files for each relevant clojure file under src
.
Test runners are highly configurable and can run a very specific set of tests, although Clojure is usually fast enough to run all the tests each time.
Run the tests in a project with the kaocha test runner by issuing the following command in the root of the project.
clojure -X:test/run\n
Or continually watch for changes and run kaocha test runner each time a file is saved (typically in a separate terminal)
clojure -X:test/watch\n
Test run will stop on the first failed test unless :fail-fast? false
is passed as an argument to either command.
Running Unit Tests in an Editor Emacs and Neovim can run the kaocha test runner if one of the :repl/reloaded
, :dev/reloaded
or :lib/kaocha
aliases are used to start the Clojure REPL.
Emacs, Neovim and VS Code Calva also have their own built-in test runners. Test and source code must be evaluated in the REPL for the editor test runners to discover this code. Editor test runners to not read code from the Clojure files in src
or test
directories.
Writing Unit Tests for Clojure Using Test Runners with Projects
"},{"location":"clojure-cli/repl-reloaded/#performance-tests","title":"Performance tests","text":"time
is a quick way to see if an expression is worth further performance investigation.
(time ,,,)
wrapped around an expression will print the duration that expression took to run. This provides a very rough indicator of the performance of code, although as it only runs once then results may vary and are easily affected by the environment (Java Virtual Machine, Operating System, other concurrent processes).
Criterium provides more realistic performance results which are less affected by the environment, providing a better indication of performance to inform design choices.
Criterium tools take a little longer to run in order to return more accurate and consistent performance results.
REPLProject Require the criterium library
(require '[criterium.core :as benchmark])\n
Require the criterium library via the ns expression
(ns user\n (:require [criterium.core :as benchmark]))\n
Or require the criterium library in a rich comment expression (comment\n (require '[criterium.core :as benchmark]))\n
Wrap the quick-bench
function around the expression to run performance testing upon
(benchmark/quick-bench ,,,)\n
The expression being tested will be called multiple times and the duration and average times will be printed.
Criterium automatically adjusts the benchmark run time according to the execution time of the measured expression. If the expression is fast, Criterium will run it plenty of times, but if a single iteration is quite slow, it will be executed fewer times
Use quick-bench rather than bench
The bench macro is claimed to be more accurate than quick-bench, but in practice, it runs for much longer and doesn't yield significantly different results in most cases
Criterium API Documentation Benchmark with Criterium article
"},{"location":"clojure-cli/repl-reloaded/#log-and-publish-events","title":"Log and publish events","text":"mulog is a micro-logging library that logs events and data extremely fast and provides a range of publishers for log analysis.
Use the mulog log
function to create events to capture useful information about the Clojure system operation. Publish the logs locally to a console or to a log analysis service such as zipkin or Grafana
REPLProject Require the mulog library
(require '[com.brunobonacci.mulog :as mulog])\n
Require the mulog library via the namespace form
(ns your-ns\n (:require [com.brunobonacci.mulog :as mulog]))\n
Optionally create an event global context, containing information that will be included in every event created
(mulog/set-global-context! {:service-name \"Practicalli GameBoard\", :version \"1.0.1\", :env \"dev\"})\n
Create events with an identity that contains key/value pairs of data that captures the desired information about the event.
(mulog/log ::system-started :version \"0.1.0\" :init-time 32)\n
Start a publisher to see all the events created. The publisher can be to the console or to log analysis tools like zipkin or Grafana
(mulog/start-publisher! {:type :console})\n
trace
provides accurate data around instrumented operations of a single system or over a distributed system. trace data can be used in Elasticsearch and real-time streaming system sudh as Apache Kafka.
trace will track the rate of a complex operation, including the outcome and latency, within the contextual information of that operation.
Consider a function that calls several external services
(defn product-status [product-id]\n (let [stock (http/get availability-service {:product-id product-id})\n pricing (http/get pricing-service {:product-id product-id})]))\n
Create a trace between the function calls
(mulog/trace ::product-status\n [:product-id product-id]\n (product-status product-id))\n
trace
starts a timer then calls (product-status product-id)
. Once the execution completes a log an event is created using log
and uses the global context. By including the product id in the trace call, information is captured about the specific product involved in the trace log.
;; {:mulog/event-name :practicalli.service/products,\n;; :mulog/timestamp 1587504242983,\n;; :mulog/trace-id #mulog/flake \"4VTF9QBbnef57vxVy-b4uKzh7dG7r7y4\",\n;; :mulog/root-trace #mulog/flake \"4VTF9QBbnef57vxVy-b4uKzh7dG7r7y4\",\n;; :mulog/duration 254402837,\n;; :mulog/namespace \"practicalli.service\",\n;; :mulog/outcome :ok,\n;; :app-name \"Practicalli GameBoard\",\n;; :env \"dev\",\n;; :version \"1.0.1\"}\n
"},{"location":"clojure-cli/repl-reloaded/#trace-function-calls","title":"Trace function calls","text":"clojure.tools.trace can trace values, functions and a whole namespace of functions.
Tracing a value will show how that value flows through the code
Tracing a function shows the arguments passed to the function each time it is called and the results. Tracing will identify forms that are failing and also show the results of the function call, helping spotting unwanted nil
arguments and parts of a function definition that is failing.
trace
values, optionally assigning a tag trace-vars
dynamically trace a given fully qualified function untrace-vars
- remove trace from a given fully qualified function trace-ns
dynamically trace all functions in the given namespace untrace-ns
remove trace from all functions in the given namespace
:repl/reloaded
and :dev/reloaded
include the clojure.tools.trace dependency, i.e. org.clojure/tools.trace {:mvn/version \"0.7.11\"}
tools.trace API Reference
REPLProject Require the clojure.tools.trace
library and refer the trace
and untrace
functions
(require '[clojure.tools.trace :as trace])\n
Require the clojure.tools.trace
library using the alias trace
(ns user\n (:require '[clojure.tools.trace :as trace]))\n
To trace a value returned from an expression, optionally adding a tag
(trace/trace \"increments\" (map inc [1 2 3 4 5]))\n;;=> TRACE increments: (2 3 4 5 6)\n;;=> (2 3 4 5 6)\n
Trace a function call and its return value
(deftrace random-function [namespace] (rand-nth (vals (ns-publics namespace))))\n
Call the function to see the output of trace
(random-function 'clojure.core)\n;;=> TRACE t1002: (random-function 'clojure.core)\n;;=> TRACE t1002: => #'clojure.core/iteration\n;;=> #'clojure.core/iteration\n
Trace functions can identify which form is failing
(trace/trace-vars practicalli.random-function/random-number)\n;;=> #'practicalli.random-function/random-number\n
Call the function that is being traced and see the error
(practicalli.random-function/random-number 42)\n;;=> TRACE t10951: (practicalli.random-function/random-number 42)\n;;=> Execution error (ArithmeticException) at practicalli.random-function/random-number (random_function.clj:17).\n;;=> Divide by zero\n
Dynamically trace all functions in the given name space
(trace-ns domain.namespace)\n
Or remove all function traces in a namespace
(untrace-ns domain.namespace)\n
Dynamically trace a given function
(trace-vars domain.namespace/function-name)\n ```\n\nRemove the trace on a given function\n\n```clojure\n(untrace-vars domain.namespace/function-name)\n
"},{"location":"clojure-cli/repl-startup/","title":"Configure REPL on Startup","text":"A Clojure REPL starts in the user
namespace and automatically loads common tools to support REPL based development.
When interacting with the REPL prompt directly, use require
expressions to include additional functions into the user
nameapace rather than use potentially complex commands to set the namespace.
Clojure REPL only starts in user namespace The Clojure REPL only guarantees startup in the user
namespace. There is no specific mechanism to start the REPL in any other namespace than user
.
Clojure CLI could use the general --eval
flag as a hack to set a different namespace with an in-ns
expression, although this may affect other tools and add complexity to the startup process.
Default REPL Tools The Clojure REPL automatically loads common tools to support the foundation of a REPL driven workflow:
clojure.repl namespace loads:
- apropos - function names fuzzy matching a given regex pattern
- dir - sorted list of public vars (functions) in a given namespace
- doc - doc-string of a give Clojure function / symbol
- find-doc - doc-string of matching functions, given a string or regex pattern
- source - source code of a given function
- pst print stack trace, optionally setting depth
clojure.java.javadoc loads javadoc to show the doc-string of Java methods
clojure.pprint namepace loads pp & pprint to return pretty printed (human friendly format) evaluation results
"},{"location":"clojure-cli/repl-startup/#custom-user-namespace","title":"Custom user namespace","text":"Add a custom user
namespace to further enhance the Clojure REPL workflow:
- load code into the REPL by requiring namespaces
- call functions to start services that support development, e.g. logging publisher, print REPL command help menu
- launch development tools - e.g. portal data inspector
- start components (i.e for mount, component, integrant)
- hotload libraries into the REPL process without restart (Clojure 1.12 onward)
Create a project with custom user namespace
Projects created with Practicalli Project Templates contain a dev/user.clj
file for configuring the REPL at start up.
Practicalli custom user namespace supports the Practicalli REPL Reloaded workflow
Start the REPL with either the :dev/env
, :dev/reloaded
or :repl/reloaded
alias from Practicalli Clojure CLI Config to include dev
directory on the class path and automatically load dev/user.clj
code on REPL startup.
"},{"location":"clojure-cli/repl-startup/#define-user-namespace","title":"Define user namespace","text":"A custom user.clj
is typically placed in a dev
folder within the root of the project, with the dev
path defined in an alias to keep it separated from production code.
Create a dev/user.clj
file with a namespace called user
.
dev/user.clj(ns user)\n
Create an alias to include the dev
path when running a REPL process
Practicalli Clojure CLI ConfigManual Practicalli Clojure CLI Config includes aliases that add dev
directory to the class path
:dev/env
alias only adds the dev
directory to the classpath :dev/reloaded
adds library hotload, namespace reload, porta data inspector and testing libraries & test
:repl/reloaded
adds Rebel rich terminal UI to the tools provided by :dev/reloaded
Add an alias to the user deps.edn
configuration, i.e. $XDG_CONFIG_HOME/clojure/deps.edn
or $HOME/.clojure/deps.edn
Clojure User Config
:env/dev\n {:extra-paths [\"dev\"]}\n
Review Practicalli Clojure CLI Config for further alias examples. Run a Clojure REPL with the :repl/reloaded
alias (or :dev/reloaded
:dev/env
) to add the dev
directory to the class path and load the code in dev/user.clj
file into the REPL.
clojure -M:repl/reloaded\n
Keep user.clj
separate
The user.clj
code should not be included in live deployments, such as a jar or uberjar. Including the dev/
directory via an alias separates the user.clj
from deployment actions.
"},{"location":"clojure-cli/repl-startup/#requiring-namespaces","title":"Requiring namespaces","text":"Namespaces required in the user
ns form will also be loaded. If a required namespace also requires namespaces, they will also be loaded into the REPL during startup.
Functions (defn)
and data (def)
are immediately available.
Require namespace in user ns expression
Add a require expression to the namespace definition in dev/user.clj
dev/user.clj
(ns user\n (:require [practicalli.project-namespace]))\n
Requiring a large number of libraries may slow REPL start up time Require namespace in require expression
If the library is not always required, place a require
within a (comment ,,,)
expression to be evaluated by the developer any time after REPL startup. dev/user.clj
(ns user)\n\n(comment\n (require '[practicalli.project-namespace])\n#_())\n
"},{"location":"clojure-cli/repl-startup/#calling-functions","title":"Calling functions","text":"Use the fully qualified function name from the required namespace can be called, to start the application for example.
Example
dev/user.clj(ns user\n (:require [practicalli.project-namespace]))\n\n(practicalli.project-namespace/-main)\n
An alias can be used in the require expression, useful if multiple functions from a namespace are to be called
Example
dev/user.clj(ns user\n (:require [practicalli.service :as service]))\n\n(service/-main)\n
"},{"location":"clojure-cli/repl-startup/#repl-help-menu","title":"REPL Help menu","text":"Printing a menu of functions provided by the custom user namespace helps with the usability of a project.
Define a help
function that prints out commands with a breif explination of their purpose.
Add a (help)
expression to call the help function on REPL startup, displaying the help menu.
REPL Help menu
;; ---------------------------------------------------------\n;; Help\n\n(println \"---------------------------------------------------------\")\n(println \"Loading custom user namespace tools...\")\n(println \"---------------------------------------------------------\")\n\n(defn help\n []\n (println \"---------------------------------------------------------\")\n (println \"System components:\")\n (println \"(start) ; starts all components in system config\")\n (println \"(restart) ; read system config, reloads changed namespaces & restarts system\")\n (println \"(stop) ; shutdown all components in the system\")\n ;; (println \"(system) ; show configuration of the running system\")\n ;; (println \"(config) ; show system configuration\")\n (println)\n (println \"Hotload libraries: ; Clojure 1.12.x\")\n (println \"(add-lib 'library-name)\")\n (println \"(add-libs '{domain/library-name {:mvn/version \\\"v1.2.3\\\"}})\")\n (println \"(sync-deps) ; load dependencies from deps.edn\")\n (println \"- deps-* lsp snippets for adding library\")\n (println)\n (println)\n (println \"Portal Inspector:\")\n (println \"- portal started by default, listening to all evaluations\")\n (println \"(inspect/clear) ; clear all values in portal\")\n (println \"(remove-tap #'inspect/submit) ; stop sending to portal\")\n (println \"(inspect/close) ; close portal\")\n (println)\n (println \"(help) ; print help text\")\n (println \"---------------------------------------------------------\"))\n\n(help)\n\n;; End of Help\n;; ---------------------------------------------------------\n
"},{"location":"clojure-cli/repl-startup/#log-publisher","title":"Log publisher","text":"mulog is a very effective event log tool that also provides a range of log publishers. A custom user namespace can be used to start mulog log publishers to directly support the development workflow
- pretty print console output for easier to read event messages
- custom tap-publisher to send all log message to a
tap>
source, e.g. Portal data inspector
Mulog configuration and publishers
Require the mulog namespaces.
Set the global context for all mulog events, setting common key/value pairs that appear in every event created after the global context was evaluated.
Define a custom publisher to send all mulog events to the registered tap> sources, e.g. Portal data inspector.
dev/mulog_events.clj;; ---------------------------------------------------------\n;; Mulog Global Context and Custom Publisher\n;;\n;; - set event log global context\n;; - tap publisher for use with Portal and other tap sources\n;; - publish all mulog events to Portal tap source\n;; ---------------------------------------------------------\n\n(ns mulog-events\n (:require\n [com.brunobonacci.mulog :as mulog]\n [com.brunobonacci.mulog.buffer :as mulog-buffer]))\n\n;; ---------------------------------------------------------\n;; Set event global context\n;; - information added to every event for REPL workflow\n(mulog/set-global-context! {:app-name \"todo-basic Service\",\n :version \"0.1.0\", :env \"dev\"})\n;; ---------------------------------------------------------\n\n;; ---------------------------------------------------------\n;; Mulog event publishing\n\n(deftype TapPublisher\n [buffer transform]\n com.brunobonacci.mulog.publisher.PPublisher\n (agent-buffer [_] buffer)\n (publish-delay [_] 200)\n (publish [_ buffer]\n (doseq [item (transform (map second (mulog-buffer/items buffer)))]\n (tap> item))\n (mulog-buffer/clear buffer)))\n\n#_{:clj-kondo/ignore [:unused-private-var]}\n(defn ^:private tap-events\n [{:keys [transform] :as _config}]\n (TapPublisher. (mulog-buffer/agent-buffer 10000) (or transform identity)))\n\n(def tap-publisher\n \"Start mulog custom tap publisher to send all events to Portal\n and other tap sources\n `mulog-tap-publisher` to stop publisher\"\n (mulog/start-publisher!\n {:type :custom, :fqn-function \"mulog-events/tap-events\"}))\n\n#_{:clj-kondo/ignore [:unused-public-var]}\n(defn stop\n \"Stop mulog tap publisher to ensure multiple publishers are not started\n Recommended before using `(restart)` or evaluating the `user` namespace\"\n []\n tap-publisher)\n\n;; Example mulog event message\n;; (mulog/log ::dev-user-ns :message \"Example event message\" :ns (ns-publics *ns*))\n;; ---------------------------------------------------------\n
"},{"location":"clojure-cli/repl-startup/#reload-namespaces","title":"Reload Namespaces","text":"The REPL state can become 'stale' and contain vars (data and function names) that are no longer part of the source code, especially after a code refactor.
Rather than restart the repl, clojure.tools.namespace.repl provides functions that can clean the REPL state and reload changed namespaces from source code.
Clojure Namespace Tools - reload
Require the clojure.tools.namespace.repl
namespace to access the refresh
and set-refresh-dirs
functions to support reloading of source code into a clean REPL state.
dev/user.clj(ns user\n \"Tools for REPL Driven Development\"\n (:require\n [clojure.tools.namespace.repl :refer [set-refresh-dirs]]))\n
Use the set-refresh-dirs
function to define directories to reload when calling refresh
, effectively excluding dev
and other directories by not including their names as arguments.
dev/user.clj;; ---------------------------------------------------------\n;; Avoid reloading `dev` code\n;; - code in `dev` directory should be evaluated if changed to reload into repl\n(println\n \"Set REPL refresh directories to \"\n (set-refresh-dirs \"src\" \"resources\"))\n;; ---------------------------------------------------------\n
"},{"location":"clojure-cli/repl-startup/#hotload-libraries","title":"Hotload libraries","text":"Hotload is a way to add libraries to a running REPL process which were not include as a dependency during REPL startup.
Hotload libraries is SNAPSHOT feature - this guide will change when Clojure 1.12 is released Functions to hotload libraries are part of the Clojure 1.12 development releases and an official feature as of the stable 1.12 release.
For Clojure 1.11 and similar functions are available in the add-libs3 branch of the now deprecated clojure.tools.deps.alpha
library.
clojure/tools.deps is the official library for all released functions from the alpha library
This guide will be significantly rewritten once Clojure 1.12 is released.
Practicalli Clojure CLI ConfigManual :repl/reloaded
and dev/reloaded
aliases in Practicalli Clojure CLI Config provide the add-libs
function.
Edit the project deps.edn
configuration and add an :lib/hotload
alias for the clojure.tools.deps.alpha.repl
library. Or add an alias to the user level configuration for use with any Clojure CLI project.
The add-libs
code is on a separate add-libs3 branch, so requires the SHA from the head of add-libs3 branch
:lib/hotload\n {:extra-deps {org.clojure/tools.deps.alpha\n {:git/url \"https://github.com/clojure/tools.deps.alpha\"\n :git/sha \"e4fb92eef724fa39e29b39cc2b1a850567d490dd\"}}}\n
Alias example from Practicalli Clojure CLI Config
Start a REPL session using Clojure CLI with :repl/reloaded
, dev/reloaded
or :lib/hotload
aliases
clojure -M:repl/reloaded\n
Require and refer add-libs function
Require the clojure.tools.deps.alpha
library and refer the add-libs
function. The add-libs
function can then be called without having to use an alias or the fully qualified name.
(require '[clojure.tools.deps.alpha.repl :refer [add-libs]])\n
Hotload one or more libraries into the REPL using the add-lib
function, including the fully qualified name of the library and version string.
Hotload the hiccup library
The hiccup library converts clojure structures into html, where vectors represent the scope of keywords that represent html tags. Load the hiccup library using add-libs
(add-libs '{hiccup/hiccup {:mvn/version \"2.0.0-alpha2\"}})\n
Require the hiccup library so its functions are accessible from the current namespace in the REPL.
(require '[hiccup.core :as hiccup])\n
Enter an expression using the hiccup/html
function to convert a clojure data structure to html. (hiccup/html [:div {:class \"right-aligned\"}])\n
"},{"location":"clojure-cli/repl-startup/#system-components","title":"System Components","text":"Clojure has several library to manage the life-cycle of components that make up the Clojure system, especially those components with state. The order in which components are started and stopped can be defined to keep the system functioning correctly.
Components can include an http server, routing, persistence, logging publisher, etc.
Example system component management libraries included
- mount - manage system state in an atom
- integrant and Integrant REPL - data definition of system and init & halt defmethod interface
- donut system
- component
Require system namespace in user ns expression
Require the system namespace and use start
, restart
and stop
functions to manage the components in the system dev/user.clj
(ns user\n (:require [system]))\n\n(comment\n (system/start)\n (system/restart)\n (system/stop)\n )\n
Define code in the dev/system.clj
file which controls the component life-cycle services library for the project.
Create a dev/system.clj
to manage the components, optionally using one of the system component management libraries.
"},{"location":"clojure-cli/repl-startup/#life-cycle-functions","title":"life-cycle functions","text":"Start, stop and restart the components that a system is composed of, e.g. app server, database pool, log publisher, message queue, etc.
Atom restartMountIntegrant REPLDonut SystemComponent Clojure web services run ontop of an HTTP server, e.g. http-kit, Jetty.
A Clojure aton can be used to hold a reference to the HTTP server, allowing commands to stop that server.
Use clojure.tools.namespace.repl/refresh
when restarting the server (in between stop
and start
) to remove stale information in the REPL state.
Restart an HTTP server for Clojure Web Service & Refresh namespaces
dev/system_repl.clj;; ---------------------------------------------------------\n;; System REPL - Atom Restart \n;;\n;; Tools for REPl workflow with Aton reference to HTTP server \n;; https://practical.li/clojure-web-services/app-servers/simple-restart/\n;; ---------------------------------------------------------\n\n(ns system-repl\n (:require \n [clojure.tools.namespace.repl :refer [refresh]]\n [practicalli.todo-basic.service :as service]))\n\n;; ---------------------------------------------------------\n;; HTTP Server State\n\n(defonce http-server-instance \n (atom nil)) ; (1)! \n;; ---------------------------------------------------------\n\n;; ---------------------------------------------------------\n;; REPL workflow commands\n\n(defn stop\n \"Gracefully shutdown the server, waiting 100ms.\n Check if an http server isntance exists and \n send a `:timeout` key and time in milliseconds to shutdown the server.\n Reset the atom to nil to indicate no http server is running.\"\n []\n (when-not (nil? @http-server-instance)\n (@http-server-instance :timeout 100) ; (2)!\n (reset! http-server-instance nil) ; (3)!\n (println \"INFO: HTTP server shutting down...\")))\n\n(defn start\n \"Start the application server and run the application,\n saving a reference to the https server in the atom.\"\n [& port]\n (let [port (Integer/parseInt\n (or (first port)\n (System/getenv \"PORT\")\n \"8080\"))]\n (println \"INFO: Starting server on port:\" port)\n\n (reset! http-server-instance\n (service/http-server-start port)))) ; (4)!\n\n\n(defn restart\n \"Stop the http server, refresh changed namespace and start the http server again\"\n []\n (stop)\n (refresh) ; (5)! \n (start))\n;; ---------------------------------------------------------\n
-
A Clojure Aton holds a reference to the http server instance
-
Shut down http server instance without stopping the Clojure REPL
-
Reset the value in the atom to mil, indicating that no http server instance is running
-
Reset the value in the atom to a reference for the running http server. The reference is returned when starting the http server.
-
Refresh the REPL state and reload changed namespaces from source code using clojure.tools.namespace.repl/refresh
Define a dev.clj
file with go
, stop
and restart
functions that manage the life-cycle of mount components. A start
function contains the list of components with optional state.
Require the mount namespace and the main namespace for the project, which should contain all the code to start and stop services.
dev/user.clj(ns user\n :require [mount.core :refer [defstate]]\n [practicalli.app.main])\n
Define a start function to start all services
dev/user.clj(defn start []\n (with-logging-status)\n (mount/start #'practicalli.app.conf/environment\n #'practicalli.app.db/connection\n #'practicalli.app.www/business-app\n #'practicalli.app.service/nrepl))\n
The go
function calls start
and marks all components as ready.
dev/user.clj(defn go\n \"Start all states defined by defstate\"\n []\n (start)\n :ready)\n
The stop
function stops all components, removing all non-persistent state.
(defn stop [] (mount/stop))\n
The reset function that calls stop
, refreshes the namespaces so that stale definitions are removed and starts all components (loading in any new code).
dev/user.clj(defn reset\n \"Stop all states defined by defstate.\n Reload modified source files and restart all states\"\n []\n (stop)\n (namespace/refresh :after 'dev/go))\n
Example dev.clj file for mount
Use dev
namespace during development
Require practicalli.app.dev
namespace rather than main, to start components in a development environment.
Integrant REPL - Practicalli Clojure Web Services
User manager - Integrant
donut.system is a dependency injection library for Clojure and ClojureScript using system and component abstractions to organise and manage startup & shutdown behaviour.
Basic usage guide shows how to define a donut.system
seancorfield/usermanager-example is an example project that uses Component for lifecycle management
"},{"location":"clojure-cli/repl-startup/#reference","title":"Reference","text":" - Mount project on GitHub
- Mount - collection of Clojure/Script mount apps
- donut.system
- Component
- A tutorial to Stuart Sierra's Component
- Refactoring to Components - Walmart Labs Lacinia
- Integrant
- Compojure and Integrant
- Build a Clojure web app using Duct - CircleCI
- Reloading Woes - Lambda island
"},{"location":"clojure-cli/projects/","title":"Clojure projects","text":"Clojure CLI projects use a deps.edn
file to specifies source paths and libraries required for the project to run.
alias are defined in the deps.edn
file to support development tasks, providing additional libraries, paths and tools.
Generate a project from a template
Create a project from a template for a consistent project structure and include commonly used libraries.
Practicalli Project Templates create production grade projects providing a detailed starting point with configuration files for building and deploying the project.
"},{"location":"clojure-cli/projects/#create-minimal-project","title":"Create minimal project","text":"Create a deps.edn
file containing {}
in the root of a directory for a minimal configuration.
Create a src
directory as the root of the source code, and test
directory to contain unit test code.
Linux command to create a minimal clojure project Run these Linux commands in the root of a directory to create a minimal Clojure project structure.
touch deps.edn && echo '{}' > deps.edn && mkdir src test\n
The project can now be run with a REPL via a terminal UI or Clojure aware Editor.
Migrate project to Clojure CLI Guide to Migrating a project to Clojure CLI
"},{"location":"clojure-cli/projects/#project-structure","title":"Project Structure","text":"The essence of most Clojure CLI projects contains the following files and directories.
path purpose deps.edn core project configuration, paths, dependencies and aliases build.clj build specific configuration, create jars and uberjars src root directory of Clojure source files test root directory for Clojure test source files README.md Description of the project and how to develop / maintain it CHANGELOG.md Meaningful history of changes to the project organised by release .git Local git repository and configuration .gitignore Git ignore patterns for the project Example deps.edn configuration file
{:paths\n [\"src\" \"resources\"]\n\n :deps\n {org.clojure/clojure {:mvn/version \"1.11.1\"}}\n http-kit/http-kit {:mvn/version \"2.6.0\"} \n metosin/reitit {:mvn/version \"0.5.13\"}\n com.brunobonacci/mulog {:mvn/version \"0.9.0\"}\n\n :aliases\n {;; Clojure.main execution of application\n :run/service\n {:main-opts [\"-m\" \"practicalli.donuts.service\"]}\n\n ;; Clojure.exec execution of specified function\n :run/greet\n {:exec-fn practicalli.donuts.service/greet\n :exec-args {:name \"Clojure\"}}\n\n ;; Add libraries and paths to support additional test tools\n :test/env\n {}\n\n ;; Test runner - local and CI\n ;; call with :watch? true to start file watcher and re-run tests on saved changes\n :test/run\n {:extra-paths [\"test\"]\n :extra-deps {lambdaisland/kaocha {:mvn/version \"1.85.1342\"}}\n :main-opts [\"-m\" \"kaocha.runner\"]\n :exec-fn kaocha.runner/exec-fn\n :exec-args {:randomize? false\n :fail-fast? true}}\n\n ;; tools.build `build.clj` built script\n :build\n {:replace-paths [\".\"]\n :replace-deps {io.github.clojure/tools.build\n {:git/tag \"v0.9.4\" :git/sha \"76b78fe\"}}\n :ns-default build}}}\n
"},{"location":"clojure-cli/projects/add-libraries/","title":"Add libraries to a project","text":"The project deps.edn
file is used to add specific versions of libraries to a project.
The :deps
top-level key defines libraries that are always included, e.g when starting the REPL or packaging a project in an Uberjar.
Aliases are defined to include libraries only when the alias name is included, e.g. :dev/reloaded
alias includes several libraries only relevant during development of a Clojure project.
There are thousands of community Clojure and ClojureScript libraries available via clojars.org and Maven Central.
:deps
top level key contains a hash-map of dependencies, each dependency of the form domain/name {:mvn/version \"version-number\"}
Project deps.edn{:deps\n {org.clojure/clojure {:mvn/version \"1.11.1\"}\n hiccup/hiccup {:mvn/version \"2.0.0-alpha2\"}}}\n
Finding libraries Search for community libraries via the Clojars.org website or visit the Clojure Toolbox to browse some of the community libraries available
clojure -M:search/libraries pattern
where pattern is the name of the library to search for. Copy the relevant results into the project deps.edn
file.
clojure -M:search/libraries --format:merge pattern
will automatically add the library into the deps.edn
file.
clojure -X:deps find-versions :lib fully.qualified/library-name :n 5
returns the last 5 versions of the given library.
"},{"location":"clojure-cli/projects/add-libraries/#alias-libraries","title":"Alias libraries","text":":aliases
top-level key contains a hash-map of alias definitions.
Each alias has a unique name with :aliases
and is represented by a Clojure keyword associated with a Clojure hash-map, {}
:extra-deps
keyword is associated with hash-map that contains one or more fully qualified library names and the version of the library to use. The version of the library is defined with the maven form {:mvn/version \"0.4.2\"}
or Git form {:git/url \"https://github.com/clojure/tools.deps.alpha\" :git/sha \"e4fb92eef724fa39e29b39cc2b1a850567d490dd\"}
The following example can be added to a project deps.edn
, within the :aliases {}
form.
deps.edn alias definition with maven and git versions:dev/reloaded\n{:extra-deps {djblue/portal {:mvn/version \"0.34.2\"}\n lambdaisland/kaocha {:mvn/version \"1.71.1119\"}\n org.clojure/test.check {:mvn/version \"1.1.1\"}\n org.clojure/tools.namespace {:mvn/version \"1.3.0\"}\n org.clojure/tools.deps.alpha {:git/url \"https://github.com/clojure/tools.deps.alpha\"\n :git/sha \"e4fb92eef724fa39e29b39cc2b1a850567d490dd\"}}}\n
When the alias is included in the command to start the REPL, the libraries are placed on the class path and can be required for use.
clojure -M:dev/reloaded:repl/rebel\n
"},{"location":"clojure-cli/projects/add-libraries/#hotload-libraries","title":"Hotload libraries","text":"add-libs
is a function to load one or more libraries into a running REPL, negating the need to restart the REPL process.
Start a REPL process with clojure -M:repl/reloaded
to include the add-libs librar. Alternatively, include :dev/reloaded
or :lib/hotload
alias with any Clojure command to start a REPL.
Hotload Libraries explained
REPL Reloaded - Hotload Libraries details all the options for including the clojure.tools.deps.alpha library that contains the add-libs
function
Use a rich comment block or a dev/user.clj
file to require the clojure.tools.deps.alpha.repl
namespace and write add-libs
expressions to hot-load libraries.
A rich comment block ensures add-libs
code is only evaluated manually by a developer.
(comment\n (require '[clojure.tools.deps.alpha.repl :refer [add-libs]])\n (add-libs '{http-kit/http-kit {:mvn/version \"2.5.1\"}})\n)\n
rich-comment-hotload Clojure LSP snippet Snippets provided by Clojure LSP include rich-comment-hotload
, to add a rich comment block with a require for clojure.tools.deps.alpha
and an add-libs
expression, making it very quick to add this code.
deps-maven
and deps-git
snippets help ensure the correct syntax is used for the add-libs
expression for each library dependency to be added.
Practicalli Clojure LSP Config contains a wide range of snippets
"},{"location":"clojure-cli/projects/add-libraries/#hotload-example","title":"Hotload Example","text":"Create a web server from scratch, serving pages generated from hiccup, with all libraries hot-loaded as the code is being written. Demonstrates that it is possible to write an application when only starting the REPL once.
Web server from scratch (comment\n ;; run REPL with :lib/hotload alias\n (require '[clojure.tools.deps.alpha.repl :refer [add-libs]])\n\n ;; hotload the libraries required for the server\n (add-libs\n '{http-kit/http-kit {:mvn/version \"2.5.1\"}})\n ;; => (http-kit/http-kit)\n\n\n ;; Require the namespace from the http-kit library\n (require '[org.httpkit.server :as app-server])\n\n ;; Define a handler for http requests\n (defn welcome-page\n [request]\n {:status 200\n :body \"Welcome to the world of Clojure CLI hotloading\"\n :headers {}})\n\n ;; Start the application server with the handler\n (app-server/run-server #'welcome-page {:port (or (System/getenv \"PORT\") 8888)})\n\n ;; Visit http://localhost:8888/ to see the welcome-page\n\n ;; Hotload Hiccup to generate html for the welcome page\n (add-libs '{hiccup/hiccup {:mvn/version \"2.0.0-alpha2\"}})\n\n (require '[hiccup.core :as hiccup])\n (require '[hiccup.page :as hiccup-page])\n\n ;; Create a page template\n (defn page-template [content]\n (hiccup-page/html5\n {:lang \"en\"}\n [:head (hiccup-page/include-css \"https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css\")]\n [:body\n [:section {:class \"hero is-info\"}\n [:div {:class \"hero-body\"}\n [:div {:class \"container\"}\n [:h1 {:class \"title\"} (:title content) ]\n [:p {:class \"subtitle\"} (:sub-title content)]]]]]))\n\n ;; Check the page template returns HTML\n (page-template {:title \"Hotload Libraries in the REPL\"\n :sub-title \"REPL driven development enables experimentation with designs\"})\n\n\n ;; redefine the welcome page to call the page template\n (defn welcome-page\n [request]\n {:status 200\n :body (page-template {:title \"Hotload Libraries in the REPL\"\n :sub-title \"REPL driven development enables experimentation with designs\"})\n :headers {}})\n\n ;; Visit http://localhost:8888/ and refresh the page to see the new welcome-page\n )\n
"},{"location":"clojure-cli/projects/add-libraries/#excluding-dependencies","title":"Excluding dependencies","text":"Adding several libraries as dependencies to a project may cause conflicts. The :exclusions
key will prevent libraries within a library dependency from being included in the project
For example, library-a and library-b both have a dependency on library-c, as defined in the project configuration for library-a and library-b. When including library-a and library-b in the project as dependencies, there could be a conflict if the both libraries use a different version of library-c. Adding an exclude to library-a or library-b will stop library-c being included twice.
A Library that is self-contained and does not itself include any dependencies on any other libraries is unlikely to cause conflicts. Using these self-contained libraries simplifies the overall application design.
{:deps {:org.clojure/clojure {:mvn/version \"1.10.2\"}\n :cheshire/cheshire {:mvn/version \"5.10.0\"\n :exclusions \"com.fasterxml.jackson.core/jackson-core\"}}}\n
"},{"location":"clojure-cli/projects/hotload-in-project/","title":"Hotload libraries in Clojure Projects","text":"When starting a REPL process the dependencies listed in the project deps.edn
file are added to the class path. To add further dependencies the REPL has to be restarted to include new libraries added to the deps.edn
file.
Practicalli REPL Reloaded workflow allows new dependencies to be added to a running REPL process, negating the need to restart the REPL process which would loose the current REPL state.
"},{"location":"clojure-cli/projects/hotload-in-project/#hotload-repl","title":"Hotload REPL","text":"Start a REPL with an alias that includes the add-libs
library.
Terminal REPLClojure Editor Start a terminal REPL with the :repl/reloaded
alias and connect
clojure -M:repl/reloaded\n
Connect to the REPL process from a Clojure editor for an enhanced development experience. Run a Clojure REPL from the editor (jack-in command) configured with the :dev/reloaded
alias or :lib/hotload
alias in an Editor jack-in command or other REPL startup command.
Alternatively, run a Terminal REPL and connect the editor to that REPL process (connect command)
Practicalli REPL Reloaded Configuration
"},{"location":"clojure-cli/projects/hotload-in-project/#rich-comment-block","title":"Rich Comment Block","text":"Use a rich comment block or a dev/user.clj
file to require the clojure.tools.deps.alpha.repl
namespace and write add-libs
expressions to hot-load libraries.
A rich comment block ensures add-libs
code is only evaluated manually by a developer.
(comment\n (require '[clojure.tools.deps.alpha.repl :refer [add-libs]])\n (add-libs '{http-kit/http-kit {:mvn/version \"2.5.1\"}})\n)\n
Rich-comment-hotload Practicalli Clojure LSP Config includes the rich-comment-hotload
snippet which adds a rich comment block with a require for clojure.tools.deps.alpha
and an add-libs
expression, making it very quick to add this code.
deps-maven
and deps-git
snippets help ensure the correct syntax is used for the add-libs
expression for each library dependency to be added.
"},{"location":"clojure-cli/projects/hotload-in-project/#hotload-example","title":"Hotload Example","text":"Create a web server from scratch, serving pages generated from hiccup, with all libraries hot-loaded as the code is being written. Demonstrates that it is possible to write an application when only starting the REPL once.
(comment\n ;; run REPL with :lib/hotload alias\n (require '[clojure.tools.deps.alpha.repl :refer [add-libs]])\n\n ;; hotload the libraries required for the server\n (add-libs\n '{http-kit/http-kit {:mvn/version \"2.5.1\"}})\n ;; => (http-kit/http-kit)\n\n\n ;; Require the namespace from the http-kit library\n (require '[org.httpkit.server :as app-server])\n\n ;; Define a handler for http requests\n (defn welcome-page\n [request]\n {:status 200\n :body \"Welcome to the world of Clojure CLI hotloading\"\n :headers {}})\n\n ;; Start the application server with the handler\n (app-server/run-server #'welcome-page {:port (or (System/getenv \"PORT\") 8888)})\n\n ;; Visit http://localhost:8888/ to see the welcome-page\n\n ;; Hotload Hiccup to generate html for the welcome page\n (add-libs '{hiccup/hiccup {:mvn/version \"2.0.0-alpha2\"}})\n\n (require '[hiccup.core :as hiccup])\n (require '[hiccup.page :as hiccup-page])\n\n ;; Create a page template\n (defn page-template [content]\n (hiccup-page/html5\n {:lang \"en\"}\n [:head (hiccup-page/include-css \"https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css\")]\n [:body\n [:section {:class \"hero is-info\"}\n [:div {:class \"hero-body\"}\n [:div {:class \"container\"}\n [:h1 {:class \"title\"} (:title content) ]\n [:p {:class \"subtitle\"} (:sub-title content)]]]]]))\n\n ;; Check the page template returns HTML\n (page-template {:title \"Hotload Libraries in the REPL\"\n :sub-title \"REPL driven development enables experimentation with designs\"})\n\n\n ;; redefine the welcome page to call the page template\n (defn welcome-page\n [request]\n {:status 200\n :body (page-template {:title \"Hotload Libraries in the REPL\"\n :sub-title \"REPL driven development enables experimentation with designs\"})\n :headers {}})\n\n ;; Visit http://localhost:8888/ and refresh the page to see the new welcome-page\n )\n
Using add-libs with project deps.edn A project deps.edn
file can also be used to hotload libraries with add-lib
. This has the advantage that newly added libraries become part of the normal project dependency configuration.
Add a namespace definition to the deps.edn
file to help editors understand the deps.edn
file is being used for code. Use the #_
comment reader macro with the namespace definition to only evaluate this code manually as a developer.
Add the add-libs
expression after the :deps
key so that it is easy to slurp in the existing and new dependencies as a single hash-map. Use the comment reader macro #_
to only evaluate this code manually.
To hotload, remove the #_
temporarily and slurp in the hash-map of dependencies, placing a '
at the start of the hash-map. Add the name and version of libraries to hotload in the hash-map. Evaluate the add-libs
expression which should return a list of new namespaces added.
Once hotload has finished, barf the hash-maps of dependencies from the add-libs
expression, removing the '
. Add the #_
to the add-libs
expression and save the file.
The hotloaded libraries are now available by requiring their namespaces. If the REPL is restarted, the new dependencies will be included in the Classpath as they are now part of the project configuration.
;; ---------------------------------------\n;; Project Configuration with Hotload\n;; ---------------------------------------\n\n;; Hotload requires\n#_(ns deps.edn\n (:require [clojure.tools.deps.alpha.repl :refer [add-libs]]))\n\n;; Project configuration\n{:paths\n [\"src\" \"resources\"]\n\n :deps\n #_ (add-libs)\n {org.clojure/clojure {:mvn/version \"1.10.1\"}\n http-kit/http-kit {:mvn/version \"2.5.1\"}\n hiccup/hiccup {:mvn/version \"2.0.0-alpha2\"}}\n\n :aliases {}\n
"},{"location":"clojure-cli/projects/hotload-in-project/#live-coding-video","title":"Live Coding video","text":"See the REPL driven development video by Sean Corfield for this technique.
Jump to 23 minutes into the video to see this form of hotload in action.
"},{"location":"clojure-cli/projects/migrate-project/","title":"Migrating Project To Clojure CLI","text":"Migrating an existing project to Clojure CLI can be as simple as the addition of a deps.edn
configuration file.
Leiningen plugins that change code
A few Leiningen plugins inject code into a project to make it work. For example, lein-ring injects Clojure code into the project to run an application server. These type of plugins may require updates to the Clojure code in the project.
"},{"location":"clojure-cli/projects/migrate-project/#minimal-approach","title":"Minimal approach","text":"Create a deps.edn
file in the root of the project directory, containing an empty hash-map, {}
The Clojure version will be taken from the Clojure CLI tools install configuration.
This configuration is enough to run a terminal REPL UI for the project, although requiring namespaces from the project may require libraries to be added as dependencies first.
"},{"location":"clojure-cli/projects/migrate-project/#adding-dependencies","title":"Adding dependencies","text":"All Clojure projects require the org.clojure/clojure
library and a specific version is defined in the configuration that comes with the Clojure CLI install.
Use the :deps
key in deps.edn
to specify a version of the org.clojure/clojure
library, along with any dependencies required for the Clojure code to run.
{:deps\n {org.clojure/clojure {:mvn/version \"1.11.1\"}\n integrant/integrant {:mvn/version \"0.8.0\"}}}\n
REPL Reloaded - add-libs hotload dependencies Practicalli REPL Reloaded provides the add-libs function that can hotload libraries into the running REPL, without having to restart the REPL process.
The hotload approach can also be useful for diagnosing conflicts in dependencies by loading them in stages to narrow down the library causing the conflict.
"},{"location":"clojure-cli/projects/migrate-project/#adding-paths","title":"Adding paths","text":"It is advisable to specify the directory paths to define the location of the source code in the project, especially when running the project in other environments such as a continuous integration server.
Edit the deps.edn
file in the root of the project directory and add source directory and if relevant the resources directory.
{:paths\n [\"src\" `resource`]}\n
"},{"location":"clojure-cli/projects/migrate-project/#add-test-runner","title":"Add test runner","text":"Tests can be run locally using the :test/run
or :test/watch
aliases from the Practicalli Clojure CLI Config.
Continuous Integration Support A Continuous Integration server requires an alias in the project deps.edn
file to define a test runner.
A selection of test runners are provided via aliases defined in Practicalli Clojure CLI Config. Copy a test runner alias to the project deps.edn
file.
"},{"location":"clojure-cli/projects/migrate-project/#deployment","title":"Deployment","text":"A Continuous Delivery pipeline will require an alias in the project deps.edn
file to define how to build a jar or uberjar to package the Clojure project.
Project Package section details how to use tools.build
to create jar and uberjar archives of the project for deployment.
"},{"location":"clojure-cli/projects/migrate-project/#migration-tools","title":"Migration Tools","text":"Several tools exist to support migration from Leiningen projects to Clojure CLI projects. Results will be dependant on how complex the Leiningen project configuration is.
- lein-to-deps - create a
deps.edn
configuration from a project.clj
configuration - lein-tools-deps - share Clojure CLI dependencies with Leiningen project configuration.
"},{"location":"clojure-cli/projects/namespace/","title":"Namespaces","text":"Using namespaces makes code easier to work with by provide levels of abstraction that convey the overall design of the project. Clearly organized namespaces support a simple design approach for a project and make it easier to maintain.
A namespace is a logical separation of code, usually along features of the projects. Think of all namespaces as creating an API's within the project that communicate the architecture of the system.
"},{"location":"clojure-cli/projects/namespace/#controlling-scope","title":"Controlling scope","text":"Logically related data structures and functions are defined within a namespace, limiting their default scope to that namespace.
Namespaces should limit their interdependence on each other (limited number of required namespaces) to avoid a highly coupled design.
Within a namespace a var (def
, defn
) can be called by its short-form name. Outside of the namespace, a fully qualified name must be used, or required via an alias or directly referred.
Vars can be marked as private, def ^private name
, so they can be accessed only by functions in their own namespace (athough there are ways to by-pass that scope restiction).
"},{"location":"clojure-cli/projects/namespace/#including-another-namespace","title":"Including another namespace","text":"(ns namespace.name (:require [domain/filename :as purpose]))
is used to enable access to the functions & named data structures in another namespace than the current one. The included namespace is given an alias so its clear which code comes from that namespace.
Practicalli recommends using a meaningful alias that defines the purpose of the library you are including. This helps with the understanding and maintainability of the code, especially if you wish to refactor and replace the included library with an alternative. An alias name should be meaningful and you should avoid single character and cryptic aliases.
(ns my-namespace.core\n :require [clojure.java.io :as java-io])\n\n(defn read-the-file [filename]\n (line-seq (java-io/reader filename)))\n\n(read-the-file \"project.clj\")\n
Trying out a namespace
(require '[domain/filename])
can be used within you code if testing that namespace functions to see if they are useful to the project. Using a live linter such as clj-kondo, part of Clojure LSP, will highlight missing namespaces.
"},{"location":"clojure-cli/projects/namespace/#including-specific-parts-of-a-namespace","title":"Including specific parts of a namespace","text":":refer
in the require
expression includes one or more specific vars directly in the current namespace, as if it had been defined there. Referring a var means it no longer requires a namespace qualifier.
Use :refer
when the library being required the predominant focus of that namespace. A good example is clojure.test
which is included to specifically write unit tests.
(ns practicalli.gameboard.handler-test\n :require\n [clojure.test :refer [deftest is testing]]\n [practicalli.gameboard.handler :as handler])\n\n(deftest highscore-test\n (testing \"A description of the test\"\n (is (true? (handler/public-function 42)))))\n
(deftest public-function-in-namespace-test (testing \"A description of the test\" (is (= 1 (public-function arg))) (is (predicate-function? arg))))
Rarely used options - include exclude rename vars These other options on required functions are rarely used in practice. They tend to cause more issues than they solve, so use with care.
:exclude
will prevent a var from being used from a required namespace.
:only
will include only that var from the required namespace.
:rename
changes the name of the original function, if there conflicts
"},{"location":"clojure-cli/projects/namespace/#adding-multiple-namespaces","title":"Adding multiple namespaces","text":"The idiom in Clojure is to include multiple namespaces with just one :require
statement
Here is an example namespace expression with multiple require statements from the duct web framework template
(ns duct-test.main\n (:require [clojure.java.io :as io]\n [com.stuartsierra.component :as component]\n [duct.middleware.errors :refer [wrap-hide-errors]]\n [meta-merge.core :refer [meta-merge]]\n [duct-test.config :as config]\n [duct-test.system :refer [new-system]]))\n
Avoid use form - require should be used
The use
or :use
form is not recommended as it pulls in everything the namespace and everything that the included namespace also included. This can lead to conflicts, especially in larger projects.
As Clojure is typically composed of many libraries, its prudent to only include the specific things you need from another namespace.
"},{"location":"clojure-cli/projects/namespace/#design-refactor","title":"Design & Refactor","text":"When starting a new project all the code is typically in one namespace, unless you are using a template that creates multiple namespaces with sample code.
Practicalli recommends adding comment sections as the code is developed, grouping code by its purpose. As the namespace grows in size and complexity, these groups can be moved into their own namespaces as necessary. A code refactor is much simpler as the code is already grouped logically by purpose.
Code comment sections ;; --------------------------------------------------\n;; State\n\n;; --------------------------------------------------\n\n;; --------------------------------------------------\n;; Helper functions\n\n;; --------------------------------------------------\n\n;; --------------------------------------------------\n;; System / Lifecycle\n\n;; --------------------------------------------------\n
Clojure LSP Snippets
Practicalli Clojure LSP Config defines snippets to create sections within a Clojure file
comment-header
to describe the overall purpose of the namespace
comment-section
creates a start and end comment line and text comment
One pass evaluation
A Clojure file is evaluated from top to bottom, so var (def
, defn
) definitions should come before they are used in the code.
"},{"location":"clojure-cli/projects/rich-comments/","title":"Rich Comments","text":"The (comment ,,,)
form is commonly used to contain living experimental code, so it is often referred to as a rich comment as its purpose is more than just commenting out code.
"},{"location":"clojure-cli/projects/rich-comments/#experimental-design","title":"Experimental design","text":"Whilst iterating through designs, much experimental code can be created which is not (yet) ready to be part of the main namespace.
Experimental code can be written in a (comment ,,,)
form to keep it separate from more finalised implementations.
When a namespace is evaluted, code within the (comment ,,,)
form is not automatically loaded.
Most editors support evaluation of Clojure code within the (comment ,,,)
form, allowing a range of design implementations to be evaluated against each other.
Rich comment blocks are very useful for rapidly iterating over different design decisions by including the same function but with different implementations. Hide clj-kondo linter warnings for redefined vars (def
, defn
) when using this approach.
Practicalli Clojure LSP Config - rich-comment-hotload snippet
;; Rich comment block with redefined vars ignored\n#_{:clj-kondo/ignore [:redefined-var]}\n(comment\n\n (def data-model {:nested {:hash \"map\" :design \"choice\"}})\n (def data-model [{:collection \"of\" :hash \"maps\" :design \"choice\"}\n {:collection \"of\" :hash \"maps\" :design \"choice\"}])\n\n (defn value-added-tax []\n ;; algorithm - initial design)\n\n (defn value-added-tax []\n ;; algorithm - alternate design)\n\n ) ; End of rich comment block\n
"},{"location":"clojure-cli/projects/rich-comments/#design-journal","title":"Design Journal","text":"When the problem domain or libraries selected are relatively unknown, a significant amount of learning and experimentation may be required. This learning can be captured in a separate namespace, often referred to as a design journal.
Creating a journal of the decisions made as code is designed makes the project easier to understand and maintain. Journals avoid the need for long hand-over or painful developer on-boarding processes as the journey through design decisions are already documented.
A design journal can be added as a (comment ,,,)
section at the bottom of each namespace, or more typically in its own namespace.
A journal should cover the following aspects
- Relevant expressions use to test assumptions about design options.
- Examples of design choices not taken and discussions why (saves repeating the same design discussions)
- Expressions that can be evaluated to explain how a function or parts of a function work
The design journal can be used to create meaningful documentation for the project very easily and should prevent time spent on repeating the same conversations.
Practicalli example journal
Design journal for TicTacToe game using Reagent, ClojureScript and Scalable Vector Graphics
"},{"location":"clojure-cli/projects/rich-comments/#snippets","title":"Snippets","text":"clojure-lsp contains a number of snippets to create variations of a comment form.
rich-comment
a basic comment form rich-comment-rdd
comment form that informs clj-kondo to ignore duplicate function definitions, avoids warnings when testing multiple implementations of the same function rich-comment-hotload
- comment form with Clojure CLI library hotloading
"},{"location":"clojure-cli/projects/rich-comments/#migrating-design-to-tests","title":"Migrating design to tests","text":"REPL code experiements within rich comment blocks are often a good source of code that can be converted into formal unit tests.
Example values used to test functions as they are designed can be useful to create meaningful sets of test data, especially when testing edge conditions.
"},{"location":"clojure-cli/projects/rich-comments/#live-examples","title":"Live examples","text":"A rich comment at the end of a namespace can include code that demonstrates how to use the key aspects of the namespace API.
"},{"location":"clojure-cli/projects/package/","title":"Package with Clojure tools.build","text":"The Clojure.org tools.build project is used to build jar files to deploy libraries and uberjar files to run deployed projects (e.g. in Docker containers or directly on an Operating System with Java JVM installed).
Clojure tools.build is a library to define build related tasks using Clojure code.
Practicalli Project Templates includes tools.build configuration
Clojure projects created with Practicalli Project Templates include a build.clj
configuration to build an uberjar of the project.
The make build-jar
runs the clojure -T:build jar
command to build an uberjar.
Java ARchive - jar file A .jar
file is a zip archive of the project containing all the files for running a Clojure project. The archive should contain metatdata files such as Manifest and pom.xml and can contain Clojure sources or compiled class files from the project (or both).
An ubjerjar is .jar
file that also contains all the project dependencies including Clojure. The uberjar is a self-contained file that can be easily deployed and requires only a Java run-time (Java Virtual Machine), using the java -jar project-uberjar.jar
command, with the option to pass arguments to the Uberjar also.
Practicalli Project Build tasksClojure tools.build Practicalli Project templates include a build.clj
configuration with jar
and uberjar
tasks.
Create a runnable Clojure archive
clojure -T:project/build uberjar\n
Create a Clojure library archive
clojure -T:project/build jar\n
tools.build provides an API for pragmatically defining tasks to build Clojure projects.
Create a build.clj
configuration with tasks for building a library jar or runable uberjar.
Define build.clj configuration for tools.build
"},{"location":"clojure-cli/projects/package/tools-build/","title":"Package projects with tools.build","text":"Improved build script examples have been added Please report any issues using the new examples
Clojure.org tools.build is a library to define build related tasks using Clojure code.
The tools.build API provides a consistent interface to access the project configuration (project basis) and common tasks that facilitate building and packaging projects.
Include a build alias and build script in each project to make use of Clojure tools.build:
:build/task
alias adding tools.build library to the class path in the project deps.edn
file build.clj
defines a namespace requiring tools.build, a project configuration and functions as build tasks
Practicalli Project Templates include tools.build tasks Practicalli Project templates include a build.clj
tasks to generate a library jar
or a service uberjar
.
"},{"location":"clojure-cli/projects/package/tools-build/#define-build-alias","title":"Define build alias","text":"Add an alias to the project deps.edn
file which includes the org.clojure/tools.build
project.
:build/task alias created by Practicalli Project Templates
Project deps.edn ;; tools.build `build.clj` built script\n :build/task\n {:replace-paths [\".\"]\n :replace-deps {io.github.clojure/tools.build\n {:git/tag \"v0.9.6\" :git/sha \"8e78bcc\"}}\n :ns-default build}\n
Use Clojure CLI to run any of the tasks defined in the build
namespaces.
clojure -T:build/task task-name\n
tools.build release information Clojure.org tools.build release information shows the current values for git/tag
and :git/sha
Developing code in the build script :replace-paths [\".\"]
includes the build.clj
file on the class path to allow for REPL development of the build tasks
Include :build
alias in the Clojure command when starting the REPL.
clojure -M:build/task:repl/rebel\n
"},{"location":"clojure-cli/projects/package/tools-build/#build-script","title":"Build Script","text":"Create a build.clj
file which defines a namespace requiring tools.build, a project configuration and functions as build tasks
An Uberjar file is built to deploy a Clojure service, e.g. in test, staging or production environment.
A Jar file is built to published a Clojure library to a Maven repository, e.g. Clojars.org, Maven Central or a private Maven repository.
"},{"location":"clojure-cli/projects/package/tools-build/#namespace-definition","title":"Namespace definition","text":"Define the namespace and require the clojure.tools.build.api and any additional libraries.
Service UberjarLibrary Jar Namespace definition with tools.build.api and Pretty Print
build.clj(ns build\n (:require\n [clojure.tools.build.api :as build-api]\n [clojure.pprint :as pprint]))\n
Namespace definition with tools.build.api and Pretty Print
build.clj(ns build\n (:require\n [clojure.tools.build.api :as build-api]\n [deps-deploy.deps-deploy :as deploy-api]\n [clojure.pprint :as pprint]))\n
"},{"location":"clojure-cli/projects/package/tools-build/#build-configuration","title":"Build configuration","text":"Define a hash-map containing keys and values required to build the project.
Service UberjarLibrary Jar Define a project configuration for building an Uberjar file to run a service using the java -jar
command.
The Uberjar can be deployed to run the service in test, staging and production environments.
Clojure Service build tasks
build.clj;; ---------------------------------------------------------\n;; Project configuration\n\n(def project-config\n \"Project configuration to support build tasks\"\n {:class-directory \"target/classes\"\n :main-namespace 'practicalli/project-name/service\n :project-basis (build-api/create-basis)\n :uberjar-file \"target/practicalli-servicename-standalone.jar\"})\n\n(defn config\n \"Display build configuration\"\n [config]\n (pprint/pprint (or config project-config)))\n\n;; End of Build configuration\n;; ---------------------------------------------------------\n
Define a project configuration for building a jar file for deployment on Clojars and Maven Central, or a private repository.
pom-template
is the standard structure for generating a pom.xml file, required by Maven repositories, i.e. Clojars.org and Maven Central project-config
specific values for building the project, e.g. name, version, etc. config
function to pretty print the build configuration
Clojure Library build tasks
build.clj;; ---------------------------------------------------------\n;; Build configuration\n\n(defn- pom-template\n \"Standard structure for a `pom.xml` file, a Maven project configuration \n required to deploy libraries to Clojars.org, Maven Central or private Maven repositories\n https://maven.apache.org/guides/introduction/introduction-to-the-pom.html\"\n [project-version]\n [[:description \"FIXME: add purpose of library.\"]\n [:url \"https://github.com/organisation/project-name\"]\n [:licenses\n [:license\n [:name \"Creative Commons Attribution-ShareAlike 4.0 International\"]\n [:url \"https://creativecommons.org/licenses/by-sa/4.0/\"]]]\n [:developers\n [:developer\n [:name \"Organisation name\"]]]\n [:scm\n [:url \"https://github.com/organisation/project-name\"]\n [:connection \"scm:git:https://github.com/organisation/project-name.git\"]\n [:developerConnection \"scm:git:ssh:git@github.com:organisation/project-name.git\"]\n [:tag (str \"v\" project-version)]]])\n\n\n(def project-config\n \"Project configuration to support build tasks\"\n (let [library-name 'net.clojars.organisation/project-name\n version \"0.1.0-SNAPSHOT\"]\n {:library-name library-name\n :project-version version\n :jar-file (format \"target/%s-%s.jar\" (name library-name) version)\n :project-basis (build-api/create-basis)\n :class-directory \"target/classes\"\n :src-directory [\"src\"]\n :target-directory \"target\"\n :pom-config (pom-template version)}))\n\n\n(defn config\n \"Display build configuration\"\n [config]\n (pprint/pprint (or config project-config)))\n;; End of Build configuration\n;; ---------------------------------------------------------\n
"},{"location":"clojure-cli/projects/package/tools-build/#build-task","title":"Build Task","text":"Service UberjarLibrary Jar Define Clojure functions to run the required build tasks
clean
to remove build artefacts, e.g. target
directory Uberjar
creates a Jar file for a Clojure library, ready for publishing
Clojure Service build tasks
build.clj;; ---------------------------------------------------------\n;; Build tasks\n\n(defn clean\n \"Remove a directory\n - `:path '\\\"directory-name\\\"'` for a specific directory\n - `nil` (or no command line arguments) to delete `target` directory\n `target` is the default directory for build artefacts\n Checks that `.` and `/` directories are not deleted\"\n [directory]\n (when\n (not (contains? #{\".\" \"/\"} directory))\n (build-api/delete {:path (or (:path directory) \"target\")})))\n\n\n(defn uberjar\n \"Create an archive containing Clojure and the build of the project\n Merge command line configuration to the default project config\"\n [options]\n (let [config (merge project-config options)\n {:keys [class-directory main-namespace project-basis uberjar-file]} config]\n (clean \"target\")\n (build-api/copy-dir {:src-dirs [\"src\" \"resources\"]\n :target-dir class-directory})\n\n (build-api/compile-clj {:basis project-basis\n :class-dir class-directory\n :src-dirs [\"src\"]})\n\n (build-api/uber {:basis project-basis\n :class-dir class-directory\n :main main-namespace\n :uber-file uberjar-file})))\n\n;; End of Build tasks\n;; ---------------------------------------------------------\n
Define Clojure functions to run the required build tasks
clean
to remove build artefacts, e.g. target
directory jar
creates a Jar file for a Clojure library, ready for publishing install
a built jar into the local Maven repository, e.g. `~/.m2/repository/ publish
a built jar to Clojars.org
Clojure Library build tasks
build.clj;; ---------------------------------------------------------\n;; Build tasks\n\n(defn clean\n \"Remove a directory\n - `:path '\\\"directory-name\\\"'` for a specific directory\n - `nil` (or no command line arguments) to delete `target` directory\n `target` is the default directory for build artefacts\n Checks that `.` and `/` directories are not deleted\"\n [directory]\n (when (not (contains? #{\".\" \"/\"} directory))\n (build-api/delete {:path (or (:path directory) \"target\")})))\n\n(defn jar \"Run the CI pipeline of tests (and build the JAR).\"\n [config]\n (clean \"target\")\n (let [config (project-config config)\n class-directory (config :class-directory)]\n (println \"\\nWriting pom.xml...\")\n (build-api/write-pom (merge (pom-template config)))\n (println \"\\nCopying source...\")\n (build-api/copy-dir {:src-directory [\"resources\" \"src\"] :target-directory class-directory})\n (println \"\\nBuilding JAR...\" (:jar-file config))\n (build-api/jar config))\n config)\n\n(defn install\n \"Install a built JAR in the local Maven repository, e.g. `.m2/repository`\"\n [config]\n (let [config (project-config config)]\n (build-api/install config))\n config)\n\n(defn publish \n \"Publish the built JAR to Clojars.\" \n [config]\n (let [{:keys [jar-file] :as config} (project-config config)]\n (deploy-api/deploy\n {:installer :remote :artifact (build-api/resolve-path jar-file)\n :pom-file (build-api/pom-path (select-keys config [:library-name :class-directory]))}))\n config)\n\n;; End of Build tasks\n;; ---------------------------------------------------------\n
"},{"location":"clojure-cli/projects/package/tools-build/#resources","title":"Resources","text":" Clojure.org tools.build Guide
Clojure.org tools.build API Docs
Clojure.org tools.build release information
"},{"location":"clojure-cli/projects/templates/","title":"Creating projects from templates","text":"Creating projects using a template is a quick way to get started or create a common . A template will create the project structure, add libraries and even include example code.
deps-new provides Clojure CLI specific templates and Practicalli Project Templates provides production level templates with a REPL Reloaded workflow
deps-new built-in templates The deps-new built-in templates for creating a project - app
- simple project for a running application (uberjar) - lib
- simple project for a library (jar) - scratch
- a deps.edn
file and src/scratch.clj
- template
- project for defining a custom template
Practicalli Project Templates Practicalli Project Templates provide production level templates that include Practicalli REPL Reloaded Workflow tools, Docker & Compose configurations, Makefile tasks for a consistent command line UI and GitHub workflows to manage quality of code and configuration.
practicalli/minimal
- essential tools, libraries and example code practicalli/application
- general Clojure production level project template practicalli/service
- production level web services template with component management, Http-kit, Reitit and Swagger pracicalli/landing-page
- simple clojurescript website with bulma.io CSS and Figheel-main build tool.
clj-new provides Leiningen format templates The Practicalli :project/new
alias provides the seancorfield/clj-new tool which can use a wide range of templates (although some may only create Leinginen projects). This project has been archived and deps-new is the recommended approach.
Migrate to a Clojure CLI project if the template does not include a deps.edn
file
Clojure Projects with the REPL video demonstrates shows how to use clj-new
clj-new
can create projects from deps.edn
and Leiningen templates. A wide range of templates have been created by the Clojure community which can be found by searching on Clojars.org:
- clj-templates website - leiningen and boot templates
- deps.edn projects
- Leiningen projects
"},{"location":"clojure-cli/projects/templates/#add-deps-new","title":"Add deps-new","text":"Add deps-new via a Clojure CLI user alias or install as a tool.
Practicalli Clojure CLI ConfigAlias Definitions :project/create
alias provided by Practicalli Clojure CLI Config runs the seancorfield/deps-new tool to create Clojure CLI specific projects.
:project/create
alias includes the Practicall Project templates , extending the range of available templates
Create the following alias definitions in the Clojure CLI user configuration, e.g. $XDG_CONFIG_HOME/clojure/deps.edn
or $HOME/.clojure/deps.edn
Clojure CLI user deps.edn configuration - :aliases {}
:project/create\n{:replace-deps {io.github.seancorfield/deps-new\n {:git/tag \"v0.5.2\" :git/sha \"253f32a\"}\n io.github.practicalli/project-templates\n {:git/tag \"2023-08-02\" :git/sha \"eaa11fa\"}}\n :exec-fn org.corfield.new/create\n :exec-args {:template practicalli/minimal\n :name practicalli/playground}}\n
"},{"location":"clojure-cli/projects/templates/#create-a-project","title":"Create a project","text":"Open a terminal window and change to a suitable folder and create a project.
Create a project using the :project/create
alias.
The practicalli/minimal
template and practicalli/playground
name are used if :template
and :name
arguments are not specified.
clojure -T:project/create\n
The -T
execution option runs the tool with Clojure.exec which uses keywords to specify the options for creating the project.
Use the form domain/app-or-lib-name
to specify a project name, typically with a company name or Git Service account name as the domain
.
:template
can be one of the deps-new built-in templates (app
, lib
) or one of the Practicalli Project Templates.
Create a project using the practicalli/application
template and random-function name.
clojure -T:project/create :template practicalli/application :name practicalli/random-function\n
"},{"location":"clojure-cli/projects/templates/#run-project-in-a-repl","title":"Run Project in a REPL","text":"Change into the directory and test the project runs by starting a REPL with Terminal REPL
cd playground && clojure -M:repl/rebel\n
A repl prompt should appear.
Type code expressions at the repl prompt and press RETURN to evaluate them.
(+ 1 2 3 4 5)\n
Try the project with your preferred editor Using a Clojure aware editor, open the playground project and run the REPL. Then write code expressions in the editor and evaluate them to see the result instantly.
"},{"location":"clojure-cli/projects/templates/#running-the-project","title":"Running the project","text":"Run project with or without an alias:
clojure -M:alias -m domain.app-name\nclojure -M -m domain.app-name\n
In the project deps.edn
file it can be useful to define an alias to run the project, specifying the main namespace, the function to run and optionally any default arguments that are passed to that function.
:project/run\n{:ns-default domain.main-namespace\n :exec-fn -main\n :exec-args {:port 8888}}\n
Then the project can be run using clojure -X:project/run
and arguments can optionally be included in this command line, to complement or replace any default arguments in exec-args
.
"},{"location":"clojure-cli/projects/templates/design-templates/","title":"Design templates","text":"Create a custom template project for yourself, your team / organisation or an open source project
Either copy one of the Practicalli Project Templates or create a base template project
clojure -T:project/create :template template :name domain/template-name\n
Local only template If a template is only used by yourself locally, then all that is needed is a deps.edn
config with deps-new, resources/domain/template-name/
directory containing a template.edn
and files to make up the new project and optionally a src/domain/template-name.clj
for programmatic transform
"},{"location":"clojure-cli/projects/templates/design-templates/#add-project-files","title":"Add project files","text":"resources/domain/project_name/
directory contains files that are used to create a new project when using the template.
resources/domain/project_name/template.edn
defines the declarative copy rules that manage where files are copied too, allowing for renaming of files and directories.
"},{"location":"clojure-cli/projects/templates/design-templates/#deps-new-template-project","title":"deps-new template project","text":"deps-new specification defined with clojure.spec
(s/def ::root string?)\n(s/def ::description string?)\n(s/def ::data-fn symbol?)\n(s/def ::template-fn symbol?)\n(s/def ::files (s/map-of string? string?))\n(s/def ::open-close (s/tuple string? string?))\n(s/def ::opts #{:only :raw})\n(s/def ::dir-spec (s/cat :src string?\n :target (s/? string?)\n :files (s/? ::files)\n :delims (s/? ::open-close)\n :opts (s/* ::opts)))\n(s/def ::transform (s/coll-of ::dir-spec :min-count 1))\n(s/def ::template (s/keys :opt-un [::data-fn ::description ::root ::template-fn ::transform]))\n
"},{"location":"clojure-cli/projects/templates/design-templates/#use-template-locally","title":"Use template locally","text":"Create projects from the new template locally by defining a Clojure CLI user alias using :local/root that points to the root directory of the template project.
:project/create-local\n {:replace-deps {io.github.seancorfield/deps-new\n {:git/tag \"v0.5.2\" :git/sha \"253f32a\"}\n practicalli/project-templates\n {:local/root \"/home/practicalli/projects/practicalli/project-templates/\"}}\n :exec-fn org.corfield.new/create\n :exec-args {:template practicalli/minimal\n :name practicalli/playground}}\n
Create a new project with the project/create-local
alias
clojure -T:project/create-local :template domain/template-name\n
"},{"location":"clojure-cli/projects/templates/design-templates/#unit-tests","title":"Unit tests","text":"Each template should have a unit test that checks against the deps-new template specification (written in clojure.spec)
Once the unit test pass, create a new project from the template just created
Checks should be made of the following aspects of a new project created with the new template.
- check library dependency versions
- run main and exec functions
- run test runner
- test buld task clean and jar | uberjar
template.edn contains a declarative configuration of the project a template will generate
src/domain/template-name.clj
test/domain/template_name_test.clj
defines a unit test with clojure.test
and clojure.spec
which test the practicalli/template/service/template.edn
configuration.
"},{"location":"clojure-cli/projects/templates/design-templates/#template-specification","title":"Template specification","text":"The template configuration is tested against the org.corfield.new/template
specification
Specification defined with clojure.spec
(s/def ::root string?)\n(s/def ::description string?)\n(s/def ::data-fn symbol?)\n(s/def ::template-fn symbol?)\n(s/def ::files (s/map-of string? string?))\n(s/def ::open-close (s/tuple string? string?))\n(s/def ::opts #{:only :raw})\n(s/def ::dir-spec (s/cat :src string?\n :target (s/? string?)\n :files (s/? ::files)\n :delims (s/? ::open-close)\n :opts (s/* ::opts)))\n(s/def ::transform (s/coll-of ::dir-spec :min-count 1))\n(s/def ::template (s/keys :opt-un [::data-fn ::description ::root ::template-fn ::transform]))\n
"},{"location":"clojure-cli/projects/templates/design-templates/#publish-template","title":"Publish template","text":"Templates are a shared Git repository, so push the template project to GitHub
Include the shared repository within an alias definition within the Clojure CLI user deps.edn configuration, e.g. Practicalli Clojure CLI Config.
Create a new project with the template using the alias.
clojure -T:project/create :template domain/template-name :name domain/project-name\n
:project/crate includes Practicalli Project Templates
Practicalli Clojure CLI Config has been updated to include the practicalli/project-templates dependency, making available all the Practicalli templates.
Default values for template.edn keys can also be defined in the :exec-args {}
of an alias for the project template
:exec-args {:template practicalli/service\n :name practicalli.gameboard/service}\n
"},{"location":"clojure-cli/projects/templates/practicalli/","title":"Practicalli Project Templates","text":"Practicalli Project templates provides tools for a REPL Reloaded Workflow and several production grade project configurations.
:project/create
alias defined in Practicalli Clojure CLI Config provides` provides seancorfield/deps-new tool for creating projects, including the Practicalli Project Templates
clojure -T:project/create \n
"},{"location":"clojure-cli/projects/templates/practicalli/#available-templates","title":"Available Templates","text":"Use the :template
command line argument to specify a project template to generate the new Clojure project.
practicalli/minimal
- essential tools, libraries and example code practicalli/application
- general Clojure production level project template practicalli/service
- production level web services template with Http-kit, Reitit and Swagger. Optional : component
management with :donut
or :integrant
pracicalli/landing-page
- simple clojurescript website with bulma.io CSS and Figheel-main build tool.
Create service project with Donut System components
clojure -T:project/create :template practicalli/service :name practicalli/todo-list :component :donut\n
"},{"location":"clojure-cli/projects/templates/practicalli/#common-template-design","title":"Common Template Design","text":" practicalli/project-templates provide production level templates that include Practicalli tools, Docker & Compose configurations, Makefile tasks for a consistent command line UI and GitHub workflows to manage quality of code and configuration.
"},{"location":"clojure-cli/projects/templates/practicalli/#custom-user-namespace","title":"Custom user namespace","text":"Practicalli dev/user.clj
adds tools to the REPL on start up
mulog_events.clj
custom publisher sends log events to portal portal.clj
launch portal data inspector and set log global context system_repl.clj
Component services e.g. donut-party/system, integrant REPL user.clj
provides help for custom user namespace, loads portal, mulog and tools.namespace.repl to support reloading Clojure code
"},{"location":"clojure-cli/projects/templates/practicalli/#make-tasks","title":"Make tasks","text":"Makefile
defines targets used across Practicalli projects, following the make standard targets for users
all
calling all targets to prepare the application to be run. e.g. all: deps test-ci dist clean deps
download library dependencies (depend on deps.edn
file) dist
create a distribution tar file for this program or zip deployment package for AWS Lambda lint
run lint tools to check code quality - e.g MegaLinter which provides a wide range of tools format-check
report format and style issues for a specific programming language format-fix
update source code files if there are format and style issues for a specific programming language pre-commit
run unit tests and code quality targets before considering a Git commit repl
run an interactive run-time environment for the programming language test-unit
run all unit tests test-ci
test running in CI build (optionally focus on integration testing) clean
remove files created by any of the commands from other targets (i.e. ensure a clean build each time)
Practicalli Makefile also defines docker targets to build and compose images locally, inspect images and prune containers and images.
docker-build
build Clojure project and run with docker compose docker-build-clean
build Clojure project and run with docker compose, removing orphans docker-down
shut down containers in docker compose swagger-editor
start Swagger Editor in Docker swagger-editor-down
stop Swagger Editor in Docker
"},{"location":"clojure-cli/projects/templates/practicalli/#docker","title":"Docker","text":"Docker configuration builds and runs the Clojure project in a Docker container, orchestrating with other services including a Database.
The service and application project templates include the following files
Dockerfile
multi-stage build and run, with JVM optomisations for a Docker container .dockerignore
patterns to opomise copying of files to the docker build image compose.yaml
configuration for orchestrating additional services, e.g. postgresql database
"},{"location":"clojure-cli/projects/templates/practicalli/application/","title":"Practicalli Application template","text":"Create a general Clojure application with REPL Reloaded workflow.
"},{"location":"clojure-cli/projects/templates/practicalli/application/#using-the-project","title":"Using the project","text":"Run the REPL
make repl\n
The REPL prompt is displayed using Rebel for a rich UI experience.
Portal data inspector window is displayed and all evaluation results and mulog events are automatically sent to Portal.
An nREPL server is running in the background for connecting Clojure aware editors.
Run tests (stopping on first failing test)
make test\n
"},{"location":"clojure-cli/projects/templates/practicalli/landing-page/","title":"Practicalli Landing Page","text":"Build simple websites and landing pages using ClojureScript and figwheel-main.
clojure -T:project/create :template landing-page :name practicalli/website-name\n
"},{"location":"clojure-cli/projects/templates/practicalli/landing-page/#using-the-project","title":"Using the project","text":"Run the REPL
make repl\n
Run tests (stopping on first failing test)
make test\n
"},{"location":"clojure-cli/projects/templates/practicalli/landing-page/#template-design","title":"Template design","text":"Configuration files
deps.edn
project dependencies and aliases defining figwheel builds dev.cljs.edn
development build configuration live.cljs.edn
live build configuration (GitHub pages deployment by default) figwheel-main.edn
general figwheel configuration
Clojure code
src/project/landing-page.clj
compose components to render the website src/project/components.clj
functions that define component and associated helper functions src/project/data.clj
data structure passed in part or whole to each component, via the landing-page
.
project.data
namespace defines an example data structure as a static value (def). Use an atom to contain the data structure if the data should be updated by components in the project.
"},{"location":"clojure-cli/projects/templates/practicalli/minimal/","title":"Practicalli Minimal Project Template","text":"A Clojure project with minimal dependencies and example code, useful for experimenting or building a new project from a minimal setup.
"},{"location":"clojure-cli/projects/templates/practicalli/minimal/#using-the-project","title":"Using the project","text":"Run the REPL
make repl\n
The REPL prompt is displayed using Rebel for a rich UI experience.
Portal data inspector window is displayed and all evaluation results and mulog events are automatically sent to Portal.
An nREPL server is running in the background for connecting Clojure aware editors.
Run tests (stopping on first failing test)
make test\n
"},{"location":"clojure-cli/projects/templates/practicalli/service/","title":"Practicalli Service template","text":"Develop web services and APIs using the practicalli/service
template.
clojure -T:project/create :template practicalli/service\n
The practicalli/services
includes:
- http-kit provides an HTTP web server responding to HTTP requests
- reitit routing
- mulog event logging and publisher
- Portal data inspector
- Makefile with common Practicalli tasks
A component system can be included by providing the :component :donut
or :component integrant
command line arguments.
"},{"location":"clojure-cli/projects/templates/practicalli/service/#component-systems","title":"Component systems","text":"Components in the system can be managed during development by evaluating functions in the REPL.
(start)
starts all components in order (stop)
stops all components in order (restart)
reload changed namespaces and restart all components in order (system)
prints out the system configuration
The system-repl.clj
defines the functions to manage components, using the chosen component library, e.g. Donut system, Integrant REPL.
When running the application from the command line, the src/domain/project/service/-main
function calls the initialisation of components and creates a var called running-system
that contains the initialised system components. -main
contains a shutdown hook that responds to SIGTERM signals, triggering a shutdown of components in the running-system
.
Donut SystemIntegrant Include Donut system configuration and REPL functions
clojure -T:project/create :template practicalli/service :component :donut\n
src/domain/project/system.clj
defines the system components dev/system-repl.clj
defines funtions to manage the system components
Each component is defined within the system
namespace in the domain.project.system/main
hash-map. Each component definition has a start and stop function, optionally passing configuration options and environment variables for that component.
Include Integrant system configuration and Integrant REPL functions to support development
clojure -T:project/create :template practicalli/service :component :integrant\n
src/domain/project/system.clj
defines the system components dev/system-repl.clj
defines funtions to manage the system components
Each component is started with an init multi-method with a the specific component name (keyword). Each init
multi-method provides the specific Clojure code to start the component.
A halt
multi-method is provided for each component that requires shutting down, e.g. http server, database pool, logging publisher, etc.
During development and testing, the components are managed from the user
namespace by evaluating the (start)
, (stop) or (restart)
functions.
"},{"location":"clojure-cli/projects/templates/practicalli/service/#using-the-project","title":"Using the project","text":"Run the REPL
make repl\n
The REPL prompt is displayed using Rebel for a rich UI experience. Portal data inspector window is displayed and all evaluation results and mulog events are automatically sent to Portal.
An nREPL server is running in the background for connecting Clojure aware editors.
Run tests (stopping on first failing test)
make test\n
"},{"location":"clojure-cli/repl/","title":"Clojure REPL","text":"The REPL is the environment in which all Clojure code runs, whether that be during development, testing or in production systems.
A Terminal REPL provides a simple way to interact with the REPL, sending code expressions for evaluation and returning results.
Use a terminal REPL for
- quick experiments
- long running processes (e.g. http severs running Clojure)
- interact with the REPL state and manage components (e.g restarting system components, querying UI component state or services system state).
- a REPL process separate from a specific editor control
REPL connected Editor
A Clojure aware editor connected to the REPL is used for the majority of Clojure development. One or more expressions from a source code file can be sent to the REPL for evaluation, displaying the results inline.
"},{"location":"clojure-cli/repl/#rebel-terminal-repl-ui","title":"Rebel Terminal REPL UI","text":"Rebel is a REPL terminal UI that provides auto-completion, function call syntax help and documentation, themes and key binding styles to enhance the development experience. Clojure tools also include a REPL with a minimal interface by default.
"},{"location":"clojure-cli/repl/#install-rebel","title":"Install Rebel","text":"Practicalli Clojure CLI ConfigDefine Rebel AliasClojure CLI REPL :repl/rebel
alias is provided by Practicalli Clojure CLI Config to run rebel readline.
:repl/reloaded
alias runs Rebel with tools to support the Practicalli REPL Reloaded, providing a custom REPL startup with support for Portal data inspector and Mulog event logs.
Both aliases will start an nREPL server for Clojure aware editors to connect.
Rebel libraries are downloaded the first time the Rebel alias is used.
Add an alias called :repl/rebel
to the user deps.edn
configuration, e.g. ~/.config/clojure/deps.edn
Basic Rebel terminal UI alias
~/.config/clojure/deps.edn:repl/rebel \n{:extra-deps {com.bhauman/rebel-readline {:mvn/version \"0.1.5\"}}\n :main-opts [\"-m\" \"rebel-readline.main\"]}\n
Rebel terminal UI alias with nREPL for editor connection
~/.config/clojure/deps.edn:repl/rebel\n{:extra-deps {nrepl/nrepl {:mvn/version \"1.0.0\"}\n cider/cider-nrepl {:mvn/version \"0.31.0\"}\n com.bhauman/rebel-readline {:mvn/version \"0.1.4\"}}\n :main-opts [\"-e\" \"(apply require clojure.main/repl-requires)\"\n \"--main\" \"nrepl.cmdline\"\n \"--middleware\" \"[cider.nrepl/cider-middleware]\"\n \"--interactive\"\n \"-f\" \"rebel-readline.main/-main\"]}\n
Practicalli Clojure CLI Config contains aliases for a basic terminal UI and a headless (non-interactive) terminal UI, each starting an nREPL server for editor connection.
Alias definitions for a basic terminal UI REPL
Interactive client REPL with nREPL server for Clojure Editor support
:repl/basic\n{:extra-deps {nrepl/nrepl {:mvn/version \"1.0.0\"}\n cider/cider-nrepl {:mvn/version \"0.28.7\"}}\n :main-opts [\"-m\" \"nrepl.cmdline\"\n \"--middleware\" \"[cider.nrepl/cider-middleware]\"\n \"--interactive\"]}\n
Headless REPL with nREPL server for Clojure Editor support
:repl/headless\n{:extra-deps {nrepl/nrepl {:mvn/version \"1.0.0\"}\n cider/cider-nrepl {:mvn/version \"0.28.7\"}}\n :main-opts [\"-m\" \"nrepl.cmdline\"\n \"--middleware\" \"[cider.nrepl/cider-middleware]\"]}\n
To have a basic terminal UI REPL prompt use the :repl/basic
alias to start a REPL process with nREPL connection.
clj -M:repl/basic\n
To only have the REPL process without a REPL prompt, use the :repl/headless
aliase to start a REPL process with nREPL connection. This approach is useful to separate the REPL output from the editor whilst keeping all the interaction with the REPL via the editor.
clj -M:repl/headless\n
Terminal REPL and Editor Including an nREPL server when starting the REPL allows clojure ware editors to connect to the REPL process, providing a more effective way to write and extend Clojure code.
An external REPL can still be of use even when only evaluating code in a Clojure editor. Separating the REPL process from the editor process allows the editor to be closed, upgraded or swapped for a different editor without having to end the REPL session. Different editors could be connected to the same REPL to use particular features they provide.
A REPL process can be long running, staying alive for days, weeks or months when working on larger projects. Avoiding stop and start of the REPL maintains state in the REPL, maintaining the flow of the Clojure workflow.
"},{"location":"clojure-cli/repl/#customize-rebel-readline","title":"Customize Rebel Readline","text":":repl/help
in the repl prompt shows the Rebel configuration options
Set configuration options in a rebel_readline.edn
file, in $XDG_CONFIG_HOME/clojure/
or $HOME/.clojure
Practicalli Rebel Readline Configuration options $XDG_CONFIG_HOME/clojure/rebel_readline.edn;; ---------------------------------------------------------\n;; Rebel Readline Configuration\n;;\n;; Customise use and appearance\n;; ---------------------------------------------------------\n\n{;; Vi or Emacs style key-map\n ;; :viins or :emacs. Default :emacs\n :key-map :viins\n\n ;; Color theme - light or dark\n ;; :color-theme :light-screen-theme\n :color-theme :dark-screen-theme\n\n ;; Enable syntax highlight. Default true}\n :hihighlight true\n\n ;; Enable complete on tab. Default true}\n :completion true\n\n ;; Enable function documentation Default true\n :eldoc true\n ;; auto indent code on newline. Default true}\n :indent true\n\n ;; rebind root *out* during read to protect linereader, Default true}\n :redirect-output true\n\n ;; Custom key-bindings applied after all other \n :key-bindings {}}\n
"},{"location":"clojure-cli/repl/#next-steps","title":"Next Steps","text":" Code In The REPL
Managing Libraries In The REPL
Help In The REPL
Custom REPL Startup
REPL Uncovered
"},{"location":"clojure-cli/repl/coding/","title":"Coding in the REPL","text":"Clojure code can be typed into the REPL directly and the result instantly returned. Code can also be loaded from a project source code files, to run pre-written code.
Clojure Editors are the main tool for writing code An editor connected to a Clojure REPL and evaluating from source code files is the most effective way for writing Clojure code.
Evaluating code in an editor automatically uses the correct namespace, avoiding the need to change namespaces or fully qualify function calls. Evaluation results can be shown in-line, as comments next to the code or in a data inspector.
Editors provide structural editing and Clojure syntax checking, along with general editor features. and
"},{"location":"clojure-cli/repl/coding/#using-the-repl","title":"Using the REPL","text":"Use the clojure
command to start a REPL with Rebel, or the clj
wrapper with the Clojure CLI REPL (requires rlwrap
binary).
Rebel REPLClojure CLI REPL Start a Clojure REPL with Rebel terminal UI which also starts an nREPL server which a Clojure editor can connect too.
clojure -M:repl/rebel\n
A REPL prompt displays ready to evaluate a Clojure expression.
Start a Clojure REPL with a basic UI which also starts an nREPL server which a Clojure editor can connect too.
clj -M:repl/basic\n
The clj
wrapper requires rlwrap
binary.
A REPL prompt displays ready to evaluate a Clojure expression.
Project dependencies automatically downloaded on REPL start When a REPL is started from the root of a Clojure project the project dependencies are automatically downloaded (unless previously downloaded to the local maven cache, .m2/
) and project specific paths are added, e.g. src
tree.
Use REPL with a Clojure project A REPL can run without a Clojure project, however, libraries and code are simpler to manage within project source and configuration files.
"},{"location":"clojure-cli/repl/coding/#repl-start-state","title":"REPL start state","text":"The Clojure REPL always starts in the user
namespace.
During startup the the clojure.core
functions are required (made available) in the user namespace, so (map inc [1 2 3])
can be called without specifying the clojure.core
namespace in which those functions are defined.
If clojure.core were not required, then the expression would be (clojure.core/map clojure.core/inc [1 2 3])
"},{"location":"clojure-cli/repl/coding/#evaluating-code","title":"Evaluating code","text":"Type Clojure code at the => user
REPL prompt
Press Enter
to evaluate the code and see the result.
Up and Down navigate the REPL history, providing an efficient way to evaluate the same code many times.
In Rebel, typing part of function name shows matches available, Tab to cycle through the choices, Enter to select.
"},{"location":"clojure-cli/repl/coding/#load-code-from-file","title":"Load code from file","text":"Clojure code is usually saved in files and each file has a namespace definition that matches the file path, using the ns
function. The file src/practicalli/playground.clj
has the namespace practicalli.playground
(ns practicalli.playground)\n
Requiring the namespace of a file will evaluate (load) the code from that file in the REPL.
(require 'practicalli.playground)\n
Functions defined in that namespace can be called using their fully qualified names. e.g. if the namespace contains a function called main
, that function can be called using (practicalli.playground/main)
.
Change namespaces Change the namespace to practicalli.playground
to call functions defined in that namespace by their unqualified function name, eg. (main)
, rather than the fully qualified name, e.g. (practicalli.playground/main)
in-ns
will change change the current namespace to the one specified as an argument.
(in-ns 'practicalli.playground)\n
Now the (main)
function can be called without having to include the full namespace name.
Typically it is more efficient to stay in the user
namespace and require all other namespaces required.
"},{"location":"clojure-cli/repl/coding/#reload-code-changes","title":"Reload code changes","text":"The :reload
option to require
will load in any changes to a namespace that happened outside of the REPL, eg. using an editor to change the source code in the file.
(require 'practicalli.playground :reload)\n
Use the :verbose
option when issues occur loading a particular namespace. As the namespace being required may also require other namespaces, multiple namespaces may be loaded from one require
expression.
:verbose
shows a full list of the namespaces being loaded.
(require 'practicalli.playground :reload :verbose)\n
Reload in Terminal REPL for unconnected editor When using an editor that is not connected to the Clojure REPL, then reloading is an effective way of updating the code with all the changes saved in the file.
"},{"location":"clojure-cli/repl/coding/#close-repl","title":"Close REPL","text":":repl/quit
at the REPL prompt will end the REPL session and all code not saved to a file will be lost.
Ctrl+c if the repl process does not return to the shell prompt.
"},{"location":"clojure-cli/repl/coding/#next-steps","title":"Next steps","text":"Managing Library dependencies in REPL
"},{"location":"clojure-cli/repl/help/","title":"Help at the REPL","text":"rebel readline provides tools to help you discover and use functions from clojure.core and any other libraries you add to the REPL.
:repl/help
will show all the commands available for rebel readline
Tab to autocomplete the current characters into a function name. All functions that match the characters will be show, allowing quick discovery of functions available. Typing in the first few characters of a function and press
Moving the cursor after the name of a function will show the signatures available, so a function can be called with the correct number and form of arguments.
Ctrl+C+Ctrl+d on a function name shows the docstring to help understand the functions purpose.
clojure.repl/doc
function also shows the docstring of a function (clojure.repl/doc doc)
Ctrl+C+Ctrl+a on a name shows all the possible matching functions to help you discover what is available. Tab through the list of matches, Enter to select a function
"},{"location":"clojure-cli/repl/help/#rebel-commands","title":"Rebel Commands","text":"Type :repl/help
or :repl
TAB to see a list of available commands.
Keybinding Description :repl/help
Prints the documentation for all available commands. :repl/key-bindings
search or list current key bindings :repl/quit
Quits the REPL :repl/set-color-theme
Change the color theme :dark-screen-theme
:light-screen-theme
:repl/set-key-map
Change key bindings to given key-map, :emacs
:vicmd
:viins
:repl/toggle-color
Toggle ANSI text coloration on and off :repl/toggle-completion
Toggle the completion functionality on and off :repl/toggle-eldoc
Toggle the auto display of function signatures on and off :repl/toggle-highlight
Toggle readline syntax highlighting on and off :repl/toggle-indent
Toggle the automatic indenting of Clojure code on and off"},{"location":"clojure-cli/repl/help/#key-bindings","title":"Key-bindings","text":"Keybinding Description Ctrl-C
aborts editing the current line Ctrl-D
at the start of a line => sends an end of stream message TAB
word completion or code indent when cursor in whitespace at the start of line Ctrl-X_Ctrl-D
Show documentation for word at point Ctrl-X_Ctrl-S
Show source for word at point Ctrl-X_Ctrl-A
Show apropos for word at point Ctrl-X_Ctrl-E
Inline eval for SEXP before the point Examine key-bindings with the :repl/key-bindings
command.
"},{"location":"clojure-cli/repl/libraries/","title":"Using Clojure libraries in the REPL","text":"A library should be included as a dependency in order to use it within the REPL.
Add library dependencies to the top level :deps
key in a project deps.edn
configuration file, or add via an alias if the library is use at development time.
Aliases from a user configuration can also add optional libraries when running a REPL, e.g. Practicalli Clojure CLI config
{:paths [\"src\" \"resources\"]\n\n :deps\n {org.clojure/clojure {:mvn/version \"1.10.3\"}}\n\n :aliases\n {\n :database/h2\n {:extra-deps {com.h2database/h2 {:mvn/version \"2.1.210\"}\n com.github.seancorfield/next.jdbc {:mvn/version \"1.2.772\"}}}\n #_()}\n
Finding libraries Search for community libraries via the Clojars.org website
clojure -M:search/libraries pattern
where pattern is the name of the library to search for. Copy the relevant results into the project deps.edn
file.
clojure -M:search/libraries --format:merge pattern
will automatically add the library into the deps.edn
file.
clojure -X:deps find-versions :lib fully.qualified/library-name :n 5
returns the last 5 versions of the given library.
"},{"location":"clojure-cli/repl/libraries/#include-library","title":"Include library","text":"Open a terminal and change to the root of the Clojure project directory, where the deps.edn
file can be found.
Start the REPL including the :database/h2
alias to include every library defined in the :deps
key and libraries in the :database/h2
alias. This example is using rebel readline rich terminal UI
clojure -M:repl/rebel\n
This command will include
Add aliases to include optional libraries, such as those used for development. In this example, the H2 database and next.jdbc libraries are included along with those libraries in the :deps
key of deps.edn
clojure -M:database/h2:repl/rebel\n
"},{"location":"clojure-cli/repl/libraries/#load-namespace","title":"Load namespace","text":"At the REPL prompt, require a namespace from the project to load all the code from that namespace and any namespaces required.
If a project was created with the command clojure -T:project/new :template app :name practicalli/status-monitor
then the main namespace will be practicalli.status-monitor
(require '[practicalli.status-monitor])\n
The require
function loads all the code from the main namespace. When an ns
form is read, required namespaces in the ns
form are also loaded.
"},{"location":"clojure-cli/repl/libraries/#reloading-namespace","title":"Reloading namespace","text":"Clojure is a dynamic environment, so changes to function definitions (defn
) and shared symbol names (def
) can be updated without restarting the REPL.
require
loads the code from the specified namespace. Using the :reload
option forces the namespace to be loaded again, even if it was already loaded.
When changes are made to a namespace in the source code file, :reload
ensures those changes become the code running in the REPL
(require '[practicalli.status-monitor] :reload)\n
If errors occur when loading or reloading the namespace with require, the :verbose
option will show all the namespaces that are loaded. This may show issues or help track down conflicting namespaces or functions.
(require '[practicalli.status-monitor] :reload :verbose)\n
"},{"location":"clojure-cli/repl/libraries/#hotload-libraries","title":"Hotload libraries","text":"Hotload Libraries in the REPL add-libs
function from the clojure.tools.deps.alpha
library is an experimental approach to hot-loading library dependencies without having to restart the REPL or add those dependencies to the project deps.edn
. This provides a simple way to try out libraries.
hotload libraries secion for more details and how to use with Clojure editors.
Start a REPL session using Clojure CLI with the :lib/hotload alias
, including rebel readline for an enhance REPL terminal UI.
clojure -M:lib/hotload:repl/rebel\n
Require the clojure.tools.deps.alpha
library and refer the add-libs
function. The add-libs
function can then be called without having to use an alias or the fully qualified name.
(require '[clojure.tools.deps.alpha.repl :refer [add-libs]])\n
Hotload a library into the REPL using the add-lib
function in the following form, where domain/library
is the fully qualified name of the library and RELEASE
is a string of the version number of that library to use.
(add-libs '{domain/library {:mvn/version \"RELEASE\"}})\n
Multiple libraries can be hot-loaded in a single add-libs
expression
(add-libs '{hhgttg/meaning {:mvn/version \"4.2.0\"}\n eternity/room {:mvn/version \"1.0.1\"}})\n
"},{"location":"clojure-cli/repl/libraries/#hotload-hiccup-in-a-terminal-repl","title":"Hotload hiccup in a terminal REPL","text":"The hiccup library converts clojure structures into html, where vectors represent the scope of keywords that represent html tags.
Load the hiccup library using add-libs
(add-libs '{hiccup/hiccup {:mvn/version \"2.0.0-alpha2\"}})\n
Require the hiccup library so its functions are accessible from the current namespace in the REPL.
(require '[hiccup.core :as hiccup])\n
Enter an expression using the hiccup/html
function to convert a clojure data structure to html.
(hiccup/html [:div {:class \"right-aligned\"}])\n
The hiccup expression returns a string of the html code.
"},{"location":"clojure-cli/repl/repl-uncovered/","title":"Read, Evaluate Print Loop (REPL)","text":"The REPL provides a fast, powerful and fun way to develop code and is the hard of the Clojure developers workflow. The REPL allows you to quickly test out designs and your domain knowledge of the system you are building, easily accommodating multiple designs to help you evaluate the best approach.
Starting a REPL is the first thing you do after creating or downloading a project.
The REPL allows you to run any existing code, write new code and change code. Each time you can see the results of your code instantly.
The REPL can run all of your code or simply get the result of an individual expression. You can inspect run-time values and continually develop your code without having to restart each time.
Hint If you are not using the REPL for your Clojure development you are missing out on a highly productive workflow. Once you start using a REPL as part of you development cycle you will feel lost without one.
"},{"location":"clojure-cli/repl/repl-uncovered/#how-the-repl-works-simple-version","title":"How the REPL works (simple version)","text":"A Clojure REPL has 4 stages:
- Read - read in the code
- Evaluate - evaluate the code
- Print - show the results
- Loop - on to the next expression
Its useful to understand the difference between Read and Evaluate, especially when you get as far as writing macro's for Clojure.
"},{"location":"clojure-cli/repl/repl-uncovered/#the-reader","title":"The Reader","text":"The Reader parses the Clojure source code, form by form, producing the Clojure data structures an [Abstract Syntax Tree] (AST).
Due to the syntax of Clojure, much of the source code is already in the right structure. Any macros will be expanded into its Clojure structure.
These data structures are then evaluated: Clojure traverses the data structures and performs actions like function application or var lookup based on the type of the data structure.
For example, when Clojure reads the text (+ 1 2), the result is a list data structure whose first element is a + symbol, followed by the numbers 1 and 2. This data structure is passed to Clojure\u2019s evaluator, which looks up the function corresponding to + and applies that function to 1 and 2.
"},{"location":"clojure-cli/repl/repl-uncovered/#the-reader_1","title":"The Reader","text":"Hint Clojure is a homoiconic language, which is a fancy term describing the fact that Clojure programs are represented by Clojure data structures. This is a very important difference between Clojure and most other programming languages. It means that Clojure is defined in terms of the evaluation of data structures and not in terms of the syntax of character streams/files.
It is quite common, and easy, for Clojure programs to manipulate, transform and produce other Clojure programs.
The reader has syntax defined in terms of characters, and the Clojure language has syntax defined in terms of symbols, lists, vectors, maps etc. The reader is represented by the function read, which reads the next form (not character) from a stream, and returns the object represented by that form.
There are also Reader Macros that define special rules on top of the Clojure syntax. They give the language some additional syntax sugar, making your Clojure code compact. See the reference section on reader macros for more information
"},{"location":"clojure-cli/repl/repl-uncovered/#evaluator","title":"Evaluator","text":"The Evaluator takes the data structure as an argument (from the Reader) and processes it using rules corresponding to the data structure\u2019s type, returning the result.
To evaluate a symbol, Clojure looks up what the symbol refers to.
To evaluate a list, Clojure looks at the first element of the list and calls a function, macro, or special form.
Any other values including strings, numbers and keywords simply evaluate to themselves.
Hint Read the section on Reading, Evaluation and Macros from BraveClojure to see examples of the REPL process.
"},{"location":"clojure-cli/repl/troubleshooting/","title":"Troubleshooting the REPL","text":"The aspects to consider when a REPL process fails to run are:
- Some code expressions are not correct
- Dependencies are not available
- Project (or editor) misconfigured
"},{"location":"clojure-cli/repl/troubleshooting/#code-expression-failing","title":"Code expression failing","text":"All code in the project must compile and be syntactically correct, even if that code is in a rich (comment ,,,)
block.
A Clojure expression following a Reader comment, #_
does not have to compile, however it must be syntactically correct, i.e. balanced parentheses.
Add a line comment, ;;
, to any code that is suspected of not compiling or being syntactically incorrect (or delete that code).
"},{"location":"clojure-cli/repl/troubleshooting/#editor-repl-fails-to-start","title":"Editor REPL fails to start","text":"If using a jack-in approach with the editor to start the repl, run a terminal UI REPL with an nREPL server and try connecting to that REPL from the editor.
Clojure CLI repl - rebel terminal UI
clojure -M:repl/rebel\n
Then require the main namespace and see if there are issues, optionally using :verbose to see which libraries are being loaded.
(require '[practicalli.service] :verbose)\n
If the REPL runs correctly, it is likely the editor configuration is missing something or is incorrect. Check the configuration for running a Clojure project with the editor.
"},{"location":"clojure-cli/repl/troubleshooting/#terminal-ui-repl-fails-in-project","title":"Terminal UI REPL fails in project","text":"If the REPL does not run correctly or the namespace fails to load, run a repl without any extra development dependencies (tooling, dev libraries, etc) and load the main namespace
clj\n
"},{"location":"clojure-cli/repl/troubleshooting/#repl-doesnt-start-in-any-project","title":"REPL doesnt start in any project","text":"Run the clojure
command in a directory that is not part of any existing Clojure project. This will run the REPL with only the org.clojure/clojure
dependency
Run clojure -Sdescribe
to check that the Clojure CLI is using the correct configuration files and is the expected version.
If a REPL prompt appears, then Clojure CLI is working. If a REPL prompt does not appear, then reinstall the Clojure CLI or upgrade to a newer version.
Clojure CLI install - Practicalli Guide
"},{"location":"clojure-cli/repl/troubleshooting/#repl-starts-but-requiring-code-fails","title":"REPL starts but requiring code fails","text":"Creating a new project is a fast way to check development tooling is working correctly. A project can be created with clojure -T:project/create
(with Practicalli Clojure CLI Config installed)
If a REPL process starts correctly for a new project but not the existing project, then its most likely one or more expressions in the existing project that are causing an error or the project deps.edn
configuration.
Copy the deps.edn
configuration from the existing project to the root of the new project (or just the :deps
section of the deps.edn
configuration). Run the REPL again using the clojure
command. If the REPL fails then it is likely an issue with the exiting projects deps.edn
file or one of the dependencies
"},{"location":"clojure-cli/repl/troubleshooting/#dependency-issues","title":"Dependency issues","text":"Projects typically depend on many other libraries and sometimes those libraries depend on other libraries too.
When running the clojure
command to run a terminal UI REPL, libraries are retrieved from remote repositories (Maven Central, Clojars.org) and stored in a local cache ~/.m2/repositories
If a dependency is not available then a warning should state which library cannot be downloaded and from which repository
Check the extent of the dependencies for the existing project:
clojure -Stree\n
Use the antq tool to check for a newer version of a dependency
clojure -T:project/outdated\n
If libraries are likely to become unavailable (i.e. old versions) then consider creating a local repository service with Artefactory or Nexus, which can share library depenencies between development teams of an organisation.
"},{"location":"clojure-editors/","title":"Editors for Clojure development","text":"The best editor to use for learning Clojure is the editor already familiar with (or want to learn).
Use SublimeText & ClojureSublimed if unsure where to start as it will be the simplest tool to use.
"},{"location":"clojure-editors/#clojure-editor-features","title":"Clojure editor features","text":"An ideal Clojure editor includes the these core features
- running / connecting to a REPL process
- evaluation results inline or in a repl window (fast feedback on what the code does)
- syntax highlighting (including highlight of matching parens)
- structural editing to ensure parens are balanced when writing and refactor code
- data inspector to visualise large and nested data, or connection to data inpector tools
"},{"location":"clojure-editors/#clojure-aware-editors","title":"Clojure aware editors","text":"Emacs (Spacemacs, Doom, Prelude), Neovim (Conjure) and VSCode (Calva) are the most common open source Editors for Clojure and ClojureScript development.
SublimeText and IntelliJ are commercial editors (with limited free editions) which also provide Clojure support
EmacsNeovimVSCodeSublimeTextPulsar (Atom)Intellij Emacs is a very powerful editor with thousands of packages enabling a person to do almost any digital task concievable. Emacs is highly extensible via the ELisp programming language used to write configuration and the numerous Emacs packages. Native Compilation of Emacs packages dramatically speeds up many common tasks.
Emacs uses CIDER and Clojure LSP for a feature rich clojure development experience.
Use one of the popular community configurations for Emacs or visit the CIDER documentation to learn how to add Clojure support to Emacs.
SpacemacsPrelude EmacsDoom EmacsVanilla Emacs & Cider Spacemacs is a community configuration bringing Emacs features and Vim style editing together. Spacemacs uses a mnemonic menu system that makes it easy to learn and provides detailed documentation for configuring and using Emacs.
Practicalli Spacemacs provides a guide to Clojure development, vim-style editing, documenting with org-mode, Git version control with Magit, Issues & Pull Requests with Forge and dozens of other features.
Practicalli Spacemacs Config contains a customised configuration for Clojure development and supporting tools.
Free Desktop XDG ConfigClassic Config git clone https://github.com/practicalli/spacemacs.d.git $XDG_CONFIG_HOME/spacemacs`\n
git clone https://github.com/practicalli/spacemacs.d.git $HOME/.spacemacs.d`\n
The Practicalli configuration should replace the ~/.spacemacs
file if it exists
Spacemacs install guide - Practicalli Spacemacs
Emacs Prelude is an easy to use Emacs configuration for Emacs newcomers and lots of additional power for Emacs power users, from the author of CIDER - the definitive Clojure IDE for Emacs.
Prelude uses the traditional chorded key bindings to drive Emacs, e.g. Ctrl+c Ctrl+c to evaluate the current top-level form.
Prelude Install Guide
Doom Emacs is a community configuration for Emacs that provides a minimalistic configuration that is readily customisable. Doom Emacs is most suited to those coming from Vim and have a strong experience for multi-modal editing.
Practicalli Doom Emacs Book
Practicalli Doom Emacs Config contains a customised configuration for Clojure development and supporting tools.
Free Desktop XDG ConfigClassic Config git clone https://github.com/practicalli/doom-emacs-config.git $XDG_CONFIG_HOME/doom`\n
The Practicalli configuration should replace the ~/.config/doom/
directory created by the doom install
command. git clone https://github.com/practicalli/doom-emacs-config.git $HOME/.doom.d`\n
The Practicalli configuration should replace the ~/.doom.d/
directory created by the doom install
command.
Emacs 29 is recommended as it includes native compilation support and optomised JSON support which is valuable for Language Server Protocol servers.
Emacs is available for Linux, MacOSX and Windows.
Ubuntu / DebianHomebrew / MacOSXWindowsMsys2 apt-cache show emacs
to check available versions of Emacs in the Ubuntu package manager. If version 28 is available, install Emacs using the Ubuntu package manager.
sudo apt install emacs\n
Additional versions of Emacs are available via the Ubuntu Emacs Team Personal Package Archive.
sudo apt install emacs-snapshot
package to use the latest nightly build of Emacs, although be aware that some things may break.
Build Emacs 29 from source Building Emacs 29 from source code on Ubuntu is relatively straight forward task, although it will take a little time to compile. Building Emacs allows customisation of some features, such as native compilatin of elisp to enhance the performance of Emacs.
Emacs Plus from Homebrew provides many options, including native compilation and Spacemacs Icon for application launchers.
brew tap d12frosted/emacs-plus`\nbrew install emacs-plus@28 --with-native-comp --with-spacemacs-icon\n
Emacs.app is installed to: /usr/local/opt/emacs-plus@28
Optionally run Emacs plus as a service
brew services start d12frosted/emacs-plus/emacs-plus@28\n
Run emacs
Get a hot cup of something as Emacs native compilation compiles all the things.
The Spacemacs README lists other options for MacOSX.
Download Emacs-28.2 from the GNU repository and extract the zip file to %AppData%/local/Programs/emacs
.
Alternatively, if you are using the Chocolatey package manager then install Emacs version 28
Add the Emacs directory to the PATH
variable in your user account environment variables.
To start Emacs run the command runemacs.exe
. You can also pin this to the start menu or task bar.
Access to common Unix tools Command line tools, such as diff
, are used by Emacs. To have these command line tools available in Windows, install Emacs as above but then run emacs from a Unix shell such as GitBash.
Install Emacs (64bits build) with the following:
pacman -S mingw-w64-x86_64-emacs\n
Once Emacs is installed, add the cider package for essential Clojure support.
Cider Install Guide
Neovim is a hyper-extensible text editor that runs in a terminal, configured with the Lua programming language. Configuration can also be written in Fennel (a lisp dialect), using nfnl to generate Lua code.
Neovim is based on multi-model editing (e.g. normal, insert, visual editing states) providing a highly effective tool for writing code, configuration and documentation.
Neovim includes Treesitter which understands the syntax of a great many programming and configuration languages, which can be coupled with analysist from Language Sever Protocol (LSP) servers to provide live feedback on code quality.
Conjure provides Clojure interactive (REPL) development, supporting Clojure CLI, Leiningen and Babashka projects (as well as several other Lisp dialects and interesting languages)
Try the Conjure interactive :ConjureSchool
tutorial which only requires a recent version of neovim
curl -fL conjure.fun/school | sh\n
:q
to quit the tutorial.
Practicalli AstroNvimPracticalli NeovimSpaceVimVimIced Practicalli Neovim - AstroNvim install
AstroNvim community configuration for Neovim provides an engaging UI, using Lazy plugin manger and Mason to manage LSP servers, format & lint tools.
Practicalli AstroNvim Config provides a user configuration for Astronvim, including Conjure, parinfer, LSP server and treesitter parser for Clojure development.
Practicalli Neovim provides an install and user guide for Neovim and Conjure for Clojure development, folloiwng a REPL driven workflow.
Archived project - to be reimplemented using nfnl rather than aniseed. Recommend using AstroNvim instead.
Practicalli Neovim Config Redux configuration for Neovim which adds a range of Neovim plugins for a rich development experience.
- mnemonic of key bindings to make adoptiong Neovim easier
- visual navigation for files, project, environment variables and any other list items
- version control and GitHub issue & pull request management
Conjure provides interactive environment for evaluating Clojure code and providing inline results (or see results in an Heads Up Display or Log buffer).
Practicalli Neovim provides an install and user guide for Neovim and Conjure for Clojure development, folloiwng a REPL driven workflow.
SpaceVim is a fully featured vim experience that includes a minimal Clojure development environment based around vim-fireplace
Follow the Quick Start Guide to install SpaceVim
Add the Clojure layer to the SpaceVim custom configuration file ~/.SpaceVim.d/init.toml
[[layers]]\n name = \"lang#clojure\"\n
SpaceVim quickstart guide SpaceVim - Clojure Layer
Interactive Clojure Environment for Vim8/Neovim, aimed at the more experienced Vim/Neovim user.
vim-iced uses vim-sexp for structural editing
vim-plug is required to install the vim-iced packages.
vim-iced documentation
VS Code is a freely available editor build on open source and available for Linux, MacOS and Microsoft Windows.
VSCode Getting Started Guide
VS Code has a large marketplace of extensions, proiding additional tools for a complete development environment.
Calva is the most commonly used extension for Clojure support and aims to provide similar features to Emacs Cider (and uses some of the same Clojure libraries).
Clojure CLI User Aliases not directly supported
Calva does not support Clojure CLI user aliases directly (only project deps.edn). A JSON mapping must be added to the Calva configuration for each user alias (duplicating configuration)
Practicalli recommends starting the Clojure REPL in a terminal and specifying the required Clojure CLI user aliases, using Calva connect once the REPL has started.
VSpaceCode provides a mnemonic menu to drive VS Code by keyboard alone, vim editing and rich Git client (edamagit). VSpaceCode extension also provides key bindings for common Calva commands. Users experienced with Neovim and Emacs (Spacemacs / Doom) will find this extension makes VS Code easiter to use than vanilla VS Code or VS Code with an Neovim backend.
Clover provides a mininal, highly customisable environment using Socket REPL.
CalvaVSpaceCodeClover The Calva extension adds Clojure REPL support to VS Code editor, including Clojure LSP, formatting, structural editing and many other features.
Calva is under active development and the #calva channel on the Clojurians Slack community can be supportive.
Calva Getting Started Guide Calva - VS Code Marketplace
VSpaceCode is a Spacemacs-like community configuration for Microsoft VS Code. Drive VS Code entirely from the keyboard, using easy to remember mnemonic keys for all commands and full vim-stile editing tools.
Calva extension must be added as it is not part of VSpaceCode, although Calva commands are included in the VSpaceCode mneomoic menu when a Clojure file is open.
Edamagit is a sophisticated text based Git client (like magit for Emacs) is also included in the VSpacemacs extension.
Practicalli VSpaceCode install guide Practicalli VSpaceCode user guide
Clover is a Socket REPL based development tool for Clojure with some ClojureScript support (not including Figwheel).
Clojure GitLab repository includes usage details.
SublimeText 4 is a lightweight and feature rich text editor, especially of interest to those that like a simple and uncluttered UI. SublimeText is a commercial project although has free trial version available (check conditions of use).
Clojure-Sublimed provides Clojure support for SublimeText 4, with support for Clojure & Edn syntax, code formatting and an nREPL client to connect to a Clojure REPL process.
Tutkain is an Sublime 4 package that supports Clojure development (self-described as alpha software).
Build configuration to start a REPL
Clojure Sublime connects to a REPL via an nREPL server. Run a terminal REPL using Clojure CLI, Leinginen (lein repl
) or Shadow-cljs (shadow-cljs watch app
)
Alternatively, configure Clojure Sublimed to run a REPL process by creating a new build system via Tools \u00bb Build System \u00bb New Build System. The following example starts a Clojure CLI REPL with nREPL server and assumes Java and Clojure CLI are installed.
{\"env\": {\"JAVA_HOME\": \"/path/to/java\"},\n \"cmd\": [\"/usr/local/bin/clojure\", \"-Sdeps\", \"{:deps {nrepl/nrepl {:mvn/version \\\"1.0.0\\\"}}}\", \"-M\", \"-m\", \"nrepl.cmdline\"]}\n
Run a REPL process via Tools \u00bb Build With\u2026 and connect to the REPL using the command Clojure Sublimed: Connect SublimeText install Clojure-Sublimed install SublimeText Documentation
Pulsar Community-led Hyper-Hackable Editor is a very new project to create a community version of the Atom editor from GitHub.
Chlorine plugin provides a Clojure and ClojureScript development environment using Socket-REPL integration
Pulsar Community-led Hyper-Hackable Editor Pulsar Chlorine plugin
Atom not actively developed
Atom will be archived on December 15 2022 with no further updates from GitHub team
Consider using VSCode with Clover or Calva plugin or help support the evolution of the Pulsar project
Cursive may be an appropriate choice for people from a Java background who are already familiar with IntelliJ. Cursive will run static analysis of Clojure code when opening a Clojure project, as IntelliJ does with other languages.
Follow the Cursive user guide to configure IntelliJ and install Cursive.
Requires license for commercial development
There is a free license when development is not for commercial projects, however, a license must be purchased for each developer working on a commercial project.
IntelliJ & Cursive install guide
"},{"location":"clojure-editors/clojure-lsp/","title":"Language Server Protocol","text":" Language Server Protocol provides a standard to provide a common set of development tools, e.g. code completion, syntax highlighting, refactor and language diagnostics.
Each language requires a specific LSP server implementation.
An editor or plugin provides an LSP client that uses data from language servers, providing information about source code and enabling development tools to understand the code structure.
"},{"location":"clojure-editors/clojure-lsp/#clojure-lsp","title":"Clojure LSP","text":" clojure-lsp is an implementation of an LSP server for Clojure and ClojureScript languages. Clojure LSP is built on top of clj-kondo which provides the static analysis of Clojure and ClojureScript code.
Most Clojure aware editors provide an LSP client.
"},{"location":"clojure-editors/clojure-lsp/#install","title":"Install","text":"Clojure LSP installation guide covers multiple operating systems.
LinuxHomebrew Practicalli recommends downloading the clojure-lsp-native-linux-amd64
from GitHub release page
Extracts the clojure-lsp
binary to ~/.local/bin/clojure-lsp
Clojure LSP project provides a custom tap for installing the latest version.
brew install clojure-lsp/brew/clojure-lsp-native\n
Homebrew default package deprecated The clojure-lsp
formula is deprecated and should not be used.
brew remove clojure-lsp
if the default clojure-lsp was installed
Check Clojure LSP server is working via the command line
clojure-lsp --version\n
Editors may provide install mechanism for Clojure LSP Spacemacs LSP layer will prompt to install a language server when first opening a file of a major mode where LSP is enabled. E.g. when a Clojure related file is opened, the Clojure LSP server is downloaded if not installed (or not found on the Emacs path).
Neovim package called mason manages the install of lint & format tools as well as LSP servers, or an externally installed LSP server can also be used.
VSCode Calva plugin includes the clojure-lsp server, although an external server can be configured.
"},{"location":"clojure-editors/clojure-lsp/#configure","title":"Configure","text":"Practicalli Clojure LSP Configuration config.edn is the recommended configuration from Practicalli.
;; ---------------------------------------------------------\n;; Clojure LSP user level (global) configuration\n;; https://clojure-lsp.io/settings/\n;;\n;; Complete config.edn example with default settings\n;; https://github.com/clojure-lsp/clojure-lsp/blob/master/docs/all-available-settings.edn\n;; default key/value are in comments\n;; ---------------------------------------------------------\n\n;; Refact config from all-available-settings example\n\n{;; ---------------------------------------------------------\n ;; Project analysis\n\n ;; auto-resolved for deps.edn, project.clj or bb.edn projects\n ;; :source-paths #{\"src\" \"test\"}\n\n ;; Include :extra-paths and :extra-deps from project & user level aliases in LSP classpath\n ;; :source-aliases #{:dev :test}\n :source-aliases #{:dev :test :dev/env :dev/reloaded}\n\n ;; Define a custom project classpath command, e.g. Clojure CLI\n ;; :project-specs [{:project-path \"deps.edn\"\n ;; :classpath-cmd [\"clojure\" \"-M:env/dev:env/test\" \"-Spath\"]}]\n ;; Check the default at clojure-lsp.classpath/default-project-specs\n ;; :project-specs []\n\n ;; ignore analyzing/linting specific paths\n :source-paths-ignore-regex [\"target.*\" \"build.*\" \"console-log-.*\"]\n\n ;; Additional LSP configurations to load from classpath\n ;; https://clojure-lsp.io/settings/#classpath-config-paths\n ;; :classpath-config-paths []\n\n ;; :paths-ignore-regex []\n\n ;; Watch for classpath changes\n ;; :notify-references-on-file-change true\n ;; :compute-external-file-changes true\n\n ;; Approach for linking dependencies\n ;; :dependency-scheme \"zipfile\"\n\n ;; generate and analyze stubs for specific namespaces on the project classpath\n ;; typically for closed source dependencies, e.g. datomic.api\n ;; https://clojure-lsp.io/settings/#stub-generation\n ;; :stubs {:generation {:namespaces #{}\n ;; :output-dir \".lsp/.cache/stubs\"\n ;; :java-command \"java\"}\n ;; :extra-dirs []}\n\n ;; Java Sources from Ubuntu package openjdk-17-source\n ;; jdk-source-uri takes precedence\n ;; :java\n ;; {:jdk-source-uri \"https://raw.githubusercontent.com/clojure-lsp/jdk-source/main/openjdk-19/reduced/source.zip\"\n ;; :home-path nil ;; jdk-source-uri takes precedence\n ;; :download-jdk-source? false\n ;; :decompile-jar-as-project? true}\n ;; :java\n ;; {:jdk-source-uri \"file:///usr/lib/jvm/openjdk-17/lib/src.zip\"}\n :java nil\n\n ;; End of Project analysis\n ;; ---------------------------------------------------------\n\n ;; ---------------------------------------------------------\n ;; Linter configuration\n\n ;; clj-kondo Linter rules\n ;; https://github.com/clj-kondo/clj-kondo/blob/master/doc/config.md#enable-optional-linters\n ;; :linters {:clj-kondo {:level :on\n ;; :report-duplicates true\n ;; :ns-exclude-regex \"\"}\n ;; :clj-depend {:level :info}} ;; Only if any clj-depend config is found\n\n ;; asynchronously lint project files after startup, for features like List project errors\n ;; :lint-project-files-after-startup? true\n\n ;; copy clj-kondo hooks configs exported by libs on classpath during startup\n ;; :copy-kondo-configs? true\n\n ;; End of Linter configuration\n ;; ---------------------------------------------------------\n\n ;; ---------------------------------------------------------\n ;; Refactor code\n\n ;; Namespace format\n ;; :clean {:automatically-after-ns-refactor true\n ;; :ns-inner-blocks-indentation :next-line\n ;; :ns-import-classes-indentation :next-line\n ;; :sort {:ns true\n ;; :require true\n ;; :import true\n ;; :import-classes {:classes-per-line 3} ;; -1 for all in single line\n ;; :refer {:max-line-length 80}}}\n\n ;; Do not sort namespaces\n :clean {sort {:ns false\n :require false\n :import false}}\n\n ;; Automatically add ns form to new Clojure/Script files\n ;; :auto-add-ns-to-new-files? true\n\n ;; use ^private metadata rather than defn-\n ;; :use-metadata-for-privacy? false\n :use-metadata-for-privacy? true\n\n ;; Keep parens around single argument functions in thread macro\n ;; :keep-parens-when-threading? false\n :keep-parens-when-threading? true\n\n ;; End of Refactor code\n ;; ---------------------------------------------------------\n\n ;; ---------------------------------------------------------\n ;; Clojure formatting configuration - cljfmt\n\n ;; location of cljfmt configuration for formatting\n ;; Path relative to project root or an absolute path\n ;; :cljfmt-config-path \".cljfmt.edn\"\n :cljfmt-config-path \"cljfmt.edn\"\n\n ;; Specify cljfmt configuration within Clojure LSP configuration file\n ;; :cljfmt {}\n\n ;; End of Clojure formatting configuration - cljfmt\n ;; ---------------------------------------------------------\n\n ;; ---------------------------------------------------------\n ;; Visual LSP components\n\n ;; :hover {:hide-file-location? false\n ;; :arity-on-same-line? false\n ;; :clojuredocs true}\n\n ;; :completion {:additional-edits-warning-text nil\n ;; :analysis-type :fast-but-stale}\n\n ;; :code-lens {:segregate-test-references true}\n\n ;; LSP semantic tokens server support for syntax highlighting\n ;; :semantic-tokens? true\n\n ;; Documentation artefacts\n ;; :document-formatting? true\n ;; :document-range-formatting? true\n\n ;; End of Visual LSP components\n ;; ---------------------------------------------------------\n\n ;; ---------------------------------------------------------\n ;; LSP general configuration options\n\n ;; Exit clojure-lsp if any errors found, e.g. classpath scan failure\n ;; :api {:exit-on-errors? true}\n\n ;; Synchronise whole buffer `:full` or only related changes `:incremental`\n ;; :text-document-sync-kind :full\n\n ;; End of LSP general configuration options\n ;; ---------------------------------------------------------\n\n ;; ---------------------------------------------------------\n ;; File locations\n\n ;; project analysis cache to speed clojure-lsp startup\n ;; relative path to project root or absolute path\n ;; :cache-path \".lsp/.cache\"\n\n ;; Absolute path\n ;; :log-path \"/tmp/clojure-lsp.*.out\"\n\n ;; End of file locations\n ;; ---------------------------------------------------------\n\n ;; ---------------------------------------------------------\n ;; LSP snippets\n ;; https://clojure-lsp.io/features/#snippets\n\n :additional-snippets\n [;; Documentation / comments\n\n {:name \"comment-heading\"\n :detail \"Comment Header\"\n :snippet\n \";; ---------------------------------------------------------\n ;; ${1:Heading summary title}\n ;;\n ;; ${2:Brief description}\\n;; ---------------------------------------------------------\\n\\n$0\"}\n\n {:name \"comment-separator\"\n :detail \"Comment Separator\"\n :snippet\n \";; ---------------------------------------------------------\\n;; ${1:Section title}\\n\\n$0\"}\n\n {:name \"comment-section\"\n :detail \"Comment Section\"\n :snippet\n \";; ---------------------------------------------------------\\n;; ${1:Section title}\\n\\n$0\\n\\n\n ;; End of $1\\n;; ---------------------------------------------------------\\n\\n\"}\n\n {:name \"wrap-reader-comment\"\n :detail \"Wrap current expression with Comment Reader macro\"\n :snippet \"#_$current-form\"}\n\n {:name \"rich-comment\"\n :detail \"Create rich comment\"\n :snippet\n \"(comment\n $0\n #_()) ;; End of rich comment\"}\n\n {:name \"rich-comment-rdd\"\n :detail \"Create comment block\"\n :snippet\n \"#_{:clj-kondo/ignore [:redefined-var]}\n (comment\n $0\n #_()) ; End of rich comment\"}\n\n {:name \"rich-comment-hotload\"\n :detail \"Rich comment library hotload\"\n :snippet\n \"#_{:clj-kondo/ignore [:redefined-var]}\n (comment\n ;; Add-lib library for hot-loading\n (require '[clojure.tools.deps.alpha.repl :refer [add-libs]])\n (add-libs '{${1:domain/library-name} {:mvn/version \\\"${2:1.0.0}\\\"}$3})\n $0\n #_()) ; End of rich comment block\"}\n\n {:name \"wrap-rich-comment\"\n :detail \"Wrap current expression with rich comment form\"\n :snippet\n \"(comment\n $current-form\n $0\n #_()) ;; End of rich comment\"}\n\n ;; Core functions\n\n {:name \"def\"\n :detail \"def with docstring\"\n :snippet \"(def ${1:name}\\n \\\"${2:doc-string}\\\"\\n $0)\"}\n\n {:name \"def-\"\n :detail \"def private\"\n :snippet \"(def ^:private ${1:name}\\n \\\"${2:doc-string}\\\"\\n $0)\"}\n\n {:name \"defn\"\n :detail \"Create public function\"\n :snippet \"(defn ${1:name}\\n \\\"${2:doc-string}\\\"\\n [${3:args}]\\n $0)\"}\n\n {:name \"defn-\"\n :detail \"Create public function\"\n :snippet \"(defn ^:private ${1:name}\\n \\\"${2:docstring}\\\"\\n [${3:args}]\\n $0)\"}\n\n {:name \"ns\"\n :detail \"Create ns\"\n :snippet \"(ns ${1:name}\\n \\\"${2:doc-string}\\\"\\n ${3:require})\"}\n\n ;; Clojure CLI alias snippets\n\n {:name \"deps-alias\"\n :detail \"deps.edn alias with extra path & deps\"\n :snippet\n \":${1:category/name}\n {:extra-paths [\\\"${2:path}\\\"]\n :extra-deps {${3:deps-maven or deps-git}}}$0\"}\n\n {:name \"deps-alias-main\"\n :detail \"deps.edn alias with extra path & deps\"\n :snippet\n \":${1:category/name}\n {:extra-paths [\\\"${2:path}\\\"]\n :extra-deps {${3:deps-maven or deps-git}}\n :main-opts [\\\"-m\\\" \\\"${4:main namespace}\\\"]}$0\"}\n\n {:name \"deps-alias-exec\"\n :detail \"deps.edn alias with extra path & deps\"\n :snippet\n \":${1:category/name}\n {:extra-paths [\\\"${2:path}\\\"]\n :extra-deps {${3:deps-maven or deps-git}}\n :exec-fn ${4:domain/function-name}\n :exec-args {${5:key value}}}$0\"}\n\n {:name \"deps-alias-main-exec\"\n :detail \"deps.edn alias with extra path & deps\"\n :snippet\n \":${1:category/name}\n {:extra-paths [\\\"${2:path}\\\"]\n :extra-deps {${3:deps-maven or deps-git}}\n :main-opts [\\\"-m\\\" \\\"${4:main namespace}\\\"]\n :exec-fn ${4:domain/function-name}\n :exec-args {${5:key value}}}$0\"}\n\n {:name \"deps-maven\"\n :detail \"deps.edn Maven dependency\"\n :snippet\n \"${1:domain/library-name} {:mvn/version \\\"${2:1.0.0}\\\"}$0\"}\n\n {:name \"deps-git\"\n :detail \"deps.edn Git dependency\"\n :snippet\n \"${1:domain/library-name}\n {:git/sha \\\"${2:git-sha-value}\\\"}$0\"}\n\n {:name \"deps-git-tag\"\n :detail \"Git dependency\"\n :snippet\n \"${1:domain/library-name}\n {:git/tag \\\"${2:git-tag-value}\\\"\n :git/sha \\\"${3:git-sha-value}\\\"}$0\"}\n\n {:name \"deps-git-url\"\n :detail \"Git URL dependency\"\n :snippet\n \"${1:domain/library-name}\n {:git/url \\\"https://github.com/$1\\\"\n :git/sha \\\"${2:git-sha-value}\\\"}$0\"}\n\n {:name \"deps-local\"\n :detail \"deps.edn Maven dependency\"\n :snippet\n \"${1:domain/library-name} {:local/root \\\"${2:/path/to/project/root}\\\"}$0\"}\n\n ;; Requiring dependency snippets\n\n {:name \"require-rdd\"\n :detail \"require for rich comment experiments\"\n :snippet \"(require '[${1:namespace} :as ${2:alias}]$3)$0\"}\n\n {:name \"require\"\n :detail \"ns require\"\n :snippet \"(:require [${1:namespace}])$0\"}\n\n {:name \"require-refer\"\n :detail \"ns require with :refer\"\n :snippet \"(:require [${1:namespace} :refer [$2]]$3)$0\"}\n\n {:name \"require-as\"\n :detail \"ns require with :as alias\"\n :snippet \"(:require [${1:namespace} :as ${2:alias}]$3)$0\"}\n\n {:name \"use\"\n :detail \"require refer preferred over use\"\n :snippet \"(:require [${1:namespace} :refer [$2]])$0\"}\n\n ;; Unit Test snippets\n\n {:name \"deftest\"\n :detail \"deftest clojure.test\"\n :snippet\n \"(deftest ${1:name}-test\n (testing \\\"${2:Context of the test assertions}\\\"\n (is (= ${3:assertion-values}))$4)) $0\"}\n\n {:name \"testing\"\n :detail \"testing asserting group for clojure.test\"\n :snippet \"(testing \\\"${1:description-of-assertion-group}\\\"\\n $0)\"}\n\n {:name \"is\"\n :detail \"assertion for clojure.test\"\n :snippet \"(is (= ${1:function call} ${2:expected result}))$0\"}\n\n ;; ---------------------------------------------------------\n ;; Clojure LSP and Clj-kondo snippets\n\n {:name \"lsp-ignore-redefined\"\n :detail \"Ignore redefined Vars\"\n :snippet\n \"#_{:clj-kondo/ignore [:redefined-var]}\n $0\"}\n\n ;; End of Clojure LSP and Clj-kondo snippets\n ;; ---------------------------------------------------------\n ]\n ;; End of LSP snippets\n ;; ---------------------------------------------------------\n\n }\n
Include :extra-paths
and :extra-deps
from project & user level aliases in LSP classpath. e.g. support a custom user
namespace in dev/user.clj
:source-aliases #{:dev :test :env/dev :env/test :lib/reloaded}\n
Include Java Sources installed via Debian / Ubuntu package openjdk-21-source
to support calls to Java Objects and Methods.
:java\n {:jdk-source-uri \"file:///usr/lib/jvm/openjdk-21/lib/src.zip\" ;;\n :home-path nil ;; jdk-source-uri takes precedence\n :download-jdk-source? false}\n
Disable Java analysis
If not using Java Interop with Clojure, it can be an advantage to disable the Java analysis. This should remove Java functions from autocomplete.
:java nil\n
Clean namespace ns
forms but do not sort require names
:clean {:automatically-after-ns-refactor true\n :ns-inner-blocks-indentation :next-line\n :ns-import-classes-indentation :next-line\n :sort {:ns false\n :require false\n :import false\n :import-classes {:classes-per-line 3} ;; -1 for all in single line\n :refer {:max-line-length 80}}}\n
Use ^private
metadata for private function definitions rather than defn-
:use-metadata-for-privacy? true\n
Location of cljfmt configuration for formatting, path relative to project root. The defaults for cljfmt are used, except :remove-consecutive-blank-lines?
which is set to false to enable more readable code.
:cljfmt-config-path \"cljfmt.edn\"\n
cljfmt configuration included example :indents
rules for clojure.core, compojure, fuzzy rules and examples used by the Clojure LSP maintainer.
"},{"location":"clojure-editors/clojure-lsp/#practicalli-snippets","title":"Practicalli snippets","text":"Practicalli Snippets are defined in the :additional-snippets
section of the Practicalli Clojure LSP config.
"},{"location":"clojure-editors/clojure-lsp/#docs-comments","title":"Docs / comments","text":" comment-heading
- describe purpose of the namespace comment-separator
- logically separate code sections, helps identify opportunities to refactor to other name spaces comment-section
- logically separate large code sections with start and end line comments wrap-reader-comment
- insert reader comment macro, #_
before current form, informing Clojure reader to ignore next form
"},{"location":"clojure-editors/clojure-lsp/#repl-driven-development","title":"Repl Driven Development","text":" rich-comment
- comment block rich-comment-rdd
- comment block with ignore :redefined-var for repl experiments rich-comment-hotload
- comment block with add-libs code for hotloading libraries in Clojure CLI repl wrap-rich-comment
- wrap current form with comment reader macro require-rdd
- add a require expression, for adding a require in a rich comment block for RDD
"},{"location":"clojure-editors/clojure-lsp/#standard-library-functions","title":"Standard library functions","text":" def
- def with docstring def-
- private def with docstring defn
- defn with docstring defn-
private defn with docstring ns
- namespace form with docstring
"},{"location":"clojure-editors/clojure-lsp/#clojure-cli-depsedn-aliases","title":"Clojure CLI deps.edn aliases","text":" deps-alias
- add Clojure CLI alias deps-maven
- add a maven style dependency deps-git
- add a git style dependency using :git/sha
deps-git-tag
- as above including :git/tag
deps-git-url
- add git style dependency using git url (url taken from dependency name as it is typed - mirrored placeholder) deps-local
- add a :local/root
dependency
"},{"location":"clojure-editors/clojure-lsp/#requiring-dependencies","title":"Requiring dependencies","text":" require-rdd
- add a require expression, for adding a require in a rich comment block for RDD require
- simple require require-refer
- require with :refer
require-as
- require with :as
alias use
- creates a require rather than the more troublesome use
"},{"location":"clojure-editors/clojure-lsp/#unit-testing","title":"Unit testing","text":" deftest
- creates a deftest with testing directive and one assertion testing
- creates a testing testing directive and one assertion is
- an assertion with placeholders for test function and expected results
"},{"location":"clojure-editors/clojure-lsp/#references","title":"References","text":" - LSP mode - A guide on disabling / enabling features - if the Emacs UI is too cluttered or missing visual features
- Configure Emacs as a Clojure IDE
- Language Server Protocol support for Emacs
"},{"location":"clojure-editors/clojure-lsp/practicalli-snippets/","title":"Practicalli Snippets for Clojure LSP","text":"Custom snippets created by Practicalli and added via the :additional-snippets
key in the Clojure LSP configuration (.lsp/config.edn
or user level configuration). Snippets are defined as a vector of hash-maps
{:additional-snippets [{} {} {} ,,,]}\n
Practicalli Snippets available in practicalli/clojure-lsp-config
Move or delete the clojure configuration directory and clone the Practicalli Clojure CLI Config
"},{"location":"clojure-editors/clojure-lsp/practicalli-snippets/#documentation","title":"Documentation","text":"A comment heading to describe the purpose and important information about the current namesapce.
{:name \"comment-heading\"\n :detail \"Comment Header\"\n :snippet\n \";; ---------------------------------------------------------\n ;; ${1:Heading summary title}\n ;;\n ;; ${2:Brief description}\\n;; ---------------------------------------------------------\\n\\n$0\"}\n
A comment separator for marking logical sections within a namespace, useful for navigating code and identifying opportunities to refactor a namespace into multiple namespaces.
{:name \"comment-separator\"\n :detail \"Comment Separator\"\n :snippet\n \";; ---------------------------------------------------------\\n;; ${1:Section title}\\n\\n$0\"}\n
A comment section with start and end titles for marking logical sections within a namespace, again for navigation and identifying opportunities to refactor a namespace.
{:name \"comment-section\"\n :detail \"Comment Section\"\n :snippet\n \";; ---------------------------------------------------------\\n;; ${1:Section title}\\n\\n$0\\n\\n\n ;; End of $1\\n;; ---------------------------------------------------------\\n\\n\"}\n
"},{"location":"clojure-editors/clojure-lsp/practicalli-snippets/#repl-driven-development","title":"REPL Driven Development","text":"A rich comment block typically used to hold function calls to show how to make use of the important aspects of the current namespace. For example, calls to start
, restart
, stop
functions in a namespace that defines the service life-cycle.
This provides a live executable guide to using the namespace, without being called if the whole namespace is evaluated.
A commented expression is placed before the closing paren to ensure that closing paren is not folded up into the previous line. This makes it easier to add further code to the rich comment block.
{:name \"rich-comment\"\n :detail \"Create rich comment\"\n :snippet\n \"(comment\n $0\n #_()) ;; End of rich comment\"}\n
A modified rich comment block with clj-kondo configuration to suppress warnings for duplicate function definition names, supporting alternative function implementations as part of a REPL driven development workflow.
{:name \"rich-comment-rdd\"\n :detail \"Create comment block\"\n :snippet\n \"#_{:clj-kondo/ignore [:redefined-var]}\n (comment\n $0\n #_()) ;; End of rich comment\"}\n
Wrap an existing form in a rich comment
{:name \"wrap-rich-comment\"\n :detail \"Wrap current expression with rich comment form\"\n :snippet\n \"(comment\n $current-form\n $0\n #_()) ;; End of rich comment\"}\n
Comment an existing form with the Clojure Comment macro, _#
{:name \"wrap-reader-comment\"\n :detail \"Wrap current expression with Comment Reader macro\"\n :snippet \"#_$current-form\"}\n
"},{"location":"clojure-editors/clojure-lsp/practicalli-snippets/#hot-loading-library-dependencies","title":"Hot loading library dependencies","text":"Clojure CLI projects can hotload library dependencies into a running Clojure REPL. This requires starting a REPL with the clojure.tools.deps.alpha
library as a dependency which can be done by including the :lib/hotload
alias from practicalli/clojure-deps-edn. Note this library is alpha and the API could change in future.
Create a rich comment block that requires the clojure.tools.deps.alpha
namespace and an add-libs
expression to hotload one or more libraries in a hash-map. Tab stops with placeholders are included for adding the first library to hotload.
{:name \"rich-comment-hotload\"\n :detail \"Rich comment library hotload\"\n :snippet\n \"#_{:clj-kondo/ignore [:redefined-var]}\n (comment\n ;; Add-lib library for hot-loading\n (require '[clojure.tools.deps.alpha.repl :refer [add-libs]])\n (add-libs '{${1:domain/library-name} {:mvn/version \\\"${2:1.0.0}\\\"}$3})\n $0\n #_()) ;; End of rich comment block\"}\n
"},{"location":"clojure-editors/clojure-lsp/practicalli-snippets/#core-functions","title":"Core functions","text":"Create a public var using a def
form with a doc-string, with placeholders for name and value.
{:name \"def\"\n :detail \"def with docstring\"\n :snippet \"(def ${1:name}\\n \\\"${2:docstring}\\\"\\n $0)\"}\n
Create a private var using a def
form with ^:private
meta data and a doc-string, with placeholders for name and value.
{:name \"def-\"\n :detail \"def private\"\n :snippet \"(def ^:private ${1:name}\\n \\\"${2:doc-string}\\\"\\n $0)\"}\n
A defn
form with name, doc-string and args tab-stops
{:name \"defn\"\n :detail \"Create public function\"\n :snippet \"(defn ${1:name}\\n \\\"${2:docstring}\\\"\\n [${3:args}]\\n $0)\"}\n
A defn
form with private metatdata. Including name, doc-string and args tab-stops
{:name \"defn-\"\n :detail \"Create public function\"\n :snippet \"(defn ^:private ${1:name}\\n \\\"${2:docstring}\\\"\\n [${3:args}]\\n $0)\"}\n
A namespace form with name, doc-string and require tab-stop.
{:name \"ns\"\n :detail \"Create ns\"\n :snippet \"(ns ${1:name}\\n \\\"${2:docstring}\\\"\\n ${3:require})\"}\n
"},{"location":"clojure-editors/clojure-lsp/practicalli-snippets/#clojure-cli-aliases-and-library-dependencies","title":"Clojure CLI aliases and library dependencies","text":"Add Clojure CLI alias to deps.edn
, with an :extra-paths
and :extra-deps
section
{:name \"deps-alias\"\n :detail \"deps.edn alias with extra path & deps\"\n :snippet\n \":${1:category/name}\n {:extra-paths [\\\"${2:path}\\\"]\n :extra-deps {${3:deps-maven or deps-git}}}$0\"}\n
Add a Maven style dependency to a Clojure CLI deps.edn
project.
{:name \"deps-maven\"\n :detail \"deps.edn Maven dependency\"\n :snippet\n \"${1:domain/library-name} {:mvn/version \\\"${2:1.0.0}\\\"}$0\"}\n
Add a dependency from a Git repository, where the library is named after the remote Git repository, i.e io.github.user|org/library-name for the GitHub repository https://github.com/user|org/library-name
.
The :git/sha
defines a specific commit to use for the dependency.
{:name \"deps-git\"\n :detail \"deps.edn Git dependency\"\n :snippet\n \"${1:domain/library-name}\n {:git/sha \\\"${2:git-sha-value}\\\"}$0\"}\n
Additionally a Git tag can be specified, enabling the use of the short SHA value for :git/sha
(short sha is the first 7 characters of the 40 character SHA-1 value).
A Git client can obtain the short form of a SHA from a Git repository
git rev-parse --short 1e872b59013425b7c404a91d16119e8452b983f2\n
{:name \"deps-git-tag\"\n :detail \"Git dependency\"\n :snippet\n \"${1:domain/library-name}\n {:git/tag \\\"${2:git-tag-value}\\\"\n :git/sha \\\"${3:git-sha-value}\\\"}$0\"}\n
If a library is not named after the domain of the Git repository, the URL of the Git repository must be specified using the :git/url
key.
{:name \"deps-git-url\"\n :detail \"Git URL dependency\"\n :snippet\n \"${1:domain/library-name}\n {:git/url \\\"https://github.com/$1\\\"\n :git/sha \\\"${2:git-sha-value}\\\"}$0\"}\n
Add a library dependency that is a local Clojure project.
{:name \"deps-local\"\n :detail \"deps.edn Maven dependency\"\n :snippet\n \"${1:domain/library-name} {:local/root \\\"${2:/path/to/project/root}\\\"}$0\"}\n
"},{"location":"clojure-editors/clojure-lsp/practicalli-snippets/#require-library-dependencies","title":"Require Library Dependencies","text":"Require a library when using REPL driven development in a rich comment block, adding a (require ,,,)
form when evaluating the use of a library without forcing it to be loaded when loading the namespace.
{:name \"require-rdd\"\n :detail \"require for rich comment experiments\"\n :snippet \"(require '[${1:namespace} :as ${2:alias}]$3)$0\"}\n
A basic :require
expression for an ns
form.
{:name \"require\"\n :detail \"ns require\"\n :snippet \"(:require [${1:namespace}])$0\"}\n
A :require
expression for an ns
form, including a :as
directive to define an alias for the required namespace.
{:name \"require-as\"\n :detail \"ns require with :as alias\"\n :snippet \"(:require [${1:namespace} :as ${2:alias}]$3)$0\"}\n
A :require
expression for an ns
form, including a :refer
directive to include specific function definitions and vars by name.
{:name \"require-refer\"\n :detail \"ns require with :refer\"\n :snippet \"(:require [${1:namespace} :refer [$2]]$3)$0\"}\n
It is idiomatic to use require with refer to pull in specific functions and vars from another namespace. The use
function is not recommended as it can easily pull more transitive dependencies into the current namespace, causing unexpected results
{:name \"use\"\n :detail \"require refer preferred over use\"\n :snippet \"(:require [${1:namespace} :refer [$2]])$0\"}\n
"},{"location":"clojure-editors/clojure-lsp/practicalli-snippets/#clojuretest-snippets","title":"Clojure.test snippets","text":"When writing a deftest
, a new assertion written may be better in a new group. The testing
snippet will create a new testing
form and pull in the following assertion.
{:name \"deftest\"\n :detail \"deftest clojure.test\"\n :snippet\n \"(deftest ${1:name}-test\n (testing \\\"${2:Context of the test assertions}\\\"\n (is (= ${3:assertion-values}))$4))\n $0\"}\n
Create a new assertion group using the clojure.test/testing
form.
Using testing
before an assertion form pull that assertion into the group
{:name \"testing\"\n :detail \"testing clojure.test\"\n :snippet \"(testing \\\"${1:description-of-assertion-group}\\\"\\n $0)\"}\n
Define an is
assertion for a deftest
{:name \"is\"\n :detail \"assertion for clojure.test\"\n :snippet \"(is (= ${1:function call} ${2:expected result}))$0\"}\n
"},{"location":"clojure-editors/clojure-lsp/snippets/","title":"Clojure LSP Snippets","text":"Custom snippets are defined in the Clojure LSP EDN configuration using the :additional-snipets
key. The snippet body uses the same tab stop and placeholder syntax as Yasnipets, although the body is contained within a string.
Built-in snippets can include Clojure code for generating the text of the snippet when expanded. Custom snippets do not currently support evaluation of code in the snippet.
Clojure LSP Configuration locations Project specific configuration resides in .lsp/config.edn
User level configuration is either $XDG_CONFIG_HOME/clojure-lsp/config.edn
or $HOME/.clojure-lsp/config
"},{"location":"clojure-editors/clojure-lsp/snippets/#snippet-definition","title":"Snippet definition","text":"The :additional-snippets
key is associated with a vector or hash-maps, [{}{},,,]
with each hash-map defining a snippet using the keys:
:name
- name of the snippet, typed into the editor for completion
:detail
- a meaningful description of the snippet
:snippet
- the definition of the snippet, with tab stops and current-form syntax
The :snippet
can be any text, ideally with syntax that is correct for the particular language
"},{"location":"clojure-editors/clojure-lsp/snippets/#snippet-tab-stops","title":"Snippet Tab Stops","text":"Include $
with a number, e.g. $1
,$2
,$3
, to include tab stops in the snippet. Once the snippet code has been generated, TAB
key jumps through the tab stops in sequence, allowing customisation of a generic snippet.
$0
marks the final position of the cursor, after which TAB
has no more positions in the snippet to jump to.
"},{"location":"clojure-editors/clojure-lsp/snippets/#snippet-current-form","title":"Snippet current-form","text":"When a Clojure LSP snipped includes $current-form
then typing a snippet name in front of an existing Clojure form includes that form in the generated code.
{:additional-snippets [{:name \"wrap-let-sexpr\"\n :detail \"Wrap current sexpr in let\"\n :snippet \"(let [$1 $current-form] $0)\"}]}\n
Limited scope with current-form
A Snippet including $current-form
is only active when typed in front of an existing expression. A snippet is not recognised when typed at the top level.
"},{"location":"clojure-editors/clojure-lsp/snippets/#placeholders","title":"Placeholders","text":"Tab Stops can also include default values or text used as hint on what each tab stop value is for. These are referred to as placeholders.
${1:default-value}
is the form of a placeholder for tab stop 1. When the cursor tabs to tab stop 1, the default-value text is highlighted and replaces as soon as characters are typed.
Placeholder text is not replaced for $0
tab-stop, as the snippet interaction is effectively over at this point.
The deftest
custom snippet shows examples of placeholders for three tab stops.
{:name \"deftest\"\n :detail \"deftest clojure.test\"\n :snippet\n \"(deftest ${1:name}-test\n (testing \\\"${2:Context of the test assertions}\\\"\n (is (= ${3:assertion-values}))$4))\n $0\"}\n
Escape string quotes in snippet body
Use \\
character before the \"
character within the snippet body. For example, doc-strings in defn
function definitions or the string in testing
function.
"},{"location":"clojure-editors/clojure-lsp/snippets/#code-driven-snippet","title":"Code driven snippet","text":"The built-in defn
snippet uses Clojure code to help generate the snippet.
%s
is a substitution point within a snippet, used by the standard Clojure format
command, used to included either defn ^:private
or defn-
, depending on the value returned from the if
expression.
:use-metadata-for-privacy?
is a key from the Clojure LSP configuration
{:label \"defn-\"\n :detail \"Create private function\"\n :insert-text (format \"(defn%s ${1:name} [$2]\\n ${0:body})\"\n (if (:use-metadata-for-privacy? settings)\n \" ^:private\"\n \"-\"))}\n
The syntax for built-in snippets is slightly different that the :additional-syntax
form. The internal form uses :label
for :name
and :insert-text
for :snippet
.
Code supported only in built-in snippets
Clojure code only works for built-in snippets and not for :additional-snippets
.
Clojure LSP is compiled by Graal to a native binary, including the built-in snippets. To include Clojure code in a snippet then consider submitting a pull request to the Clojure LSP project to add a built-in snippet.
"},{"location":"clojure-spec/","title":"Clojure Specifications","text":"Clojure Spec is a library for defining specifications around data and functions to test for correctness.
A spec defines the expected shape of specific values in Clojure and specs are intended to be used across multiple projects. Specifications for more complex values are composed of specific value specifications, providing a flexible way to define what key parts of the system should look like.
Clojure specs are used to generate comprehensive test data, identifying more scenarios and edge cases with less code.
Spec is included in Clojure version 1.9 onward and can be used by requiring the clojure.spec.alpha
in the REPL or in namespaces of a Clojure project.
"},{"location":"clojure-spec/#recommended-reading","title":"Recommended Reading","text":"What is Clojure spec - an illustrated guide
"},{"location":"clojure-spec/#purpose-of-clojure-spec","title":"Purpose of Clojure spec","text":"A summary highlighting the common purposes that Clojure Spec is used for
Purpose Description Living documentation Use spec to include specifications in Function documentation (fdef
) Data Validation Ensure the data entering and leaving the system or its key functions are of the correct form Test Data Generation Provide extensive test data coverage with minimal code maintenance Generative testing of functions Test functions using their spec defined contract (fdef
) Generative scenario testing Specific correct usage paths for known states Development time checking Instrument functions to ensure correctness Derive code from specifications Specify a system of record for data structures, internal and external to the system."},{"location":"clojure-spec/#example-use-cases","title":"Example use cases","text":" - API requests (schema is often used here, but so can spec)
- Checking data pulled from / pushed to message systems (e.g. Kafka, TIBCO)
- Data specifications (eg. Vega-lite)
"},{"location":"clojure-spec/#example-code","title":"Example code","text":" practicalli/leveraging-spec
practicalli/leveraging-spec - basic examples of using spec, following the Practicalli Spec broadcasts
"},{"location":"clojure-spec/#understanding-the-basics-of-clojure-spec","title":"Understanding the basics of Clojure Spec","text":""},{"location":"clojure-spec/#trying-clojurespec","title":"Trying clojure.spec","text":"Follow the examples in these two excellent videos
"},{"location":"clojure-spec/#why-is-the-spec-library-called-alpha","title":"Why is the spec library called alpha?","text":"The library is called clojure.spec.alpha
as the design of spec is still evolving and there may be some changes to the design in later versions. Clojure aims for backwards compatibility, so new versions typically do not break existing use of libraries.
There are some important changes being developed for spec version 2 and a few things may change, however, the large majority of Spec will remain the same and is safe to use.
"},{"location":"clojure-spec/#references","title":"References","text":"spec guide - clojure.org Introducing clojure.spec clojure.spec - rational and overview
spec.alpha API reference
How do you use clojure.spec - Sean Corfield
Specifications for clojure.core
Leveraging clojure.spec - Stuart Halloway spec.test - Stuart Halloway
Clojure Spec: Expressing Data Constraints without Types
"},{"location":"clojure-spec/add-spec-to-projects/","title":"Add Spec to a project","text":"Create a new project or clone practicalli/leveraging-spec which includes several examples of using Clojure Spec.
Create new projectClone Practicalli Leveraging Spec project Create a new Clojure CLI project using the Practicalli project templates
Create new projectclojure -T:project/create :template practicalli/minimal :name practicalli/leveraging-spec\n
Practicalli Clojure CLI Config - :project/create alias Practicalli Clojure CLI Config repository includes the :project/create
alias for creating new Clojure projects from a template using deps-new
.
The project is created with Clojure as a dependency, which includes the clojure.spec.alpha
library.
Clojure 1.9.0 or higher versions include Clojure Spec. Clojure 1.11.1 is recommended.
practicalli/leveraging-spec project includes Clojure Spec examples for values and functional arguments, along with unit tests using clojure spect-test.
https://github.com/practicalli/leveraging-spec.git\n
"},{"location":"clojure-spec/add-spec-to-projects/#project-dependencies","title":"Project Dependencies","text":"Clojure Spec is included in Clojure so only org.clojure/clojure
dependency is required in the deps.edn
file for the project.
Clojure project dependency
deps.edn{:paths [\"src\" \"resources\"]\n :deps {org.clojure/clojure {:mvn/version \"1.11.1\"}}}\n
"},{"location":"clojure-spec/add-spec-to-projects/#use-spec-in-namespace","title":"Use spec in namespace","text":"Require the clojure.spec.alpha
namespace in the ns
definition using the spec
alias name. Practicalli recommends using spec
(rather than s
as it is much clearer as to where the functions are defined)
Clojure project dependency
(ns practicalli.leveraging-spec\n (:require [clojure.spec.alpha :as spec]))\n
Evaluate the namespace definition to start using the functions from the namespace.
All functions from clojure.spec.alpha
are now accessible using the spec
alias, e.g. spec/conform
, spec/valid?
, spec/def
.
"},{"location":"clojure-spec/add-spec-to-projects/#testing-with-spec","title":"Testing with spec","text":"Add the clojure.spec.test.alpha
using the spec-test
alias, along with clojure.spec.test.alpha
as spec
alias
Clojure project dependency
src/practicalli/leveraging_spec.clj(ns practicalli.leveraging-spec\n (:require\n [clojure.spec.alpha :as spec]\n [clojure.spec.test.alpha :as spec-test]))\n
"},{"location":"clojure-spec/organising-specs/","title":"Organizing Specifications","text":"Data and function definition specifications are typically placed in a dedicated specification
namespaces, e.g src/domain/project/specification.clj
.
Add the data specifications (spec/def
), custom predicate functions and function specifications (spec/fdef
) to the specifications
namespace.
Specifications for an architecture layer can be organised next to the namespaces managing the layer, e.g. database.
Migrate specifications to a library once they are applicable to multiple projects.
"},{"location":"clojure-spec/organising-specs/#instrumenting-functions","title":"Instrumenting functions","text":"Add spec-test/instrument
expressions to the specifications
file, after the spec/fdef
expressions.
Rather than create individual expressions, create a clojure.core/def
to contain a collection of all the spec/fdef
expressions. This list can then be used to instrument
and unstrument
all the spec/fdef
specifications.
(def ^:private function-specifications\n [`card-game/deal-cards\n `card-game/winning-player])\n
Write simple helper functions to wrap the instrumenting of function specifications
(defn instrument-all-functions\n []\n (spec-test/instrument function-specifications))\n\n(defn unstrument-all-functions\n []\n (spec-test/unstrument function-specifications))\n
"},{"location":"clojure-spec/organising-specs/#unit-testing","title":"Unit testing","text":"Specifications can be incorporated into the existing unit tests, so it is sensible to keep them under the corresponding test
directory files.
"},{"location":"clojure-spec/organising-specs/#generative-testing","title":"Generative testing","text":"Using spec-test/check
will generate 1000 data values for each expression, so by default these tests will take far longer that other tests.
Configuring generative tests to only generate a small number of values will make spec-test/check
expressions return almost instantaneously. In this example, only 10 data values are generated
(spec-test/check `deal-cards\n {:clojure.spec.test.check/opts {:num-tests 10}})\n
Generative testing with small generators can be run regularly during development without impacting fast feedback.
Testing with Clojure Spec
"},{"location":"clojure-spec/using-spec-in-the-repl/","title":"REPL Experiments with Clojure Spec","text":"Create a minimal Project Clojure Spec can be tried without creating a Clojure project, although creating a project is useful if saving the Clojure Spec code experiments.
Create a minimal Clojure project with a Clojure CLI deps.edn configuration.
clojure -T:project/create :template practicalli/minimal :name practicalli/spec-experiments\n
Run a Clojure REPL with a rich terminal UI
REPL RebelREPL Reloaded A REPL with a rich terminal UI
clojure -M:repl/rebel\n
A REPL with a rich terminal UI and tools to support the Practicalli REPL Reloaded workflow.
clojure -M:repl/reloaded\n
Require the clojure.spec.alpha
using an alias called spec
to use functions from that namespace.
(require '[clojure.spec.alpha :as spec])\n
NOTE: clojure.spec.alpha
is often aliased as s
, although Practicalli avoids
"},{"location":"clojure-spec/using-spec-in-the-repl/#spec-auto-completion","title":"Spec auto-completion","text":"Using rebel-readline for the Clojure REPL will show autocompletion for all spec functions once the spec namespace has been required.
Type (spec /
and press TAB
to list all the functions in the namespace.
Typing a space character after the full name of a function shows the function signature with arguments that should be passed to that function.
Ctrl x Ctrl d displays the documentation for the current function
"},{"location":"clojure-spec/using-spec-in-the-repl/#check-data-conforms-to-specification","title":"Check data conforms to specification","text":"Use the spec/conform
and spec/valid?
functions to test if data matches a specification. In these examples, predicate functions are used as a specification.
"},{"location":"clojure-spec/using-spec-in-the-repl/#example-expressions","title":"Example expressions","text":"spec/conform
will return the value if it conforms to the specification, or :clojure.spec.alpha/invalid
if the data does not conform.
Clojure Spec - Conform values
(spec/conform odd? 101)\n\n(spec/conform integer? 1)\n\n(spec/conform seq? [1 2 3])\n\n(spec/conform seq? (range 10))\n\n(spec/conform map? {})\n\n(spec/conform map? (hash-map :a 1 :b 2))\n
spec/valid?
returns true or false
Clojure Spec - validate values
(spec/valid? even? 180)\n\n(spec/valid? string? \"Am I a valid string\")\n\n(spec/valid? (fn [value] (> value 10000)) 30076)\n\n(spec/valid? #(> % 10000) 30076)\n\n(spec/conform #(> % 10000) 30076)\n
"},{"location":"clojure-spec/data/","title":"Clojure Spec for data","text":"Specifications can be defined for any data in Clojure, be that simple values or complex data structures. More complex specifications are composed of individual specifications, providing a flexible way to define specifications without building a brittle hierarchy.
"},{"location":"clojure-spec/data/#what-is-a-specification","title":"What is a specification","text":"Specifications can be predicates (return true or false), literal values in sets and entity maps.
There are many predicate functions that come with Clojure which help speed the creation of specifications. Clojure function definitions (fn
, defn
) can be used to define custom predicate functions too.
"},{"location":"clojure-spec/data/#do-values-meet-a-specification","title":"Do values meet a specification","text":"The functions use to compare data with a specification are:
conform
- test if data conforms to a specification, returning the conformed value valid?
- predicate to test if data conforms to a specification, returning true of false explain
- explain why a value is not conforming to a specification
There are variations on explain, that present the results in different formats.
"},{"location":"clojure-spec/data/#workflow-for-data-specifications","title":"Workflow for data specifications","text":"Using Clojure Specification is very flexible, they can be used as much or as little as required.
Typically Specifications are created when data structures are being modeled, which can be done via experimenting in the REPL or taking a test first approach. Either way is viable.
The generative tests section shows how specifications are used to generate mock data, so creating specifications earlier on in the development process will provide a wider range of data for unit tests and repl experimentation.
Spec and nil values
Some predicates do not consider nil
as a valid value, espcially those predicates that check for a specific type
spec/nilable
will transform a predicate to use nil
(spec/valid? string? nil)\n\n(type \"what type am I\")\n(type nil)\n\n(spec/valid? (spec/nilable string?) nil)\n
"},{"location":"clojure-spec/data/and-or-specifications/","title":"Combining specifications with and and or","text":"clojure.core/and
function and clojure.core/or
function can be used to define a specification with multiple parts.
"},{"location":"clojure-spec/data/and-or-specifications/#conform-to-one-or-more-specifications","title":"Conform to One or more specifications","text":"A specification for residential address included either a house number or name. The clojure.core/or
function allows either type of value to be used and conform to the specification.
(spec/def ::house-name-number (or string? int?))\n
Using spec/or
then unique keys are required for each possible type of value. Keys are used to explain where a failure occurred if values do not conform to the specification.
(spec/def ::house-name-number (spec/or :string string?\n :number int?))\n
If specifications are uses as the options in the clojure.spec.alpha/or
then those specification names are used as the keys to explain where failure to conform to the specification happened.
(spec/def ::social-security-id (spec/or ::social-security-id-uk\n ::social-security-id-usa))\n
"},{"location":"clojure-spec/data/and-or-specifications/#conform-to-all-specifications","title":"Conform to all specifications","text":"Create a composite specification using clojure.spec.alpha/and
when all specifications should be conformed by the values
For an arranged banking overdraft limit, the value should be a positive number, that is an integer type and is less than 1000.
(spec/def ::arranged-overdraft-limit (spec/and pos? int? #(> 1000 %)))\n
If a value does not conform to any of the three specifications then the value fails the ::arranged-overdraft-limit
specification.
"},{"location":"clojure-spec/data/composite-specifications/","title":"Composing Specifications","text":"No spec is an island
Composing individual specifications is an effective way to build larger abstractions in specifications without creating fixed hierarchical structures that are harder to refactor.
Require specification namespace to the page
(ns practicalli.clojure\n (:require [clojure.spec.alpha :as spec]))\n
spec/and
is used when all specifications should be true.
(spec/def ::meaning-of-life\n (spec/and int?\n even?\n #(= 42 %)))\n
spec/or
is use when one or more specifications should be true
(spec/def ::meaning-of-life-int-or-string\n (spec/or :integer #(= 42 %)\n :string #(= \"forty two\" %)))\n
Each condition in the spec is annotated with a label for each conditional branches.
Labels are included in the return result from spec/explain
when values do not conform to a specification, providing context as to why a value failed the specification.
When an or is conformed, it returns a vector with the condition name and conformed value.
(spec/conform ::meaning-of-life-int-or-string 42)\n
(spec/conform ::meaning-of-life-int-or-string \"forty two\")\n
(spec/conform ::meaning-of-life-int-or-string :entropy)\n
(spec/explain ::meaning-of-life-int-or-string :entropy)\n
"},{"location":"clojure-spec/data/composite-specifications/#individual-specifications","title":"Individual specifications","text":"Before composing a more abstract specification, first define individual specifications
(spec/def ::first-name string?)\n
(spec/def ::last-name string?)\n
(spec/def ::residential-address string?)\n
"},{"location":"clojure-spec/data/composite-specifications/#composing-hash-map-specification","title":"Composing hash-map specification","text":"The individual specifications can now be composed into a single specification.
keys
function combines specifications to form a composite specification in the form of a Clojure hash-map.
(spec/def ::customer-details\n (spec/keys\n :req [::first-name ::last-name ::residential-address]))\n
Use the composite specification with a value
(spec/conform ::customer-details\n {::first-name \"Jenny\"\n ::last-name \"Jetpack\"\n ::residential-address \"42 meaning of life street, Earth\"})\n
"},{"location":"clojure-spec/data/conform/","title":"Conform","text":""},{"location":"clojure-spec/data/conform/#does-a-value-conform-to-a-specification","title":"Does a value conform to a specification?","text":"clojure.spec.alpha/conform
takes two arguments
- a specification
- a value to test against the specification
:clojure.spec.alpha/invalid
is returned when a value does not conform to a specification.
If the value does conform to the specification, then the value is returned. This value is referred to as a conformed value.
"},{"location":"clojure-spec/data/conform/#require-the-clojure-spec-library","title":"Require the Clojure spec library","text":"Set the namespace for the page and require clojure.spec.alpha library, setting the alias to spec
(ns practicalli.clojure.specifications\n (:require [clojure.spec.alpha :as spec]))\n
"},{"location":"clojure-spec/data/conform/#using-conform","title":"Using conform","text":"If the value conforms to the spec, a conformed value is returned
(spec/conform odd? 101)\n
When a value does not conform to a spec, the value :clojure.spec.alpha/invalid
is returned
(spec/conform even? 101)\n
(spec/conform integer? 1)\n
(spec/conform seq? [1 2 3])\n
(spec/conform seq? (range 10))\n
(spec/conform map? {})\n
(spec/conform map? (hash-map :a 1 :b 2))\n
"},{"location":"clojure-spec/data/defining-specifications/","title":"Defining specifications","text":"clojure.spec.alpha/def
binds a name to a specification, just like clojure.core/def
binds a name to a value.
Binding a name means specifications are available throughout the code and in other projects if the project is included as a library.
"},{"location":"clojure-spec/data/defining-specifications/#naming-fully-qualified-keywords","title":"Naming - fully qualified keywords","text":"Specification names should use fully qualified keywords, typically using the namespace in which the specification is defined in.
Define a namespace for the page and require Clojure Spec
(ns practicalli.clojure.specifications\n (:require [clojure.spec.alpha :as spec]))\n
(spec/def :practicalli.clojure.specifications/number number?)\n
"},{"location":"clojure-spec/data/defining-specifications/#auto-resolve-macro","title":"auto-resolve macro","text":"::
double colon is the auto-resolve macro, which will pre-pend the current namespace to the specification keyword. The ::
notation removes the need to edit fully qualified names should a specification be moved to a different namespace.
(spec/def ::number number?)\n
Fully Qualified keywords
Using fully qualified keywords ensures they are unique and therefore can be used across all projects.
Namespaces are usually unique as they include the name of the company or organization behind the code and any project or component names used to organize the code.
"},{"location":"clojure-spec/data/entity-maps/","title":"Entity maps","text":"An entity map is a Specification for a Clojure hash-map of key-value pairs.
A hash-map is a very effective way to express information in Clojure. The key should be a descriptive label to express meaning of the value it is associated with. Without the keys describing the meaning, it is harder for a developer to understand the data.
{:account-id 12345 :user-name \"jenny1999\" :name \"Jenny Jetpack\" :address \"42 Meaning place, Earth\" :social-security \"ABC-123-45-6789\"}\n
A hash-map contains any number of key-value pairs, keys are used for efficient lookup so there is no concern over ordering. Passing a hash-map as an argument to a function reduces refactoring required as the signature of the function remains the same and functions can be selective as to which key-value pairs they use.
For these reasons, hash-maps are a very common data structure to pass data between functions.
"},{"location":"clojure-spec/data/entity-maps/#defining-entity-maps","title":"Defining entity maps","text":"The Clojure Spec keys
function is used to create a specification for a hash-map of key-value pairs.
keys
creates a specification from required keys, :req
, and optional keys :opt
.
To define the specification for a player in an online game, first the individual specifications that make up the player hash-map are created.
(spec/def ::account-id uuid?)\n(spec/def ::name string?)\n(spec/def ::score int?)\n(spec/def ::profile string?)\n(spec/def ::games-played #{:vectron :utrazap :avakato})\n
Those specifications are composed together to create a specification for the player
(spec/def\n ::player-account\n (spec/keys :req [::account-id ::name ::score]\n :opt [::profile ::games-played]))\n
For a hash-map to meet the ::player-account
specification it must contain the :req
keys with values that conform to the individual specifications. The hash-map can also include any key-value pairs that conform to the :opt
specifications.
If any keys are in the map that do not appear in either :req
or :opt
then that hash-map does not conform to the ::player-account
specification.
"},{"location":"clojure-spec/data/entity-maps/#example-covid19-dashboard","title":"Example: covid19-dashboard","text":"The coronavirus-cases-data
function takes a hash-map of values to make that function easier to extend without breaking existing calls
Default values can be used if no value is passed as an argument. Extra values can be ignored without breaking the code.
Coronavirus Cases Specification
(defn fun-name\n [csv location date])\n\n(defn coronavirus-cases-data\n \"Extract and transform cases data for specific locations and date\"\n [{:keys [csv-file locations date]}]\n #_(-> (extract-data-from-csv csv-file)\n (data-set-remove-locations locations)\n (data-set-specific-date date)))\n\n(coronavirus-cases-data\n {:csv-file \"data-sets/uk-coronavirus-cases.csv\"\n :locations #{\"Nation\" \"Country\" \"Region\"}\n :date \"2020-04-30\"})\n\n;; Define the individual keys for the hash-map\n(spec/def ::csv-file string?)\n(spec/def ::locations set?)\n(spec/def ::date string?)\n\n(spec/def ::cases-data\n (spec/keys :req [::csv-file ::locations ::date]))\n
"},{"location":"clojure-spec/data/explain/","title":"Explaining non-conforming values","text":"clojure.spec.alpha/explain
describes why a value does not satisfy a specification.
clojure.spec.alpha/explain
takes two arguments
- a specification
- a value to test against the specification
Success
string is sent to standard out if the value meets the specification
A string explaining where the value deviates from the specification is sent to standard out if the value does not meet the specification.
There are several variations on the explain function for different situations
explain
- sends the return value to the standard out / REPL explain-str
- returns a human readable result. explain-data
- returns a data structure of the error to be processed by other code
"},{"location":"clojure-spec/data/explain/#example-of-a-failing-value","title":"Example of a failing value","text":"First define a namespace and require the Clojure Spec namespace
(ns practicalli.clojure.specifications\n (:require [clojure.spec.alpha :as spec]))\n\n(spec/def ::meaning-of-life #(= 42 %))\n
Given the following specification
(spec/explain ::meaning-of-life 24)\n
Using the value 24
with that specification will fail. Using explain we can see why
(spec/def ::meaning-of-life-int-or-string\n (spec/or :integer #(= 42 %)\n :string #(= \"forty two\" %)))\n
In this case explain returned the
- value being checked against the spec
- result of that check (failed)
- predicate used to check the value
- spec name used to check the value
Notice that the value failed on the first condition, :integer
, then stopped without checking the second, :string
. The spec/and
macro works the same as clojure.core/and
in that is stops as soon as something fails.
(spec/explain ::meaning-of-life-int-or-string 24)\n
In this case we still have the value checked, the result and the predicate More information is provided as to where in the spec the value failed :at
shows the path in the spec where the failure occurred, very useful for nested structures This shows the value of naming your specs descriptively
"},{"location":"clojure-spec/data/explain/#explain-with-a-string","title":"Explain with a string","text":"rather than send information to the system out
(spec/explain-str ::meaning-of-life 24)\n
(spec/explain-data ::meaning-of-life 24)\n
"},{"location":"clojure-spec/data/hierarchical-specifications/","title":"Hierarchical Specifications","text":"Defining specifications for data that is hierarchical or nested in nature.
(ns practicalli.clojure\n (:require [clojure.spec.alpha :as spec]))\n
"},{"location":"clojure-spec/data/hierarchical-specifications/#example-hierarchical-data","title":"Example hierarchical data","text":"{:top-level-key {:nested-key \"value\"}}\n
"},{"location":"clojure-spec/data/hierarchical-specifications/#individual-specifications","title":"Individual specifications","text":"(spec/def ::first-name string?)\n
(spec/def ::last-name string?)\n
(spec/def ::residential-address string?)\n
"},{"location":"clojure-spec/data/hierarchical-specifications/#composite-specification","title":"Composite Specification","text":"keys
function combines specifications to form a composite specification in the form of a Clojure hash-map.
(spec/def ::customer-details\n (spec/keys\n :req [::first-name ::last-name ::residential-address]))\n
"},{"location":"clojure-spec/data/hierarchical-specifications/#hierarchical-specification","title":"Hierarchical Specification","text":"A user account is composed of a user-id and customer details. Rather than include the individual customer details, the composite customer-details specification.
The ::user-id
specification is as follows
(spec/def ::user-id uuid?)\n
The ::user-account
specification
(spec/def ::user-account\n (spec/keys\n :req [::user-id ::customer-details]))\n
The following data structure will conform to the specification
{::user-id #uuid \"97bda55b-6175-4c39-9e04-7c0205c709dc\"\n ::customer-details {::first-name \"Jenny\"\n ::last-name \"Jetpack\"\n ::residential-address \"Earth\"}}\n
"},{"location":"clojure-spec/data/literal-values/","title":"Literal values","text":"Sets can be used as predicate functions returning true if the value is within the set
Checking valid playing cards
Define a namespace for the page and require Clojure Spec
(ns practicalli.clojure\n (:require [clojure.spec.alpha :as spec]))\n
(spec/valid? #{:club :diamond :heart :spade} :club)\n
(spec/valid? #{:club :diamond :heart :spade} 42)\n
Answer to the ultimate question?
(spec/valid? #{42} 42)\n
Using sets for literal values is similar to using the clojure.core/contains?
function with a set collection type.
(contains? #{:clubs :diamonds :hearts :spades} :hearts )\n
"},{"location":"clojure-spec/data/map-literals/","title":"Map literal syntax - #:
and #::
","text":"#:
map literal macro for Clojure hash-maps adds a given namespace to all the keywords contained in the hash-map.
#::
map literal macro for keyword auto-resolve adds the current fully qualified namespace to all the keywords in the hash-map
"},{"location":"clojure-spec/data/map-literals/#require-clojure-spec-in-the-namespace-definition","title":"Require clojure spec in the namespace definition","text":"(ns practicalli.clojure\n (:require [clojure.spec.alpha :as spec]))\n
In this example the keys in the map are unqualified.
{:simplifying []\n :keyword-names []\n :with-autoresolve []\n :map-literal []}\n
"},{"location":"clojure-spec/data/map-literals/#qualifying-keys-with-auto-resolve","title":"Qualifying keys with auto-resolve","text":"Using the map literal macro for auto-resolve instructs Clojure to treat all keys in the map as qualified to the current namespace
The following hash-map has the map literal macro.
#::{:simplifying []\n :keyword-names []\n :with-autoresolve []\n :map-literal []}\n
This is the same as explicitly writing out the fully qualified domain for each key in the map.
However, if we move the map to another namespace, then the explicit namespaces would need to be updated.
{:practicalli.clojure/simplifying []\n :practicalli.clojure/keyword-names []\n :practicalli.clojure/with-autoresolve []\n :practicalli.clojure/map-literal []}\n
"},{"location":"clojure-spec/data/map-literals/#qualifying-keywords-with-a-specific-name","title":"Qualifying keywords with a specific name","text":"Rather than take the name from the current namespace, an explicit name can be added to all the keys in the map
#:practicalli.naming {:simplifying []\n :keyword-names []\n :with-autoresolve []\n :map-literal []}\n
This is the same as explicitly writing that name in front of each of the keywords in the map.
# {:practicalli.naming/simplifying []\n :practicalli.naming/keyword-names []\n :practicalli.naming/with-autoresolve []\n :practicalli.naming/map-literal []}\n
Map literals are relevant to Entity maps with spec.
"},{"location":"clojure-spec/data/predicate-functions/","title":"Spec - Predicate functions","text":"A predicate is a function that returns a true or false value and their names end with ?
by convention.
(odd? 1)\n
(string? \"am i a string\")\n
(int? 2.3)\n
(int? 2.3)\n
clojure.core
predicate functions
clojure.core
defines 80+ predicate functions
"},{"location":"clojure-spec/data/predicate-functions/#predicate-functions-in-specs","title":"Predicate functions in specs","text":"Predicate functions can be used as un-named specifications to test values conform.
Include the clojure.spec.alpha
namespace to access the spec functions.
(require '[clojure.spec.alpha :as spec])\n
(spec/conform int? 42)\n
(spec/conform seq? (range 4))\n
"},{"location":"clojure-spec/data/predicate-functions/#custom-predicate-functions","title":"Custom predicate functions","text":"Define custom predicate functions with defn
or fn
or the short form #()
Using an anonymous function
(spec/conform (fn [value] (= value 42)) 42)\n
When the expression is quite terse, then the short form of an anonymous function is typically used. The %
represents the value passed as an argument.
(spec/conform #(= % 42) 42)\n
"},{"location":"clojure-spec/data/registry/","title":"Registry for unique and re-usable specifications","text":"So far we have just use predicate functions directly in the code examples.
Using a registry, specs can be uniquely defined across the whole project. Defining a spec gives that spec a name that has a fully qualified namespace
Use the spec specific def
function to bind a new spec name and fully qualified namespace and place it in the registry
(spec/def :playing-card/suit #{:club :diamond :heart :spade} )\n
(spec/conform :playing-card/suit :diamond)\n
(spec/def :show-cats/cat-bread #{:abyssinian :birman :chartreau :devon-rex\n :domestic-short-hair :domestic-long-hair})\n
"},{"location":"clojure-spec/data/registry/#removing-specs-from-the-registry","title":"Removing specs from the registry","text":"Named specifications can be removed from the registry by binding the name to nil
.
If specification names are to be refactored, then the original name should be set to nil
and evaluated, before changing the name. This will ensure stale specifications are not residing in the REPL.
Here is a named specification as an example
(spec/def ::unwanted #{:abandoned})\n
The specification is evaluated in the REPL (above) and currently works.
(spec/conform ::unwanted :abandoned)\n
Remove this specification from the registry by binding it to nil
(spec/def ::unwanted nil)\n
Now the specification is unavailable
(spec/conform ::unwanted :abandoned)\n
Registry not persistent
Restarting the REPL will loose all specification names in the registry as it is not persistent across REPL sessions.
"},{"location":"clojure-spec/data/valid-q/","title":"Is the value valid?","text":"clojure.spec.alpha/valid?
takes two arguments
- a specification
- a value to test against the specification
clojure.spec.alpha/valid?
is a predicate function.
true
is returned if the value meets the specification, otherwise false
is returned.
"},{"location":"clojure-spec/data/valid-q/#require-the-clojure-spec-library","title":"Require the Clojure spec library","text":"Set the namespace for the page and require clojure.spec.alpha library, setting the alias to spec
(ns practicalli.clojure.specifications\n (:require [clojure.spec.alpha :as spec]))\n
"},{"location":"clojure-spec/data/valid-q/#using-valid","title":"Using valid?","text":"If the value is valid then a boolean true is returned. Experiment with different values and predicate functions.
(spec/valid? even? 180)\n
(spec/valid? string? \"Am I a valid string\")\n
"},{"location":"clojure-spec/data/valid-q/#using-custom-predicate-functions","title":"using custom predicate functions","text":"Create fn
definitions to use as predicate functions. Any function that returns true or false can be used.
(spec/valid? (fn [value] (> value 1024)) 8080)\n
The custom predicate function may also be written in the shorter form of a fn
definition
(spec/valid? #(> % 1024) 8080)\n
Use def
to bind names to custom predicate functions if they are used more than once in the code base.
In this example a name is bound to a function that checks if a port is within the range of IANA registered networking ports.
(def registered-port-range?\n \"Network port number within IANA registered port range\"\n #(and (> % 1024) #(< % 49151) )\n\n(spec/valid? registered-port-range? 8080)\n
"},{"location":"clojure-spec/functions/","title":"Specification for function definitions","text":"Define specifications for your custom functions
- Additional documentation - argument and return values and the relationship between them.
- Instrumenting functions - checking for correct argument values
- Generative testing - using argument specifications to generate comprehensive test data.
Many of the functions in clojure.core
have specifications in the latest version of Clojure. The specifications for clojure.core functions can be found in the clojure/core.specs.alpha repository on GitHub.
"},{"location":"clojure-spec/functions/#clojurecore-examples","title":"clojure.core examples","text":"Specifications used for the defn
, defn-
, fn
functions in clojure.core
clojure.core specification examples
(s/def ::param-list\n (s/and\n vector?\n (s/cat :params (s/* ::binding-form)\n :var-params (s/? (s/cat :ampersand #{'&} :var-form ::binding-form)))))\n\n(s/def ::params+body\n (s/cat :params ::param-list\n :body (s/alt :prepost+body (s/cat :prepost map?\n :body (s/+ any?))\n :body (s/* any?))))\n\n(s/def ::defn-args\n (s/cat :fn-name simple-symbol?\n :docstring (s/? string?)\n :meta (s/? map?)\n :fn-tail (s/alt :arity-1 ::params+body\n :arity-n (s/cat :bodies (s/+ (s/spec ::params+body))\n :attr-map (s/? map?)))))\n
"},{"location":"clojure-spec/functions/documentation/","title":"Documentation","text":"The Clojure doc
function shows the doc string included in a function definition, eg. defn
expressions.
When a specification is defined for a function using fdef
the specification is included in the output of the Clojure doc
function.
Including specification details clarifies the precise way to use the function and the information it expects. When a function has a specification the doc string for that function can focus on the purpose of the function rather than the specific types of data used, as that is covered by the function specification.
"},{"location":"clojure-spec/functions/documentation/#example","title":"Example","text":"(clojure.repl/doc ::rank)\n\n;; :practicalli.card-game-specifications/rank\n;; Spec\n;; (into #{:king :queen :ace :jack} (range 2 11))\n
When adding a specification to a function definition, doc
will also show the specification details along with the function doc-string.
"},{"location":"clojure-spec/functions/documentation/#live-example","title":"Live example","text":"Define the namespace and include clojure spec and clojure.repl (which contains the doc function)
(ns practicalli.clojure\n (:require [clojure.repl :as repl]\n [clojure.spec.alpha :as spec]))\n
Print the documentation for the map
function
(repl/doc map)\n
Print the documentation for the :playing-card/suit
(clojure.repl/doc :playing-card/suit)\n
#{:spade :heart :diamond :club}\n
(repl/doc :cat-show:cat-bread)\n
"},{"location":"clojure-spec/functions/function-definition-specifications/","title":"Function definition specifications","text":"clojure.spec.alpha/fdef
defines a specification for a function definition, providing specific specification for
- arguments passed when calling a function
- return value expected
- relationships between arguments and return value
"},{"location":"clojure-spec/functions/function-definition-specifications/#examples","title":"Examples","text":"The practicalli.database-access/new-account-holder
function takes a customer details specification and returns an account-holder-id
specification.
practicalli.database-access/new-account-holder
(spec/fdef practicalli.database-access/new-account-holder\n :args (spec/cat :customer ::customer-details)\n :ret ::account-holder-id)\n
"},{"location":"clojure-spec/functions/higher-order-functions/","title":"Higher order functions","text":"Higher order functions are common in Clojure and spec provides fspec to support spec\u2019ing them.
(defn value-added-tax\n [tax-rate]\n #(+ (* tax-rate %) %))\n
The value-added-tax function returns an anonymous function that adds the value of tax to the given value.
Define a namespace for the page and require Clojure Spec
(ns practicalli.clojure\n (:require [clojure.spec.alpha :as spec]))\n
Declare a function spec for value-added-tax using clojure.spec.alpha/fspec
for the return value:
(s/fdef value-added-tax\n :args (spec/cat :tax-rate number?)\n :ret (spec/fspec :args (s/cat :value number?)\n :ret number?))\n
The :ret
specification uses fspec
to declare that the returning function takes and returns a number.
"},{"location":"clojure-spec/generative-testing/","title":"Generative testing with Spec and Spec Test","text":"Clojure spec has been used so far to create specifications for both data and functions.
Now spec and spec test libraries are used not just validity checking, but also to generate random samples of the data that can be used for extensive testing.
Generative testing provides a far more effective alternative to unit testing.
clojure.spec.test/check/instrument
verifies that calls to a function satisfy the function's specification, the :arg
in fdef
.
clojure.spec.test/check
function generates 1000 data values to be used as the inputs to a function, checks that the invocation of the function satisfies its specification, the :ret
and :fn
in fdef
. The argument specification, :arg
in fdef
is used to generate a wide range of results, which are more capable of finding edge cases that fail.
"},{"location":"clojure-spec/generative-testing/#example-card-game","title":"Example: card game","text":"practicalli/spec-generative-testing is a simple card game with specifications that are used for basic generative testing.
"},{"location":"clojure-spec/generative-testing/#references","title":"References","text":" - Clojure.org guides: Spec - Generators
- API reference: clojure.spec.gen.alpha
- API reference: clojure.spec.test.alpha
- Video: How to do Stateful Property Testing in Clojure?
"},{"location":"clojure-spec/generative-testing/predicate-generators/","title":"Generators for predicate specifications","text":"Specifications are used to generate a wide range of random data. A generator for the specification is obtained and then data is generated.
"},{"location":"clojure-spec/generative-testing/predicate-generators/#predicate-generators","title":"Predicate generators","text":"(spec-gen/generate (spec/gen int?))\n
(spec-gen/generate (spec/gen nil?))\n
(spec-gen/sample (spec/gen string?))\n
(spec-gen/generate (spec/gen #{:club :diamond :heart :spade}))\n
(spec-gen/sample (spec/gen #{:club :diamond :heart :spade}))\n
"},{"location":"clojure-spec/generative-testing/example-projects/","title":"Example projects using Clojure Spec","text":"Project How the project uses Spec seancorfield/next-jdbc Data specifications using predicates, function definition argument specifications More examples welcome
Other example projects that use interesting features of Spec are most welcome. Raise an issue on the project issue tracker with details.
"},{"location":"clojure-spec/generative-testing/example-projects/next-jdbc/","title":"Projects using Clojure spec - next-jdbc","text":"The next-jdbc project is a modern low-level Clojure wrapper for JDBC-based access to databases.
The project defines data specifications using predicates and
"},{"location":"clojure-spec/generative-testing/example-projects/next-jdbc/#defining-specifications","title":"Defining specifications","text":"Specifications are defined within a single file src/next/jdbc/specs.clj
.
Specifications start with clojure.spec.alpha/def
expressions, using predicate functions as specifications. There is also a custom predicate function called
Function definition specifications follow, using the clojure.spec.alpha/fdef
function. The fdef
functions define the specification for the arguments of each function. The fdef
function name is the same as the function definition it is defining a specification for.
"},{"location":"clojure-spec/generative-testing/example-projects/next-jdbc/#instrumenting-specifications","title":"Instrumenting specifications","text":"Instrumenting functions provides automatic checking that argument in a function call conforms to the specification.
Rather than write individual expressions to instrument each function, a var called fns-with-specs
contains a collection of names for all the fdef
function definition specifications.
(def ^:private fns-with-specs\n [`jdbc/get-datasource\n `jdbc/get-connection\n `jdbc/prepare\n `jdbc/plan\n `jdbc/execute!\n `jdbc/execute-one!\n `jdbc/transact\n `jdbc/with-transaction\n `connection/->pool\n `prepare/execute-batch!\n `prepare/set-parameters\n `prepare/statement\n `sql/insert!\n `sql/insert-multi!\n `sql/query\n `sql/find-by-keys\n `sql/get-by-id\n `sql/update!\n `sql/delete!])\n
Instrument all the functions by passing fns-with-specs
as an argument to the clojure.spec.test.alpha/instrument
function.
This call is wrapped in a simple handler function for convenience.
(defn instrument []\n (clojure.spec.test.alpha/instrument fns-with-specs))\n
To remove the checking of argument specifications, clojure.spec.test.alpha/unstrument
is passed fns-with-specs
, again wrapped in a convinced function.
(defn unstrument []\n (clojure.spec.test.alpha/unstrument fns-with-specs))\n
"},{"location":"clojure-spec/projects/","title":"Clojure Spec Projects","text":"A series of projects showing approaches to using specifications and generating data for testing.
Project Description card game writing specifications and generating data from those specifications banking-on-clojure simplified online bank account using TDD, data and functional specifications and generative testing Bonbon card game A flavorful card game with clojure spec Genetic Programming With clojure.spec https://www.youtube.com/watch?v=xvk-Gnydn54 ClojureScript game with spec"},{"location":"clojure-spec/projects/#references","title":"References","text":" - Practicalli - Clojure Spec playlist - live coding to define specifications and generative testing
"},{"location":"clojure-spec/projects/bank-account/","title":"Spec project: Bank Account","text":"A relatively simple bank account application with data and function specifications, including generative testing data and function instrumentation.
"},{"location":"clojure-spec/projects/bank-account/#hintunder-active-development","title":"Hint::Under active development","text":"Developed as part of the Practicalli study guide live broadcasts
"},{"location":"clojure-spec/projects/bank-account/#create-depsedn-project","title":"Create deps.edn project","text":"Use Clojure CLI and clj-new
clojure -M:new app practicalli/banking-on-clojure\n
"},{"location":"clojure-spec/projects/bank-account/#hintpracticallibanking-on-clojure-repository","title":"Hint::practicalli/banking-on-clojure repository","text":"practicalli/banking-on-clojure repository contains the latest code to date for this project.
"},{"location":"clojure-spec/projects/bank-account/#outline-design-of-project","title":"Outline design of project","text":"Data Specifications are created for
- Customer Details \u2714
- Account holder \u2714
- Bank account
- Multiple Bank accounts
- Credit Card
- Mortgage
Functions and specifications are created for
- register-account-holder \u2714
- open-credit-account
- open-savings-account
- open-credit-card-account
- open-mortgage-account
- Make a payment
- Send account notification
- Check for overdraft
"},{"location":"clojure-spec/projects/bank-account/#development-workflow","title":"Development Workflow","text":" - Write a failing test \u2714
- write mock data \u2714
- write an function definition that returns the argument \u2714
- run tests - tests should fail \u2714
- write a spec for the functions argument - customer \u2714
- write a spec for the return value \u2714
- write a spec for relationship between args and return value
- replace the mock data with generated values from specification \u2714
- update functions and make tests pass \u2714
- instrument functions
- run specification checks
\u2714
Images to add
Running tests that fail on a spec in CIDER spacemacs-cider-test-spec-fail-banking-on-clojure-project.png
Running tests that fail on a spec on CircleCI circle-ci-banking-on-clojure-spec-test-runner-fail-register-account-holder-did-not-conform-to-spec.png
"},{"location":"clojure-spec/projects/bank-account/account-holder-specification/","title":"Account holder specification","text":"The account holder has the same information as custom details with the addition of an account-id
In the register-account-holder a uuid is generated for the account id, So a spec can be defined for this type
(spec/def ::account-id uuid?)\n
"},{"location":"clojure-spec/projects/bank-account/account-holder-specification/#design-decision-hierarchical-or-composite","title":"Design decision: hierarchical or composite","text":"There are several approaches to combining, depending on the shape of the data used
The account holder is a hash-map, so spec/keys
will create the map from specification keys
Including the customer-details specification in spec/keys
would include the customer details as a nested hash-map
(spec/def ::account-holder-hierarchy\n (spec/keys\n :req [::account-id ::customer-details]))\n
A valid data structure for this specification is a map with two keys, account-id
and customer-details
. account-id
is a uuid value, customer-details is a hash-map of values that conform to the customer-details specification
(spec/valid? ::account-holder-hierarchy\n #::{:account-id (java.util.UUID/randomUUID)\n :customer-details #:: {:first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street, Earth\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"}})\n;; => true\n
Flat data structures are usually preferred in Clojure over a nested hierarchy. Rather than use the ::customer-details specification as a key in the spec/keys
expression. The individual specifications that make up ::customer-details can be used.
(spec/def ::account-holder-composition\n (spec/keys\n :req [::account-id ::first-name ::last-name ::email-address ::residential-address ::social-security-id]))\n
(spec/valid? ::account-holder-composition\n #::{:account-id (java.util.UUID/randomUUID)\n :first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street, Earth\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"})\n
"},{"location":"clojure-spec/projects/bank-account/customer-details-specification/","title":"Customer details specification","text":"Create a new file for the specifications, src/practicalli/banking_specifications.cljc
, with the namespace practicalli.banking-specifications
.
Require the clojure.spec.alpha
library with an alias of spec
.
(ns practicalli.banking-specifications\n (:require [clojure.spec.alpha :as spec]))\n
"},{"location":"clojure-spec/projects/bank-account/customer-details-specification/#define-basic-customer-details","title":"Define basic customer details","text":"Define a specification for the customer-details map, composed of all the required keys that define a customer.
The bank legally requires specific information about a customer in order to add them as an account holder
(spec/def ::first-name string?)\n(spec/def ::last-name string?)\n(spec/def ::email-address string?)\n
(spec/def ::email-address\n (spec/and string?\n #(re-matches #\"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,63}$\"\n %)))\n
This specification will require a custom generator
A residential address is made from several pieces of information and is defined as a composite specification, from several specifications.
(spec/def ::house-name-number (spec/or :string string?\n :number int?))\n(spec/def ::street-name string?)\n(spec/def ::post-code string?)\n(spec/def ::county string?)\n(spec/def ::country string?)\n
(spec/def ::residential-address (spec/keys :req [::house-name-number ::street-name ::post-code]\n :opt [::county ::country]))\n
A social security number specification is also a candidate for a composite specification. Social security numbers may take different forms and even have different names in different countries, eg. the USA SSN is a nine-digit number in the format \"AAA-GG-SSSS\"
In the UK the social security number is called the National Insurance number and is of the form QQ123456C
(spec/def ::social-security-id-uk string?)\n(spec/def ::social-security-id-usa string?)\n
(defn social-security-number-usa? [value] (= 9 (count value)))\n(defn social-security-number-uk? [value] (= 11 (count value)))\n(spec/def ::social-security-id-usa (spec/and string? social-security-number-usa?))\n(spec/def ::social-security-id-uk (spec/and string? social-security-number-uk?))\n
These specifications required a custom generator in order to produce correct data each time.
A general social security specification can now be defined, with one of any of the country specific specifications
(spec/def ::social-security-id (spec/or ::social-security-id-uk\n ::social-security-id-usa))\n
"},{"location":"clojure-spec/projects/bank-account/customer-details-specification/#infoa-more-detailed-email-specification","title":"INFO::A more detailed email specification","text":"Use a regular expression to define the syntax of an email address, eg. jenny@jetpack.org
"},{"location":"clojure-spec/projects/bank-account/customer-details-specification/#infodetailed-social-security-numbers-would-check-the-different-forms","title":"INFO::Detailed social security numbers would check the different forms","text":"Predicate functions can be defined to check for the size of the different social security forms.
"},{"location":"clojure-spec/projects/bank-account/customer-details-specification/#composing-the-customer-details-specification","title":"Composing the customer details specification","text":"A customer details specification is a hash-map of key value pairs. The keys are the specifications that have just been defined.
spec/keys
creates a specification for a hash-map with required and optional keys. spec/keys
also includes a check for a map, so no explicit check for a map is required.
(spec/def ::customer-details\n (spec/keys\n :req [::first-name ::last-name ::email-address ::residential-address ::social-security-id]))\n
"},{"location":"clojure-spec/projects/bank-account/function-specifications/","title":"Specifications for function definitions - fdef
","text":"Create a spec/fdef
for the register-account-holder function
clojure.spec.alpha/fdef
defines a specification for a function definition, defn
. Specifications can attached to the arguments using :args
, the return value using :ret
and the relationship between the two using fn
.
:args
, :ret
and fn
are optional, although args
and ret
are required if you want to use :fn
"},{"location":"clojure-spec/projects/bank-account/function-specifications/#add-a-spec-to-cover-the-function-arguments","title":"Add a spec to cover the function arguments","text":":args
is a compound specification that covers all the function arguments. The :args
spec is invoked with the arguments in a list, so working with them is like using apply.
Using regular expressions we can find the right arguments to give to the specification. Regular expression spec functions include
The register-account-holder only takes one argument, so spec/cat
is used to bind a local key to the specification.
The function is defined in the practicalli.banking-on-clojure
namespace. Require that namespace in the current ns
form.
(ns practicalli.banking-specifications\n (:require [clojure.spec.alpha :as spec]\n [clojure.spec.gen.alpha :as spec-gen]\n [clojure.spec.test.alpha :as spec-test]\n\n [practicalli.banking-on-clojure :as SUT]))\n
The SUT
alias is used for the banking-on-clojure namespace, as is done with clojure.test
unit test namespaces.
(spec/fdef SUT/register-account-holder\n :args (spec/cat :customer\n :practicalli.bank-account-spec/customer-details))\n
"},{"location":"clojure-spec/projects/bank-account/function-specifications/#checking-function-calls-against-the-spec-instrument","title":"Checking function calls against the spec - instrument","text":"spec/fdef
by itself does not run checks against the specs
(register-account-holder {})\n;; => {:account-id #uuid \"3a6dddb7-dd87-485e-90f8-8c8975302845\"}\n
Require the Clojure spec test library
(require '[clojure.spec.test.alpha :as spec-test])\n
spec/instrument
will add a run time check for the specification
(spec-test/instrument `SUT/register-account-holder)\n
No the function is instrumented, data used as arguments of a function call will be checked against the specification.
(register-account-holder {::bad \"data\"})\n
This function call throws an exception because of the specification attached to the :args
section of the fdef
specification.
The error report provides detailed and quite clear information to help diagnose the issue
1. Unhandled clojure.lang.ExceptionInfo\n Spec assertion failed.\n\n Spec: #object[clojure.spec.alpha$regex_spec_impl$reify__2509 0x12b66a86 \"clojure.spec.alpha$regex_spec_impl$reify__2509@12b66a86\"]\n Value: (#:practicalli.bank-account-design-journal{:bad \"data\"})\n\n Problems:\n\n val: #:practicalli.bank-account-design-journal{:bad \"data\"}\n in: [0]\n failed: (contains? % :practicalli.bank-account-spec/first-name)\n spec: :practicalli.bank-account-spec/customer-details\n at: [:customer]\n\n val: #:practicalli.bank-account-design-journal{:bad \"data\"}\n in: [0]\n failed: (contains? % :practicalli.bank-account-spec/last-name)\n spec: :practicalli.bank-account-spec/customer-details\n at: [:customer]\n\n val: #:practicalli.bank-account-design-journal{:bad \"data\"}\n in: [0]\n failed: (contains? % :practicalli.bank-account-spec/email-address)\n spec: :practicalli.bank-account-spec/customer-details\n at: [:customer]\n\n val: #:practicalli.bank-account-design-journal{:bad \"data\"}\n in: [0]\n failed: (contains? % :practicalli.bank-account-spec/residential-address)\n spec: :practicalli.bank-account-spec/customer-details\n at: [:customer]\n\n val: #:practicalli.bank-account-design-journal{:bad \"data\"}\n in: [0]\n failed: (contains? % :practicalli.bank-account-spec/social-security-id)\n spec: :practicalli.bank-account-spec/customer-details\n at: [:customer]\n
Calling the register-account-holder with a value that conforms to the bank-account-spec for customer details returns the new value for account-holder
(register-account-holder\n #:practicalli.bank-account-spec\n {:first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street, Earth\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"})\n\n;; => {:practicalli.bank-account-spec/first-name \"Jenny\", :practicalli.bank-account-spec/last-name \"Jetpack\", :practicalli.bank-account-spec/email-address \"jenny@jetpack.org\", :practicalli.bank-account-spec/residential-address \"42 meaning of life street, Earth\", :practicalli.bank-account-spec/postal-code \"AB3 0EF\", :practicalli.bank-account-spec/social-security-id \"123456789\", :account-id #uuid \"e0f327de-4e92-479e-a9de-468e2c7c0e6d\"}\n
"},{"location":"clojure-spec/projects/bank-account/function-specifications/#add-a-specification-to-the-return-value","title":"Add a specification to the return value","text":"Attach the account-holder details specification to :ret
(spec/fdef register-account-holder\n :args (spec/cat :customer\n :practicalli.bank-account-spec/customer-details)\n :ret :practicalli.bank-account-spec/account-holder)\n
If the register-account-holder
logic changes to return a different value that the return spec, then an exception is raised
Returns an integer rather than a uuid
(defn register-account-holder\n \"Register a new customer with the bank\n Arguments:\n - hash-map of customer-details\n Return:\n - hash-map of an account-holder (adds account id)\"\n [customer-details]\n\n (assoc customer-details\n :practicalli.bank-account-spec/account-id\n (rand-int 100000)\n #_(java.util.UUID/randomUUID)))\n
So this should fail
(register-account-holder\n #:practicalli.bank-account-spec\n {:first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street, Earth\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"})\n
It still works as spec-test/instrument
only checks the args value.
spec-test/check
will test the return value with generated tests
(require '[clojure.spec.gen.alpha :as spec-gen])\n
(spec-test/check `SUT/register-account-holder)\n
The result is 100 generated tests that all fail, because the function was changed to return integers, not uuids
1. Caused by clojure.lang.ExceptionInfo\n Couldn't satisfy such-that predicate after 100 tries.\n {:pred #function[clojure.spec.alpha/gensub/fn--1876],\n :gen {:gen #function[clojure.test.check.generators/such-that/fn--8322]},\n :max-tries 100}\n
"},{"location":"clojure-spec/projects/bank-account/function-specifications/#change-the-function-back-again","title":"Change the function back again","text":"(defn register-account-holder\n \"Register a new customer with the bank\n Arguments:\n - hash-map of customer-details\n Return:\n - hash-map of an account-holder (adds account id)\"\n [customer-details]\n\n (assoc customer-details\n :practicalli.bank-account-spec/account-id\n (java.util.UUID/randomUUID)))\n
"},{"location":"clojure-spec/projects/bank-account/function-specifications/#instrument-the-function","title":"Instrument the function","text":"Testing function calls against the specification
Requires the spec test namespace
(require '[clojure.spec.test.alpha :as spec-test])\n
Instrument the spec to add checking, this only checks the arguments are correct.
(spec-test/instrument `practicalli.bank-account/register-account-holder)\n
(register-account-holder {:first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"})\n
"},{"location":"clojure-spec/projects/bank-account/generate-test-data/","title":"Generate test data","text":""},{"location":"clojure-spec/projects/bank-account/generate-test-data/#generate-test-data-from-specifications","title":"Generate test data from Specifications","text":"Now there are specifications for the customer-details and account-details, spec can generate random data for use with tests.
"},{"location":"clojure-spec/projects/bank-account/generate-test-data/#add-requires-to-the-test-namespace","title":"Add requires to the test namespace","text":"Edit the src/test/practicalli/banking_on_clojure.clj
and add requires for the banking-specifications
, clojure.spec.alpha
and clojure.spec.test.alpha
.
(ns practicalli.banking-on-clojure-test\n (:require [clojure.test :refer [deftest is testing]]\n [clojure.spec.alpha :as spec]\n [clojure.spec.test.alpha :as spec-test]\n [clojure.spec.gen.alpha :as spec-gen]\n [practicalli.banking-on-clojure :as SUT]\n [practicalli.banking-specifications]))\n
"},{"location":"clojure-spec/projects/bank-account/generate-test-data/#using-generators-to-generate-data","title":"Using Generators to generate data","text":"Test data can now be generated from this specification, creating values for each key in the hash-map.
The ::email-address specification has been simplified, as the regular expression version requires a custom generator (no built in generator to support this specification). The simplified email specification is:
(spec/def ::email-address string?)\n
With the simplified email specification, the customer-details specification can be used to generate all the data using the built in clojure.spec.alpha generators.
(spec-gen/generate (spec/gen :practicalli.banking-specifications/customer-details))\n\n;; => #:practicalli.banking-specifications\n{:first-name \"r7q9RFB202v7a69z\",\n :last-name \"6N5\",\n :email-address \"L6dd946p680P0pIYZ33CGZd0\",\n :residential-address\n #:practicalli.banking-specifications{\n :house-name-number \"gCuRMe0C8\",\n :street-name \"5\",\n :post-code \"VN\"},\n :social-security-id \"a7P0xfBNPv6\"}\n
Bind the result of this function to a name and it can be used as mock data throughout the unit tests defined.
(defn customer-details-mock-data\n (spec-gen/generate (spec/gen :practicalli.banking-specifications/customer-details)))\n
The generated data can also be used with function definitions and clojure.spec.test.alpha/check
function.
"},{"location":"clojure-spec/projects/bank-account/generate-test-data/#hintlibraries-for-custom-generators","title":"Hint::Libraries for custom generators","text":"gfredericks/test.chuck is a utility library for test.check and will work with clojure spec as its a wrapper around test.check.
lambdaisland/regal also has test.check generators that can be used for regular expressions defined with the regal (hiccup style) syntax.
"},{"location":"clojure-spec/projects/bank-account/generate-test-data/#generating-more-than-one-value-for-a-specification","title":"Generating more than one value for a specification","text":"clojure.spec.gen.alpha/sample
will generate 10 random values from the specification
(spec-gen/sample (spec/gen :practicalli.banking-specifications/customer-details))\n\n;; => (#:practicalli.banking-specifications{:first-name \"\", :last-name \"\", :email-address \"\", :residential-address #:practicalli.banking-specifications{:country \"\", :county \"\", :house-name-number \"\", :street-name \"\", :post-code \"\"}, :social-security-id \"2P902qTJCP6\"}\n\n#:practicalli.banking-specifications{:first-name \"\", :last-name \"z\", :email-address \"\", :residential-address #:practicalli.banking-specifications{:house-name-number 0, :street-name \"\", :post-code \"R\"}, :social-security-id \"3dDBA7pa98r\"}\n\n#:practicalli.banking-specifications{:first-name \"nQ\", :last-name \"w\", :email-address \"h6\", :residential-address #:practicalli.banking-specifications{:country \"\", :county \"7u\", :house-name-number \"\", :street-name \"87\", :post-code \"\"}, :social-security-id \"x57pf2H2i16\"}\n\n#:practicalli.banking-specifications{:first-name \"ac\", :last-name \"L0x\", :email-address \"S\", :residential-address #:practicalli.banking-specifications{:country \"Xd\", :county \"\", :house-name-number \"P\", :street-name \"\", :post-code \"\"}, :social-security-id \"j5iTA70j9FW\"}\n\n#:practicalli.banking-specifications{:first-name \"e\", :last-name \"ic\", :email-address \"15G\", :residential-address #:practicalli.banking-specifications{:house-name-number \"\", :street-name \"Nj\", :post-code \"f\"}, :social-security-id \"I83rx1wUj07\"}\n\n#:practicalli.banking-specifications{:first-name \"zPr\", :last-name \"r\", :email-address \"hsVz\", :residential-address #:practicalli.banking-specifications{:country \"W\", :house-name-number \"S\", :street-name \"64\", :post-code \"85s25\"}, :social-security-id \"8EEDiy28SX7\"}\n\n#:practicalli.banking-specifications{:first-name \"QzoV\", :last-name \"\", :email-address \"iS\", :residential-address #:practicalli.banking-specifications{:county \"OaMj9\", :house-name-number 1, :street-name \"pzc0ji\", :post-code \"tv1\"}, :social-security-id \"9z88KM5TLKK\"}\n\n#:practicalli.banking-specifications{:first-name \"w73AA\", :last-name \"\", :email-address \"\", :residential-address #:practicalli.banking-specifications{:county \"sUj\", :house-name-number 4, :street-name \"jw\", :post-code \"652Z\"}, :social-security-id \"rZMUTPK72N6\"}\n\n#:practicalli.banking-specifications{:first-name \"j09f\", :last-name \"EoU\", :email-address \"sA82q\", :residential-address #:practicalli.banking-specifications{:country \"28nyq3\", :county \"5PURE\", :house-name-number \"1NzKwe\", :street-name \"28Y\", :post-code \"t\"}, :social-security-id \"yNBdc7M29Io\"}\n\n#:practicalli.banking-specifications{:first-name \"Xa38iX8FP\", :last-name \"u4G\", :email-address \"Ne1w25nJ\", :residential-address #:practicalli.banking-specifications{:country \"H07\", :house-name-number -17, :street-name \"jWRhfrrz9\", :post-code \"sF9\"}, :social-security-id \"IX2w8Xx8u0n\"})\n
Generating multiple result is useful if a collection of customer details is required for testing purposes.
"},{"location":"clojure-spec/projects/bank-account/generate-test-data/#exercising-a-specification","title":"Exercising a specification","text":"clojure.spec.test.alpha/exercise
returns pairs of generated and conformed values for a spec. exercise by default produces 10 samples (like sample) but you can pass both functions a number indicating the number of samples to produce.
(spec/exercise (spec/cat :practicalli.banking-specifications/first-name :practicalli.banking-specifications/last-name))\n\n;; => ([(\"\") #:practicalli.banking-specifications{:first-name \"\"}]\n [(\"6\") #:practicalli.banking-specifications{:first-name \"6\"}]\n [(\"\") #:practicalli.banking-specifications{:first-name \"\"}]\n [(\"6\") #:practicalli.banking-specifications{:first-name \"6\"}]\n [(\"W\") #:practicalli.banking-specifications{:first-name \"W\"}]\n [(\"ljooD\") #:practicalli.banking-specifications{:first-name \"ljooD\"}]\n [(\"704d5x\") #:practicalli.banking-specifications{:first-name \"704d5x\"}]\n [(\"EZyBT\") #:practicalli.banking-specifications{:first-name \"EZyBT\"}]\n [(\"1e6\") #:practicalli.banking-specifications{:first-name \"1e6\"}]\n [(\"v\") #:practicalli.banking-specifications{:first-name \"v\"}])\n
clojure.spec.test.alpha/exercise-fn
provides the same service but for function specifications (fdef
).
"},{"location":"clojure-spec/projects/bank-account/rebel-readline/","title":"Using the project in rebel readline","text":"Start the rebel REPL
clojure -M:repl/rebel\n
Once rebel has started a prompt will be displayed.
First required the main namespace, containing the functions of the application. This loads the code in that namespace into the REPL.
(require 'practicalli.banking-on-clojure)\n
Now add the specifications for the project
(require 'practicalli.banking-on-clojure)\n
? When does the TAB completion start to work ?
Testing the specifications
First change into the specifications namespace so the fully qualified names of the specs are not required.
(in-ns 'practicalli.banking-specifications)\n
Generate sample data from the specifications
(spec-gen/sample (spec/gen ::account-id))\n
The function specifications and the instrument functions are loaded from the requires, so test by calling the instrumented functions, first with bad data and then with correct data.
(register-account-holder {})\n
Use the specifications to generate good data
(register-account-holder ::customer-details)\n
Run generative tests on functions to check the return and fn values
(spec-test/check `register-account-holder)\n
"},{"location":"clojure-spec/projects/bank-account/rebel-readline/#hinttodo-add-screenshots-using-rebel-readline-repl","title":"Hint::TODO: add screenshots using Rebel readline REPL","text":""},{"location":"clojure-spec/projects/bank-account/test-functions-against-spec/","title":"Test functions against spec","text":""},{"location":"clojure-spec/projects/bank-account/test-functions-against-spec/#generative-testing-with-check","title":"Generative testing with check
","text":"clojure.spec.test.alpha/check
generates 1000 values from the argument section of a function definition specification.
Pass the name of the function definition that has a specification to the check
function.
(spec-test/check ``register-account-holder`)\n
"},{"location":"clojure-spec/projects/bank-account/test-functions-against-spec/#limiting-the-generated-data","title":"Limiting the generated data","text":"1000 tests can take a noticeable time to run, so check is not as often used during active development, as it would slow down the normal fast feedback cycle with Clojure.
check
takes an optional second argument which configures how the function operates. Passing a hash-map as a second argument will set the number of data values generated {:clojure.spec.test.check/opts {:num-tests 100}}
(spec-test/check\n `register-account-holder\n {:clojure.spec.test.check/opts {:num-tests 100}})\n
Configuring check
to run fewer tests provides a simple way to test multiple values without slowing down the development workflow.
"},{"location":"clojure-spec/projects/bank-account/test-functions-against-spec/#reporting-on-generative-testing","title":"Reporting on Generative testing","text":"clojure.spec.test.alpha/summarize-results
will return a brief summary including the total number of results and a count for how many results passed and failed.
(spec-test/summarize-results\n (spec-test/check `register-customer\n {:clojure.spec.test.check/opts {:num-tests 10}}))\n
Use the threading macro to summarize the results of multiple check operations
(->> (spec-test/check `register-account-holder)\n (spec-test/check `open-current-bank-account)\n (spec-test/summarize-results))\n
If this expression is bound to a name then it can be called when ever the full suite of check
generative testing is required.
"},{"location":"clojure-spec/projects/bank-account/unit-tests-with-spec/","title":"Unit tests with specs","text":"Now that customer data and account-holder data has a specification, we can use the clojure.spec.alpha/valid?
in the unity test code, as that function returns true or false.
In this example the result of a call to register-account-holder
is checked to see if it is valid against the ::account-holder
specification. This simplifies the code needed in unit test assertions, as Clojure spec is doing the work.
(deftest register-account-holder-test\n (testing \"Basic registration - happy path\"\n (is (= (set (keys (SUT/register-account-holder customer-mock)))\n (set (keys account-holder))))\n\n (is (spec/valid? :practicalli.bank-account-spec/account-holder\n (SUT/register-account-holder customer-mock) ) )\n ))\n
"},{"location":"clojure-spec/projects/bank-account/validate-customer-details-specification/","title":"Testing data Specifications","text":"The specifications defined so far can be tested with specific data, using the conform
or valid?
functions.
Generating sample data from the specifications also provides useful feedback on how well the specifications are defined.
"},{"location":"clojure-spec/projects/bank-account/validate-customer-details-specification/#generating-data-from-specifications","title":"Generating data from specifications","text":"Test data specifications by generating sample data from those specifications.
Evaluating these functions several times is a quick way to identifies specifications that may require custom generators. If individual specifications do not generate consistent data, then incorrect results may occur during composite data specifications or function specifications.
(spec-gen/sample (spec/gen ::first-name))\n(spec-gen/sample (spec/gen ::last-name))\n(spec-gen/sample (spec/gen ::email-address))\n(spec-gen/sample (spec/gen ::house-name-number))\n(spec-gen/sample (spec/gen ::street-name))\n(spec-gen/sample (spec/gen ::post-code))\n(spec-gen/sample (spec/gen ::county))\n(spec-gen/sample (spec/gen ::country))\n(spec-gen/sample (spec/gen ::residential-address))\n(spec-gen/sample (spec/gen ::social-security-id-uk))\n(spec-gen/sample (spec/gen ::social-security-id-usa))\n(spec-gen/sample (spec/gen ::social-security-id))\n(spec-gen/sample (spec/gen ::customer-details))\n(spec-gen/sample (spec/gen ::account-holder))\n
"},{"location":"clojure-spec/projects/bank-account/validate-customer-details-specification/#validating-the-customer-details-specifications","title":"Validating the customer details specifications","text":"The specifications can be checked using the conform or valid? functions with example data.
Check an example hash-map from our test conforms to the specification
(spec/conform ::customer-details\n {:first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"})\n;; => :clojure.spec.alpha/invalid\n
The mock test data does not confirm to the specification, even though it has all the same keys as the map in the specification
(spec/valid? ::customer-details\n {:first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"})\n;; => false\n
spec/explain
will provide more information to help diagnose the issue
(spec/explain ::customer-details\n {:first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"})\n\n;; {:first-name \"Jenny\", :last-name \"Jetpack\", :email-address \"jenny@jetpack.org\", :residential-address \"42 meaning of life street\", :postal-code \"AB3 0EF\", :social-security-id \"123456789\"}\n;; - failed: (contains? % :practicalli.bank-account-design-journal/first-name) spec: :practicalli.bank-account-design-journal/customer-details\n;; {:first-name \"Jenny\", :last-name \"Jetpack\", :email-address \"jenny@jetpack.org\", :residential-address \"42 meaning of life street\", :postal-code \"AB3 0EF\", :social-security-id \"123456789\"}\n;; - failed: (contains? % :practicalli.bank-account-design-journal/last-name) spec: :practicalli.bank-account-design-journal/customer-details\n;; {:first-name \"Jenny\", :last-name \"Jetpack\", :email-address \"jenny@jetpack.org\", :residential-address \"42 meaning of life street\", :postal-code \"AB3 0EF\", :social-security-id \"123456789\"}\n;; - failed: (contains? % :practicalli.bank-account-design-journal/email-address) spec: :practicalli.bank-account-design-journal/customer-details\n;; {:first-name \"Jenny\", :last-name \"Jetpack\", :email-address \"jenny@jetpack.org\", :residential-address \"42 meaning of life street\", :postal-code \"AB3 0EF\", :social-security-id \"123456789\"}\n;; - failed: (contains? % :practicalli.bank-account-design-journal/residential-address) spec: :practicalli.bank-account-design-journal/customer-details\n;; {:first-name \"Jenny\", :last-name \"Jetpack\", :email-address \"jenny@jetpack.org\", :residential-address \"42 meaning of life street\", :postal-code \"AB3 0EF\", :social-security-id \"123456789\"}\n;; - failed: (contains? % :practicalli.bank-account-design-journal/social-security-id) spec: :practicalli.bank-account-design-journal/customer-details\n
The ::customer-details
spec is given a map with unqualified keys and is failing the :req
part of the spec/keys
part of the specification
"},{"location":"clojure-spec/projects/bank-account/validate-customer-details-specification/#qualifying-keys-with-auto-resolve-macro","title":"Qualifying keys with auto-resolve macro","text":"The auto-resolve macro, #::
will add the current namespace to all the keys in a hash-map
Change the test data to use qualified keys by adding the
(spec/conform ::customer-details\n #::{:first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"} )\n;; => #:practicalli.bank-account-design-journal{:first-name \"Jenny\", :last-name \"Jetpack\", :email-address \"jenny@jetpack.org\", :residential-address \"42 meaning of life street\", :postal-code \"AB3 0EF\", :social-security-id \"123456789\"}\n
"},{"location":"clojure-spec/projects/bank-account/write-failing-tests/","title":"Write failing tests","text":"In Test Driven Development style, first write unit tests for the banking functions.
Edit the src/practicalli/banking_on_clojure_test.clj
and add deftest
tests
(deftest register-account-holder-test\n (testing \"Basic registration - happy path\"\n (is (= (set (keys (register-account-holder {})))\n (set (keys {:account-id \"123\" :customer-name \"Jenny Jetpack\"}))))))\n
"},{"location":"clojure-spec/projects/bank-account/write-failing-tests/#write-a-function-stub-to-run-the-tests","title":"Write a function stub to run the tests","text":"The tests cannot run unless they call the function to be tested. A common approach it to write a function that returns the argument.
(defn register-account-holder\n \"Register a new customer with the bank\n Arguments:\n - hash-map of customer-details\n Return:\n - hash-map of an account-holder (adds account id)\"\n\n [customer-details]\n\n customer-details)\n
"},{"location":"clojure-spec/projects/bank-account/write-failing-tests/#add-mock-data","title":"Add mock data","text":"Define some initial mock data to use with the unit tests
(def customer-mock\n {:first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street, Earth\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"})\n
account is a customer with a bank account id added\n\n(def account-holder-mock\n {:account-id #uuid \"97bda55b-6175-4c39-9e04-7c0205c709dc\"\n :first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street, Earth\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"})\n
Update the test to use the mock data.
(deftest register-account-holder-test\n (testing \"Basic registration - happy path\"\n (is (= (set (keys (register-account-holder customer-mock)))\n (set (keys account-holder-mock))))))\n
"},{"location":"clojure-spec/projects/card-game/","title":"Card game: spec and generative testing","text":"Define a data specification that represent a deck of playing cards, adding functional specifictations to check the values passed to the functions use to play a card game.
spec generators are used to return varied sample data from those specifications. Function definitions are instrumented and check for correct arguments when those functions are called.
"},{"location":"clojure-spec/projects/card-game/#create-a-new-project","title":"Create a new project","text":"Create a new Clojure project using :project/create
from Practicalli Clojure CLI Config or add an alias definition of your choosing to the Clojure CLI user configuration.
clojure -T:project/create :template app :name practicalli/card-game\n
Open the src/practicalli/card_game.clj
file and require the clojure.spec.alpha
namespace
(ns practicalli.card-game.clj\n (:require [clojure.spec.alpha :as spec]))\n
"},{"location":"clojure-spec/projects/card-game/#playing-card-specifications","title":"Playing card specifications","text":"A playing card has a face value and a suit. There are 4 suits in a card deck.
A specification for the possible suits can be defined using literal values
(spec/def ::suits #{:clubs :diamonds :hearts :spades})\n
Define a predicate function to check a value conforms to the spec using the pattern matching that is build-in to the Clojure set
data type.
(def suits? #{:clubs :diamonds :hearts :spades})\n
"},{"location":"clojure-spec/projects/card-game/#card-game-decks","title":"Card game decks","text":"Suits from different regions are called by different names. Each of these suits can be their own spec.
(spec/def ::suits-french #{:hearts :tiles :clovers :pikes})\n(spec/def ::suits-german #{:hearts :bells :acorns :leaves})\n(spec/def ::suits-spanish #{:cups :coins :clubs :swords})\n(spec/def ::suits-italian #{:cups :coins :clubs :swords})\n(spec/def ::suits-swiss-german #{:roses :bells :acorns :shields})\n
A composite specification called ::card-suits
provides a simple abstraction over all the variations of suits. Using ::card-suits
will be satisfied with any region specific suits.
(spec/def ::card-suits\n (spec/or :french ::suits-french\n :german ::suits-german\n :spanish ::suits-spanish\n :italian ::suits-italian\n :swiss-german ::suits-swiss-german\n :international ::suits-international))\n
"},{"location":"clojure-spec/projects/card-game/#define-an-alias","title":"Define an alias","text":"Jack queen king are called face cards in the USA and occasionally referred to as court cards in the UK.
Define a spec for ::face-cards
and then define :court-cards
and alias
(spec/def ::face-cards #{:jack :queen :king :ace})\n(spec/def ::court-cards ::face-cards)\n
Any value that conforms to the ::face-card
specification also conforms to the ::court-cards
specification.
(spec/conform ::court-cards :ace)\n
"},{"location":"clojure-spec/projects/card-game/#playing-card-rank","title":"Playing card rank","text":"Each suit in the deck has the same rank of cards explicitly defining a rank
(spec/def ::rank #{:ace 2 3 4 5 6 7 8 9 10 :jack :queen :king})\n
Rank can be defined more succinctly with the clojure.core/range
function. The expression (range 2 11)
will generates a sequence of integer numbers from 2 to 10 (the end number is exclusive, so 11 is not in the sequence).
Using clojure.core/into
this range of numbers can be added to the face card values.
(into #{:ace :jack :queen :king} (range 2 11))\n
The ::rank
specification now generates all the possible values for playing cards.
(spec/def ::rank (into #{:ace :jack :queen :king} (range 2 11)))\n
The specification only checks to see if a value is in the set, the order of the values in the set is irrelevant.
"},{"location":"clojure-spec/projects/card-game/#playing-card","title":"Playing Card","text":"A playing card is a combination of suit and face value, a pair of values, referred to as a tuple.
Clojure spec has a tuple
function, however, we need to define some predicates first
(spec/def ::playing-card (spec/tuple ::rank ::suits ))\n
Use the spec with values to see if they conform. Try you own values for a playing card.
(spec/conform ::playing-card [:ace :spades])\n
"},{"location":"clojure-spec/projects/card-game/#game-specs","title":"Game specs","text":"Define specifications for data used to represent players and the overall card game.
The player name is a very simple spec.
(spec/def ::name string?)\n
Score will keep a running total of a player score across games, again a simple integer value.
(spec/def ::score int?)\n
A player is represented by a hash-map that contains their name, score and the hand they are currently dealt. The hand is a collection of tuples representing a playing card.
(spec/def ::player\n (spec/keys\n :req [::name ::score ::dealt-hand]))\n
"},{"location":"clojure-spec/projects/card-game/#game-deck-specs","title":"Game deck specs","text":"A card game has a deck of 52 cards, one card for each combination of suit and rank.
The size of the card deck changes over the course of a game, so the deck can contain any number of cards. The deck must contain only cards to be valid.
(spec/def ::card-deck (spec/* ::playing-card))\n
At this stage in the design, a card game can have any number of players
(spec/def ::players (spec/* ::player))\n
A game is represented by a hash-map with a collection of players and a card deck
(spec/def ::game (spec/keys :req [::players ::card-deck]))\n
"},{"location":"clojure-spec/projects/card-game/#generative-data-from-specifications","title":"Generative data from Specifications","text":"Clojure spec can generate random data which conforms to a specification, highly useful in testing Clojure code with a wide variety of values.
clojure.spec.alpha/gen
returns a generator for the given specification. clojure.spec.gen.alpha/generate
takes that generator and creates a random value that conforms to the specification. clojure.spec.gen.alpha/sample
will generate a collection of random values that each conform to the specification.
Require the clojure spec namespaces to make use of their functions.
(ns practicalli.card-game.clj\n (:require [clojure.spec.alpha :as spec]\n [clojure.spec.gen.alpha :as spec-gen]\n [clojure.spec.test.alpha :as spec-test]))\n\n(spec/def ::suits #{:clubs :diamonds :hearts :spades})\n(spec/def ::rank #{:ace 2 3 4 5 6 7 8 9 10 :jack :queen :king})\n
To generated data based on a specification, first get a generator for a given spec,
(spec/gen ::suits)\n
generate
will return a value using the specific generator for the specification.
(spec-gen/generate (spec/gen ::suits))\n
sample
will generate a number of values from the given specification
(spec-gen/sample (spec/gen ::rank))\n
"},{"location":"clojure-spec/projects/card-game/#card-game-data","title":"Card Game data","text":"Generate a random value for the ::player
specification
(spec-gen/generate (spec/gen ::player))\n
Example Expected output from generate
```clojure
```
Generate a random value for the ::game
specification
(spec-gen/generate (spec/gen ::game))\n
Generate a collection of random values that each conform to the specification.
(spec-gen/sample (spec/gen ::game))\n
"},{"location":"clojure-spec/projects/card-game/#practicallispec-generative-testing","title":":practicalli.spec-generative-testing","text":"{:name \"Yp34KE63vAL1eriKN4cBt\", :score 225, :dealt-hand ([9 :hearts] [4 :clubs] [8 :hearts] [10 :clubs] [:queen :spades] [3 :clubs] [6 :hearts] [8 :hearts] [7 :diamonds] [:king :spades] [:ace :diamonds] [2 :hearts] [4 :spades] [2 :clubs] [6 :clubs] [8 :diamonds] [6 :spades] [5 :spades] [:queen :clubs] [:queen :hearts] [6 :spades])}
"},{"location":"clojure-spec/projects/card-game/#function-specifications","title":"Function Specifications","text":"A function specification can contain a specification for the arguments, the return values and the relationship between the two.
The specifications for the function may be composed from previously defined data specifications.
(ns practicalli.card-game\n (:require [clojure.spec.alpha :as spec]\n [clojure.spec.gen.alpha :as spec-gen]\n [clojure.spec.test.alpha :as spec-test]))\n\n(spec/def ::suit #{:clubs :diamonds :hearts :spades})\n(spec/def ::rank (into #{:jack :queen :king :ace} (range 2 11)))\n(spec/def ::playing-card (spec/tuple ::rank ::suit))\n(spec/def ::dealt-hand (spec/* ::playing-card))\n\n(spec/def ::name string?)\n(spec/def ::score int?)\n(spec/def ::player (spec/keys :req [::name ::score ::dealt-hand]))\n(spec/def ::card-deck (spec/* ::playing-card))\n(spec/def ::players (spec/* ::player))\n(spec/def ::game (spec/keys :req [::players ::card-deck]))\n
"},{"location":"clojure-spec/projects/card-game/#function-definition","title":"Function definition","text":"The card game application has three functions to start with.
(defn regulation-card-deck\n \"Generate a complete deck of playing cards\"\n [{:keys [::deck ::players] :as game}]\n (apply + (count deck)\n (map #(-> % ::delt-hand count) players)))\n
At the start of function design, the algorithm may still be undefined. Using the specifications and generators mock data can be returned as a placeholder.
(defn deal-cards\n \"Deal cards to each of the players\n Returns updated game hash-map\"\n [game]\n (spec-gen/generate (spec/gen ::game)))\n
(defn winning-player\n \"Calculate winning hand by comparing each players hand\n Return winning player\"\n [players]\n (spec-gen/generate (spec/gen ::player)))\n
Example The expected form of a player won game:
#:practicalli.player-won\n {:name \"Jenny Nada\",\n :score 225,\n :dealt-hand [[9 :hearts] [4 :clubs] [8 :hearts] [10 :clubs] [:queen :spades]]}\n
"},{"location":"clojure-spec/projects/card-game/#spec-definitions","title":"Spec definitions","text":"Define a function specification for the deal-cards
function
- argument must be of type
::game
- return type is
::game
- function applies arguments to a game and returns the game
(spec/fdef deal-cards\n :args (spec/cat :game ::game)\n :ret ::game\n :fn #(= (regulation-card-deck (-> % :args :game))\n (regulation-card-deck (-> % :ret))))\n
Define a function specification for the winning-player
function
- argument must be of type
::players
- return type is
::players
(spec/fdef winning-player\n :args (spec/cat :players ::players)\n :ret ::player)\n
"},{"location":"clojure-spec/projects/card-game/#instrument-functions","title":"Instrument functions","text":"Instrumenting functions will wrap a function definition and check the arguments of any call to the instrumented function.
(spec-test/instrument `deal-cards)\n
Calling the deal-cards
function with an incorrect argument returns an error that describes where in the specification the error occurred.
(deal-cards \"fake game data\")\n
Error in an easier to read format
ERROR: #error\n {:message \"Call to #'practicalli.card-game/deal-cards did not conform to spec:\\n\\\n \"fake game data\\\" - failed:\n map? in: [0] at: [:args :game] spec: :practicalli.card-game/game\\n\",\n :data {:cljs.spec.alpha/problems\n [{:path [:args :game],\n :pred cljs.core/map?,\n :val \"fake game data\",\n :via [:practicalli.card-game/game :practicalli.card-game/game],\n :in [0]}],\n :cljs.spec.alpha/spec #object[cljs.spec.alpha.t_cljs$spec$alpha17968],\n :cljs.spec.alpha/value (\"fake game data\"),\n :cljs.spec.alpha/args (\"fake game data\"),\n :cljs.spec.alpha/failure :instrument}}\n
"},{"location":"clojure-spec/projects/card-game/#organizing-function-instrumentation","title":"Organizing function instrumentation","text":"Instrumenting functions creates a wrapper around the original function definition.
When you change the function definition and evaluate the new code, it replaces the instrumentation of the function. Therefore each time a function is redefined it should be instrumented.
There is no specific way to manage instrumenting a function, however, a common approach is to define a collection of functions to instrument, then use a helper function to instrument all the functions at once.
Bind a name to the collection of function specifications.
(def ^:private function-specifications\n [`card-game/deal-cards\n `card-game/winning-player])\n
Define a simple helper function to instrument all the functions in the collection.
(defn instrument-all-functions\n []\n (spec-test/instrument function-specifications))\n
Refactoring the code may involve a number of changes benefit from instrumentation being switched off until its complete. The unstrument
function will remove instrumentation from all the functions in the collection.
(defn unstrument-all-functions\n []\n (spec-test/unstrument function-specifications))\n
Koacha Test Runner can include functional specifications
Koacha test runner can manage the testing of function specifications and is especially useful for managing unit level testing with specifications.
"},{"location":"clojure-spec/testing/","title":"Testing with Specifications","text":""},{"location":"clojure-spec/testing/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"clojure-spec/testing/#during-development","title":"During development","text":"Create specifications for data and functions
Selectively instrument function definitions to check function call arguments against the function specification.
- clojure.spec.test.alpha/instrument - check fdef :args
"},{"location":"clojure-spec/testing/#unit-and-integration-testing","title":"Unit and integration testing","text":"Add specification checks along with unit testing and integration testing to provide a very wide range of data values to be tested (with a minimal amount of code).
- clojure.spec.test.alpha/check - use :args to generate tests to check fdef :ret and :fn
run a suite of spec-generative tests on an entire ns with check
. Just one namespace per check
expression?
control the number of values check creates for each check expression. As the default is 1000 the checks can take a noticeable time to run (see practicalli/spec-generative-testing)
Many built-in generators for clojure.core
data predicates
composite specifications can build generators upon predicate generators.
Pass generator-returning functions to spec, supplying generators for things spec does not know about. Pass an override map to gen
in order to supply alternative generators for one or more sub-paths of a spec.
Define your own generators
"},{"location":"clojure-spec/testing/#at-run-time","title":"At run time","text":"Use specifications for run time checking, typically using conform
and valid?
functions.
Specification are typically the minimal checks required for the system, compared to more extensive checks during test and system integration.
Create lightweight private specifications for tests that run in the production environment.
"},{"location":"clojure-spec/testing/checking%20arguments/","title":"Checking arguments in function calls with specifications","text":""},{"location":"clojure-spec/testing/checking%20arguments/#instrument-functions-during-development","title":"Instrument functions during development","text":"Instrumenting a function enables the checking of arguments in a function call against the specification defined in an fdef
definition of the same name.
(clojure.spec.test.alpha/instrument `function-name)\n
Instrumenting a function swaps the function definition var with a wrapped version of the function definition which includes tests the :args
spec from the fdef
expression.
unstrument
returns the function definition to the original form and tests for that function are no longer run.
"},{"location":"clojure-spec/testing/checking%20arguments/#unit-specification-testing","title":"Unit (Specification) testing","text":"You can generate data for interactive testing with gen/sample.
"},{"location":"coding-challenges/","title":"Coding Challenges for Clojure","text":"Coding challenges are an excellent way to start learning a new language. The challenges allow you to focus on the language and not be concerned about the more general engineering aspects of software development.
Challenges are there to explore a language and practice your understanding of how to assemble working code. It is recommended to try different approaches to solving a challenges and even repeat the same challenges at a later date and see what additional approaches you have learned.
Exercism.io and 4Ever-Clojure are highly recommended starting point for learning Clojure and does not require any installation or setup. 4Ever-Clojure is a new implementation of 4Clojure.com.
"},{"location":"coding-challenges/#practicalli-challenges","title":"Practicalli Challenges","text":"Challenge that can be solved in a few hours (or less). Use a Clojure REPL connected editor to help solve these challenges.
- Simple Projects
- TDD Code Kata
"},{"location":"coding-challenges/#approach-to-solving-challenges","title":"Approach to solving challenges","text":"Take a few minutes to digest the description of the challenge and make notes.
Identify the simplest possible thing to do, solving the problem in many small pieces which encourages experimentation
- experiment, write code and evaluate to see what it does (optionally create a comment with the result)
- use a rich comment
(comment ,,,)
to capture multiple ideas and designs, even failed experiments can be useful (separates experiments from working code) - continually evaluate code as expressions are written, to ensure their behaviour is understood (try different argument values for functions)
- try different shapes of data
- transform data shapes to keep function definitions simpler
Once there is a working solution, refactor or try different approaches and evaluate the merit of alternative solutions
"},{"location":"coding-challenges/#challenge-websites","title":"Challenge websites","text":"A local Clojure development environment supports solving challenge websites, e.g Clojure CLI and a Clojure REPl connected editor
Challenge website Description Requirements 4Ever-Clojure Learning the core functions of the Clojure language Web Browser Exercism Coding exercises with mentor support Web Browser & Exercim CLI ClojureScript Koans Interactive exercises in a web browser Web Browser Advent of Code Yearly coding challenge with a seasonal theme Clojure aware editor CodeWars Mostly math-based coding challenges with Clojure variants Web Browser"},{"location":"coding-challenges/advent-of-code/","title":"Advent Of Code","text":""},{"location":"coding-challenges/advent-of-code/#advent-of-code","title":"Advent Of Code","text":"Advent of Code is the annual coding challenge with a festive theme. Each day there is a new challenge in two parts, the first fairly easy the second a little more involved. The challenges are an investment of your time to complete them all, although even trying just a few is enough to help you think in different ways.
Every programming language requires regular practice to maintain your skills. A full time developer role gives lots of opportunities to practice every day, however, its often focused in around solving problems within a specific business domain, with little time to explore others. The Advent of Code puts you in a different domain, so its great for extending your coding experiences.
Solving challenges in a different language is another great way to extend your experiences, so here are some tips and examples for solving the advent of code in Clojure.
"},{"location":"coding-challenges/advent-of-code/#solving-challenges","title":"Solving challenges","text":" - Keep the solution as simple as possible. Its very easy to over-complicate the solution and end up simply confusing yourself.
- Don't try and make the perfect solution. Write something that works, this will give you a nice ego boost. Then you can experiment with the code and see if you can improve your approach.
- Break down the problem into the simplest thing you can solve first. Trying to solve a problem all at once will quickly have you going around in circles.
- Keep all the code and make notes. I use a a design journal in my projects to document my thinking process, capture decisions that worked and those that didn't work for this project. The journal is a great way to cement learning from solving the challenge.
- Challenges are only accessible from their day of the month onwards. There is a count-down clock displayed on the next challenge to open, so you know when it will be available. Don't feel pressured to keep up with the challenges though, enjoy the experience and have fun, you will learn more that way.
"},{"location":"coding-challenges/advent-of-code/#coding-video","title":"Coding video","text":"A video guide to solving the first challenge of Advent of Code from 2018, trying out different solutions at increasing levels of abstraction. With each level of abstraction it helps to think in a more functional way.
"},{"location":"coding-challenges/advent-of-code/#creating-a-project-for-the-challenge","title":"Creating a project for the challenge","text":"clojure -T:project/create :template lib practicalli.advent-of-clojure-code/2019\n
Create a new Clojure file for each of the daily challenges. It makes sense to keep both parts of each day in the same file.
Practicalli Advent Of Code solutions repository
practicalli/advent-of-clojure-code-2019
"},{"location":"coding-challenges/advent-of-code/#useful-resources-and-examples","title":"Useful Resources And Examples","text":"Videos and code solutions to many challenges from 2019 and past years.
- fdlk/advent-2019 - example Clojure solutions to the advent of code
- Awesome Advent Of Code - a collection of solutions in various languages
- Advent of Code 2018 video walk-through of Clojure solutions by Tim Pote and GitHub repository
#adventofcode channel in the Clojurians slack channel discusses challenges and solutions, especially during December when the challenge takes place.
"},{"location":"coding-challenges/koans/","title":"ClojureScript Koans","text":"Koans are a collection of small challenges that slowly increase in complexity. They are similar to the 4Clojure challenges in scope.
"},{"location":"coding-challenges/koans/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"coding-challenges/4clojure/","title":"Coding Challenges: 4Clojure","text":"4Ever-Clojure Challenges Website
4Ever-Clojure is a simple website with 150 challenges to help discover the functions built-in to the Clojure language, the Clojure API.
The website is self-contained with nothing to install, simply paste in the missing code and run the tests. One piece of code should solve all the tests for that challenge.
The Problem List shows the challenges categorized by experience level required, (Elementary, Easy, Medium, Hard) to solve them. Start with the easiest problem or work your way through the challenges in any order you wish. The Status column tracks your progress thorugh the challenges.
Select the name of a challenge to see the description and one or more code tests that must pass.
Enter the code that should be inserted where the __
double underscore characters are.
Press the Run button to see if the code satisfies the tests
A dialog box is displayed showing how many tests have passed and failed
Start learning the Clojure API
There are over 600 functions in the clojure.core
namespace alone, with additional functions in many other namespaces that make up the https://clojure.github.io/clojure/. It is not required to learn all these functions to be productive in Clojure.
4ever-clojure replaces 4Clojure 4Ever-Clojure is a new implementation of 4Clojure.com which has now been decommissioned
"},{"location":"coding-challenges/4clojure/#help-completing-the-challenges","title":"Help completing the challenges","text":"Look at the Clojure Cheatsheet and Clojure API for an understanding of what functions are available in the core of the Clojure language.
Search directly in ClojureDocs for functions. Each function has a page that describes the function, shows the arguments it takes and provides many examples of its use. At the end of the page are related functions too.
Practicalli Code walk-through and solution journal
practicalli/four-clojure code journals for the first 60 challenges contains a design journal showing how each challenge was solved and additional refactor or alternative approaches to the solution.
Practicalli 4Clojure guides playlist provides video walk-through of the first 64 challenges, again with alternative solutions where relevant.
An Internet search of clojure topic
, where topic
is a name of the thing you want to do, should return many examples of functions that could be useful to solving the challenge. Or
Help from the community
Clojure community - getting help covers several sources of help from the Clojure community.
"},{"location":"coding-challenges/4clojure/#using-let-and-anonymous-functions","title":"Using let and anonymous functions","text":"The solution submitted should be a single form, which is inserted in the test code where the __
underscore placeholder is. It is therefore not possible to define data with def
or a separate function with defn
to support the submitted solution.
Use the anonymous function, (fn [])
, to define behaviour.
(fn [value1 value2]\n (* value1 value2))\n
Use let to bind a name to a value, so that value can be re-used throughout the expression. let
is also useful for breaking the algorithm into smaller pieces, making it easier to solve the challenge.
(let [name value]\n (* 2 value (/ value 4) (+ value 3)))\n
It is common to combine fn
and let
to solve the challenges as they grow in complexity
(fn fibonacci [length-of-series]\n (let [fib [1 1]]\n (if (< (count fib) length-of-series)\n \"iterate... to implement\"\n fib)))\n
- fn - ClojureDocs
- let - ClojureDocs
- Fibonacci sequence guide - practicalli
"},{"location":"coding-challenges/4clojure/#my-function-is-not-working","title":"My function is not working","text":"4Ever Clojure uses babashka/sci project to evaluate code on a JavaScript host. Whist this should cover 99.9% of the Clojure API there may be some code that works in a Clojure (JVM) REPL that is not supported.
Try the code in a Clojure REPL or create a Clojure project using the latest version of Clojure (1.11.x).
"},{"location":"coding-challenges/4clojure/#references","title":"References","text":" - 4Ever-Clojure
- Clojure Cheatsheet - Clojure.org
- Clojure API - Clojure.org
- practicalli/four-clojure code journals for the first 60 challenges
- 4Clojure video guides by Practicalli
- Clojure Core Library - ClojureDocs
- Clojure, The Essential Reference - Renzo Bogatti - Manning book published in 2020
"},{"location":"coding-challenges/codewars/","title":"CodeWars","text":"Coding challenges in various languages with ranking scoreboard, experience levels and voting on solutions. Many of the challenges tend toward mathematics, so may require some background research before solving them.
"},{"location":"coding-challenges/codewars/#requirements","title":"Requirements","text":"Codewars is a web browser based system in which you can write code and run tests. Sample unit tests are provided with each challenge, so its all self-contained.
Create a free account and select the language you wish to attempt challenges in. Two simple coding tests will need to be completed in order to access that specific language.
"},{"location":"coding-challenges/codewars/#challenges-dashboard","title":"Challenges Dashboard","text":"After logging in, the dashboard suggests a challenge for you at a suitable level. 8 kyu is the easiest level, the smaller the number the harder the challenge.
"},{"location":"coding-challenges/codewars/#tackling-a-challenge","title":"Tackling a challenge","text":"Read the instructions and take a look at the sample tests.
Many of the challenges have only a very basic explanation, so always review the sample unit tests to help with the understanding. The sample tests are not necessarily the full suite of tests run when testing your solution, so there may be undocumented edge cases to solve
The source and test code can be copied into a new project, as has been done with the practicalli/codewars-guides solutions
clojure -M:new lib practicalli/readability-is-king\n
Update the solution window with your solution and use the TEST button to run the sample unit tests.
The ATTEMPT button will run all the unit tests for the challenge, which may be more than the sample tests. If the attempt passes all the tests then the solution can be submitted an other solutions reviewed.
"},{"location":"coding-challenges/codewars/#tracking-progress","title":"Tracking progress","text":"View your profile page to track your progress and revisit kata challenges already completed.
"},{"location":"coding-challenges/codewars/#references","title":"References","text":"practicalli/codewars-guide - a repository of code solutions to CodeWars challenges, each challenge is its own Clojure CLI (deps.edn) project.
YouTube: CodeWars video guides Unofficial Free Code Camp Clojure Challenges
"},{"location":"coding-challenges/exercism/","title":"Exercism Challenges","text":" Exercism Clojure Track
Exercism is a learning platform for multiple programming languates (currently 67) which combines carefully crafted coding challenges and mentors who review and advise on solutions.
Solve challenges via the built-in Exercism editor.
Or download each exercise locally using the Exercism CLI, providing a Clojure CLI configured project with a test runner.
Use the Exercism CLI to submit a solution for metor feedback.
Exercism embdedded Clojure editor The Exercisim Clojure editor is powered by babashka/sci
"},{"location":"coding-challenges/exercism/#clojure-track","title":"Clojure Track","text":"All the challenges are groups into specific language tracks, including the Clojure track
Join the language track to be presented with available challenges and progress through that specific track.
"},{"location":"coding-challenges/exercism/#working-locally","title":"Working Locally","text":" Exercism Guide to working locally
Follow the Practicalli Clojure CLI Install steps (Exercism includes a similar Clojure CLI install guide)
The Exercism CLI can download a Clojure project containing the code for a specific challeng and submit the code back to exercism to confirm if the tests have passed and complete the challenge (or get feedback from a mentor).
Each challenge shows the download and submit commands Each Exercise page shows the command to download the code for that specific exercise, which is of the form
exercism download --exercise=exercise-name --track=clojure\n
Open the project source code downloaded from Exercism in a preferred Clojure editor and write a solution to solve the exercise.
clojure -X:test
command in the root of the downloaded project will run the tests supplied by the exercise
Practicalli Test Runner aliases clojure -X:test/run
runs the Kaocha test runner from the Practicalli Clojure CLI Config
clojure -X:test/watch
will automatically re-run tests when file changes are detected.
Clojure test runner covers test runner options in more detail.
Once the tests pass and you are happy with the solution, submit it to the Exercism website
exercism submit /path/to/src-file\n
"},{"location":"coding-challenges/exercism/#repl-workflow","title":"REPL Workflow","text":"Use a REPL workflow to get instant feedback on code written to make the unit test assersions pass.
Terminal UIEditor connected REPL Start a REPL via a Terminal UI in the root of the Exercism project
clojure -M:repl/rebel\n
Open the project in a Clojure aware editor and connect to the REPL process.
Open the project in a Clojure aware editor and start a Clojure REPL, e.g. jack-in
Use a rich comment
to experiment with clojure expressions that help move towards a solution, typically solving one unit test at a time. This separates experimental code from finished designs.
(comment \n ;; experiment with clojure code \n ;; evaluate expressions in the Clojure editor\n ;; and see the evalaution results inline\n)\n
Disable Linter rules
Disable Linter rules within the comment
expression that are not useful for REPL experiments.
It is common to have several implmentations of a function with the same name, so :redefined-var
is disabled.
Functions defined in the REPL experments are not ment to be used publicly (until they are copied/moved out of the comment form), so :clojure-lsp/unused-public-var
lint rule is disabled
#_{:clj-kondo/ignore [:redefined-var :clojure-lsp/unused-public-var]}\n(comment\n ,,,\n)\n
"},{"location":"coding-challenges/exercism/#support","title":"Support","text":"Mentors on the Exercism website will provide a review of your submissions and you can switch between mentor and practice modes as you prefer.
practicalli/exercism-clojure-guides contains a design journal of solutions to several Clojure exercises.
Ask for advice in the #exercism or #beginners channels of the Clojurians Slack community.
"},{"location":"coding-challenges/exercism/nucleotide-count/","title":"Nucleotide Count","text":" Clojure Track: Nucleotide Count
Given a string representing a DNA sequence, count how many of each nucleotide is present.
If the string contains characters other than A, C, G, or T then an error should be throw.
Represent a DNA sequence as an ordered collection of nucleotides, e.g. a string of characters such as \"ATTACG\".
\"GATTACA\" -> 'A': 3, 'C': 1, 'G': 1, 'T': 2\n\"INVALID\" -> error\n
DNA Nucleotide names
A
is Adenine, C
is Cytosine, G
is Guanine and T
is Thymine
Code for this solution on GitHub
practicalli/exercism-clojure-guides contains the design journal and solution to this exercise and many others.
"},{"location":"coding-challenges/exercism/nucleotide-count/#create-the-project","title":"Create the project","text":"Download the Nucleotide Count exercise using the exercism CLI tool
exercism download --exercise=nucleotide-count --track=clojure\n
Use the REPL workflow to explore solutions locally
Open the project in a Clojure aware editor and start a REPL, using a rich comment form to experiment with code to solve the challenge.
"},{"location":"coding-challenges/exercism/nucleotide-count/#starting-point","title":"Starting point","text":"Unit test code calls functions from the src
tree which must exist with the correct argument signature for the unit test code to compile successfully.
Reviewing each assertion in the unit test code identifies the function definitions required.
Exercism Unit Tests (ns nucleotide-count-test\n (:require [clojure.test :refer [deftest is]]\n nucleotide-count))\n\n(deftest empty-dna-strand-has-no-adenosine\n (is (= 0 (nucleotide-count/count-of-nucleotide-in-strand \\A, \"\"))))\n\n(deftest empty-dna-strand-has-no-nucleotides\n (is (= {\\A 0, \\T 0, \\C 0, \\G 0}\n (nucleotide-count/nucleotide-counts \"\"))))\n\n(deftest repetitive-cytidine-gets-counted\n (is (= 5 (nucleotide-count/count-of-nucleotide-in-strand \\C \"CCCCC\"))))\n\n(deftest repetitive-sequence-has-only-guanosine\n (is (= {\\A 0, \\T 0, \\C 0, \\G 8}\n (nucleotide-count/nucleotide-counts \"GGGGGGGG\"))))\n\n(deftest counts-only-thymidine\n (is (= 1 (nucleotide-count/count-of-nucleotide-in-strand \\T \"GGGGGTAACCCGG\"))))\n\n(deftest validates-nucleotides\n (is (thrown? Throwable (nucleotide-count/count-of-nucleotide-in-strand \\X \"GACT\"))))\n\n(deftest counts-all-nucleotides\n (let [s \"AGCTTTTCATTCTGACTGCAACGGGCAATATGTCTCTGTGTGGATTAAAAAAAGAGTGTCTGATAGCAGC\"]\n (is (= {\\A 20, \\T 21, \\G 17, \\C 12}\n (nucleotide-count/nucleotide-counts s)))))\n
Function definitions required to compile unit test code
src/nucleotide_count.clj(ns nucleotide-count)\n\n(defn count-of-nucleotide-in-strand\n \"Count how many of a given nucleotide is in a strand\"\n [nucleotide strand])\n\n(defn nucleotide-counts\n \"Count all nucleotide in a strand\"\n [strand])\n
"},{"location":"coding-challenges/exercism/nucleotide-count/#making-the-tests-pass","title":"Making the tests pass","text":"Select one assertion from the unit tests and write code to make the test pass.
Experiment with solutions in the comment
form and add the chosen approach to the respective function definition.
"},{"location":"coding-challenges/exercism/nucleotide-count/#counting-nucleotides","title":"Counting nucleotides","text":"Use test data from the unit test code, e.g. \"GGGGGTAACCCGG\"
How often does a nucleotide appear
Example
(map\n #(if (= % \\A) 1 0)\n \"GGGGGTAACCCGG\")\n
Add the result to get the total count
Example
(count\n (map\n #(if (= % \\A) 1 0)\n \"GGGGGTAACCCGG\"))\n
Is there a more elegant way?
When only the matching nucleotide is in the strand, then all the elements of the strand can be counted.
filter
the DNA strand with a predicate function (returns true/false) that returns only the matching nucleotide.
Example
(filter #(= % \\A) valid-nucleotides))\n
;; Count the elements in the returned sequence for the total
Example
(count\n (filter #(= % \\A) valid-nucleotides))\n
Add this code into the starting function
"},{"location":"coding-challenges/exercism/nucleotide-count/#run-unit-tests","title":"Run unit tests","text":"Run the unit tests to see if they pass. x should pass, x should fail.
"},{"location":"coding-challenges/exercism/nucleotide-count/#nucleotide-occurances","title":"Nucleotide occurances","text":"Count the occurances
\"GGGGGTAACCCGG\"
(count\n (filter (fn [nucleotide] (= nucleotide \\A))\n \"GGGGGTAACCCGG\"))\n
Define the data
(def valid-nucleotides\n \"Characters representing valid nucleotides\"\n [\\A \\C \\G \\T])\n
Exception handling required
(throw (Throwable.)) if nucleotide is \\X\n
Or use a predicate with some (some element? in the sequence)
(some #(= \\G %) valid-nucleotides)\n\n (some #{\\G} valid-nucleotides)\n
(defn count-of-nucleotide-in-strand\n [nucleotide strand]\n (if (some #(= nucleotide %) valid-nucleotides)\n (count\n (filter #(= nucleotide %)\n strand))\n (throw (Throwable.))))\n\n (count-of-nucleotide-in-strand \\T \"GGGGGTAACCCGG\")\n
Design the second function
How often does a nucleotide appear
(map\n #(if (= % \\A) 1 0)\n valid-nucleotides)\n
Add the result to get the total count
Is there a more elegant way?
(filter #(= % \\A) valid-nucleotides)\n
Count the elements in the returned sequence for the total
Design the second function
How often does a nucleotide appear
NOTE: zero must be returned when there are no appearences
Return value always in the form
{\\A 20, \\T 21, \\G 17, \\C 12}\n
"},{"location":"coding-challenges/exercism/nucleotide-count/#hammock-time","title":"Hammock time...","text":" - How often does something appear,
- how frequenct is it?
- Is there a clojure standard library for that (approx 700 functions), review https://clojure-docs.org/
(frequencies \"GGGGGAACCCGG\")\n
If there are missing nucleotides then there is no answer
What if there is a starting point
{\\A 0 \\C 0 \\G 0 \\T 0}\n
;; Then merge the result of frequencies
(merge {\\A 0 \\C 0 \\G 0 \\T 0}\n (frequencies \"GGGGGAACCCGG\"))\n
Update the function definition and run tests
"},{"location":"coding-challenges/exercism/nucleotide-count/#solutions","title":"Solutions","text":"There are many ways to solve a challenge and there is value trying different approaches to help learn more about the Clojure language.
The following solution includes filter
and frequencies
functions which are commonly used functions from the Clojure standard library.
Example Solution
src/nucleotide_count.clj(ns nucleotide-count)\n\n(def valid-nucleotides\n \"Characters representing valid nucleotides\"\n [\\A \\C \\G \\T])\n\n(defn count-of-nucleotide-in-strand\n [nucleotide strand]\n (if (some #(= nucleotide %) valid-nucleotides)\n (count\n (filter #(= nucleotide %)\n strand))\n (throw (Throwable.))))\n\n(defn nucleotide-counts\n \"Count all nucleotide in a strand\"\n [strand]\n (merge {\\A 0 \\C 0 \\G 0 \\T 0}\n (frequencies \"GGGGGAACCCGG\")))\n
"},{"location":"coding-challenges/exercism/rna-transcription/","title":"Exercise: RNA Transcription","text":" Clojure Track: Nucleotide Count
Given a DNA strand, return its RNA complement (per RNA transcription).
Both DNA and RNA strands are a sequence of nucleotides.
The four nucleotides found in DNA are adenine (A), cytosine (C), guanine (G) and thymine (T).
The four nucleotides found in RNA are adenine (A), cytosine (C), guanine (G) and uracil (U).
Given a DNA strand, its transcribed RNA strand is formed by replacing each nucleotide with its complement:
- G -> C
- C -> G
- T -> A
- A -> U
Code for this solution on GitHub
practicalli/exercism-clojure-guides contains the design journal and solution to this exercise and many others.
"},{"location":"coding-challenges/exercism/rna-transcription/#create-the-project","title":"Create the project","text":"Download the RNA transcription exercise using the exercism CLI tool
exercism download --exercise=rna-transcription --track=clojure\n
Use the REPL workflow to explore solutions locally
Open the project in a Clojure aware editor and start a REPL, using a rich comment form to experiment with code to solve the challenge.
"},{"location":"coding-challenges/exercism/rna-transcription/#designing-the-solution","title":"Designing the solution","text":"To convert a collection of values, define a hash-map where the keys are the initial DNA values and the hash-map values are the transformed RNA values. Using a hash-map in this way is often termed as a dictionary.
A string is used as a collection of character values by many of the functions in clojure.core
. The dictionary uses characters for its keys and values.
{\\G \\C \\C \\G \\T \\A \\A \\U}\n
Use the map
function to pass the dictionary over the dna string (collection of characters) to create the RNA transcription.
Use an anonymous function to wrap the dictionary and pass each a character (nucleotide) from the DNA string in turn.
(defn to-rna\n [dna]\n (map (fn [nucleotide] (get {\\G \\C \\C \\G \\T \\A \\A \\U} nucleotide))\n dna))\n
(to-rna \"GCTA\")\n
The result is returned as a sequence of characters.
Refactor the to-rna
function and add clojure.string/join
to return the RNA value as a string
(defn to-rna\n [dna]\n (clojure.string/join\n (map (fn [nucleotide] (get {\\G \\C \\C \\G \\T \\A \\A \\U} nucleotide))\n dna)))\n
Now the function returns a string rather than a collection of characters.
(to-rna \"GCTA\")\n
"},{"location":"coding-challenges/exercism/rna-transcription/#throwing-an-assertion-error-for-incorrect-nucleotide","title":"Throwing an assertion error for incorrect nucleotide","text":"In the Exercism test suite, one test checks for an AssertionError when an incorrect nucleotide is passed as part of the DNA string.
(deftest it-validates-dna-strands\n (is (thrown? AssertionError (rna-transcription/to-rna \"XCGFGGTDTTAA\"))))\n
The throw
function can be use to return any of the Java errors. An assertion error would be thrown using the following code
(throw (AssertionError. \"Unknown nucleotide\"))\n
Refactor the to-rna
function to throw an assertion error if a nucleotide if found that is not part of the dictionary.
An if
function could be used with a conditional to check if each nucleotide is one of the keys in the dictionary and throw an AssertionError if not found. This would mean consulting the dictionary twice, once for the conditional check and once for the conversion.
Is there a way to consult the dictionary once for each nucleotide?
The get
function can return a specific not-found value when a key is not found in a map.
What if the throw
function is used as the not-found value in the get
function?
(defn to-rna\n [dna]\n (clojure.string/join\n (map (fn [nucleotide ](get {\\G \\C \\C \\G \\T \\A \\A \\U} nucleotide\n (throw (AssertionError. \"Unknown nucleotide\")) ))\n dna)))\n
Unfortunately this approach will evaluate the throw expression regardless of if the nucleotide is found in the dictionary, so calling this version of the function always fails.
The or
function evaluate the first expression and if a true value is returned then any additional expressions are skipped over.
If the first expression returns false or a falsey value, i.e. nil
, then the next expression is evaluated.
Proposed Solution
(defn to-rna\n [dna]\n (clojure.string/join\n (map (fn [nucleotide](or (get {\\G \\C \\C \\G \\T \\A \\A \\U} nucleotide)\n (throw (AssertionError. \"Unknown nucleotide\"))))\n dna)))\n
Call the to-rna
function with a DNA string from the unit test code
(to-rna \"GCTA\")\n
The function should return \"CGAU\"
Call the to-rna
function with a DNA string that contains an invalid nucleotide.
(to-rna \"GCXA\")\n
An AssertionError
is thrown as the X
character does not exist in the dictionary hash-map, so the get
expression returns nil
.
"},{"location":"coding-challenges/exercism/rna-transcription/#refactor","title":"Refactor","text":"Now the function is solving unit tests, minor adjustments can be made to streamline the code.
"},{"location":"coding-challenges/exercism/rna-transcription/#hash-map-as-function","title":"Hash map as function","text":"A hash-map can be called as a function and takes a key as an argument. This acts the same as the get
function, returning the value associated to a matching key, otherwise returning nil
or the not-found value if specified.
(defn to-rna\n [dna]\n (clojure.string/join\n (map (fn [nucleotide] (or ({\\G \\C \\C \\G \\T \\A \\A \\U} nucleotide)\n (throw (AssertionError. \"Unknown nucleotide\"))))\n dna)))\n
"},{"location":"coding-challenges/exercism/rna-transcription/#anonymous-function","title":"Anonymous function","text":"The anonymous function, fn
, has a terse form.
#(* %1 %2)
is the same as (fn [value1 value2] (+ value1 value2))
This syntax sugar is often use with map
, reduce
, apply
functions as the behaviour tends to be compact and of single use.
If the function definition is more complex or used elsewhere in the namespace, then the defn
function should be used to define shared behavior.
Solution with anonymous function
(defn to-rna\n [dna]\n (clojure.string/join\n (map #(or ({\\G \\C \\C \\G \\T \\A \\A \\U} %)\n (throw (AssertionError. \"Unknown nucleotide\")))\n dna )))\n
"},{"location":"coding-challenges/exercism/rna-transcription/#named-dictionary-data","title":"Named dictionary data","text":"Replace the hard-coded hash-map by defining a name for the dictionary.
Define a name to represent the dictionary data
(def dictionary-dna-rna {\\G \\C \\C \\G \\T \\A \\A \\U})\n
Refactor the to-rna
function to use the dictionary by name.
Solution using named dictionary data
(defn to-rna\n [dna]\n (clojure.string/join\n (map #(or (dictionary-dna-rna %)\n (throw (AssertionError. \"Unknown nucleotide\")))\n dna)))\n
"},{"location":"coding-challenges/exercism/rna-transcription/#making-the-function-pure","title":"Making the function pure","text":"Its beyond the scope of the Exercism challenge, however, its recommended to use pure functions where possible.
A pure function only uses data from its arguments.
Adding a dictionary as an argument to the to-rna
function would be simple.
Pure function approach
(defn to-rna\n [dictionary dna]\n (clojure.string/join\n (map #(or (dictionary %)\n (throw (AssertionError. \"Unknown nucleotide\")))\n dna )))\n
With a dictionary as an argument the function is also more usable, as other dictionaries could be used with the function.
The function would now be called as follows
(to-rna dictionary-dna-rna \"GTGAC\")\n
"},{"location":"coding-challenges/exercism/space-age/","title":"Space Age","text":""},{"location":"coding-challenges/exercism/space-age/#topics-covered","title":"Topics covered","text":"Code for this solution on GitHub
practicalli/exercism-clojure-guides contains the design journal and solution to this exercise and many others.
"},{"location":"coding-challenges/exercism/space-age/#create-the-project","title":"Create the project","text":"Download the RNA transcription exercise using the exercism CLI tool
exercism download --exercise=rna-transcription --track=clojure\n
Use the REPL workflow to explore solutions locally
Open the project in a Clojure aware editor and start a REPL, using a rich comment form to experiment with code to solve the challenge.
"},{"location":"coding-challenges/exercism/space-age/#challenge-introduction","title":"Challenge introduction","text":"Given an age in seconds, calculate how old someone would be on:
- Earth: orbital period 365.25 Earth days, or 31557600 seconds
- Mercury: orbital period 0.2408467 Earth years
- Venus: orbital period 0.61519726 Earth years
- Mars: orbital period 1.8808158 Earth years
- Jupiter: orbital period 11.862615 Earth years
- Saturn: orbital period 29.447498 Earth years
- Uranus: orbital period 84.016846 Earth years
- Neptune: orbital period 164.79132 Earth years
So if you were told someone were 1,000,000,000 seconds old, you should be able to say that they're 31.69 Earth-years old.
"},{"location":"coding-challenges/exercism/bob/","title":"Index","text":""},{"location":"coding-challenges/exercism/bob/#bob","title":"Bob","text":"The Bob challenge involves writing a very basics example of a text parser, something that would be used for a text based adventure game.
Bob is described as a lackadaisical teenager, so responses are very limited. To create the Bob text parser we need to identify the rules that determine Bob's response.
The instructions provide some basic rules:
- Bob answers 'Sure.' if you ask him a question.
- He answers 'Whoa, chill out!' if you yell at him.
- He answers 'Calm down, I know what I'm doing!' if you yell a question at him.
- He says 'Fine. Be that way!' if you address him without actually saying anything.
- He answers 'Whatever.' to anything else.
It is important to also read through the supplied unit tests to elaborate on these rules.
"},{"location":"coding-challenges/exercism/bob/#create-the-project","title":"Create the project","text":"Download the Bob transcription exercise using the exercism CLI tool
exercism download --exercise=bob --track=clojure\n
To use the Clojure CLI tool instead of Leiningen, create a deps.edn
file containing an empty hash-map, {}
and clone Practicalli Clojure CLI Config to ~/.clojure/
.
"},{"location":"coding-challenges/exercism/bob/#rules-derived-from-the-unit-tests","title":"Rules derived from the Unit tests","text":"Reviewing all the examples from the unit tests, there are 5 rules for the Bob parser
These rules were discovered by searching through the unit test code for each reply that Bob should return, showing the tests for each reply.
Each rule also had to ensure it did not create any false positives by being true for any other reply that Bob could make, especially the whatever reply.
Name Rule description question The phrase has a ? as the last alphanumeric character, not including whitespace shouting The phrase has uppercase alphabetic characters, but no lower case alphabetic characters shouting question A combination of question and shouting silence The phrase is empty or contains characters that are not alphanumeric whatever Any phrase that does not match any of the other rules"},{"location":"coding-challenges/exercism/bob/#design-approach","title":"Design approach","text":"There are two main approaches to solving this challenge. The first is to use the clojure.string
functions to check or transform the phrase given to Bob. The second approach is to use regular expressions with functions such as re-seq
, re-find
and re-matches
.
Start by defining the rules as an expression that returns either true or false, using some of the example strings from the unit tests.
Use a let
expression to bind a name to each rule, e.g. shouting?
, question?
, silence?
. Then these names can be used in a simple cond
expression to return the appropriate phrase. Regardless of if using clojure.string
or regular expressions, the cond
code should be similar
Once you have tried this challenge for yourself, take a look at the design journal for the clojure.string approach and the regular expression approach.
Bob - clojure.string approach Bob - regular expression approach
"},{"location":"coding-challenges/exercism/bob/bob-regular-expression-approach/","title":"Bob solution - regex","text":"Solution to Bob challenge using regular expressions and the re-matches
function.
Using re-matchers
, if the string matches the pattern, then the string is returned. Otherwise nil
is returned
The regular expressions cheatsheet from Mozilla Developer Network was very helpful in understanding regular expressions
"},{"location":"coding-challenges/exercism/bob/bob-regular-expression-approach/#asking-bob-a-question","title":"Asking Bob a question?","text":"The phrase passed to Bob is a question if the last alphanumeric character is a question mark. Using a simple regular expression we can check if the last character in the string a ?
#\"\\?\"
is a literal regular expression pattern that will match a single ?
character
So the regular expression pattern will match a single ? character
(re-matches #\"\\?\" \"?\")\n
With other characters present though the pattern doesn't match.
(re-matches #\"\\?\" \"Ready?\")\n
To match ?
with other characters,
.
matches any single character except line terminators (new line, carriage return)
(re-matches #\".\\?\" \"R?\")\n
.*
matches any number of single characters one or more times,
(re-matches #\".*\\?\" \"?Ready\")\n
\\s
matches a single whitespace character and \\s*
matches multiple whitespace characters
(re-matches #\".*\\?$\" \"Okay if like my spacebar quite a bit?\")\n ;; => \"Okay if like my spacebar quite a bit?\"\n
$
is a boundary assertion so the pattern only matches the ? at the end of a string and not in the middle. However, this is not required as the re-matches
uses groups and that manages the boundary assertion.
re-matches
does not require the $
as there is an implicit boundary
(re-matches #\".*\\?\" \"Okay if like my ? spacebar quite a bit\")\n
Match if there is a single space or space type character after the ?
(re-matches #\".*\\?\\s\" \"Okay if like my spacebar quite a bit? \")\n ;; => \"Okay if like my spacebar quite a bit? \"\n
Match if there are multiple space type characters after the ?
(re-matches #\".*\\?\\s*\" \"Okay if like my spacebar quite a bit? \")\n ;; => \"Okay if like my spacebar quite a bit? \"\n
Don't match if a question mark character is not at the end of the string
(re-matches #\".*\\?\" \"Okay if like my ? spacebar quite a bit\")\n
"},{"location":"coding-challenges/exercism/bob/bob-regular-expression-approach/#shouting-a-question-at-bob","title":"Shouting a question at Bob","text":"[^a-z]
matches if there are no lower case alphabetic characters. The ^
at the start of the pattern negated the pattern.
*
any number of the proceeding pattern
[A-Z]+
any number of upper case alphabetic characters
When a phrase has all uppercase characters then we have a match
(re-matches #\"[^a-z]*[A-Z]+[^a-z]*\" \"HELLO\")\n
If there are lower case characters, even if there are uppercase characters, the pattern does not match.
(re-matches #\"[^a-z]*[A-Z]+[^a-z]*\" \"Hello\")\n
If the characters are all uppercase then the pattern matches, even if there are other non-alphabetic characters
(re-matches #\"[^a-z]*[A-Z]+[^a-z]*\" \"ABC 1 2 3\")\n
"},{"location":"coding-challenges/exercism/bob/bob-regular-expression-approach/#silence-of-the-bob","title":"Silence of the Bob","text":"\\s
matches any single whitespace character, including space, tab, form feed, line feed, and other Unicode spaces.
(re-matches #\"\\s*\" \" \\t\\t\\t\")\n
"},{"location":"coding-challenges/exercism/bob/bob-regular-expression-approach/#solution-using-regular-expressions","title":"Solution using regular expressions","text":"The re-matches
expressions with regular expressions patterns can be put into a let expression. The names are bound to the re-matches expressions which evaluated to either true
or false
The names from the let are used with a cond
function as conditions, returning the relevant reply from Bob.
For the shouting question, the and
is used to check if two names are both true.
(defn response-for\n [phrase]\n (let [;; A ? at the end of the phrase, not counting whitespace\n question (re-matches #\".*\\?\\s*\" phrase)\n\n ;; No lower case characters, at least one upper case character\n yelling (re-matches #\"[^a-z]*[A-Z]+[^a-z]*\" phrase)\n\n ;; The entire string is whitespace\n silence (re-matches #\"\\s*\" phrase)]\n\n (cond\n silence \"Fine. Be that way!\"\n (and question yelling) \"Calm down, I know what I'm doing!\"\n question \"Sure.\"\n yelling \"Whoa, chill out!\"\n :whatever \"Whatever.\")))\n
"},{"location":"coding-challenges/exercism/bob/bob-string-approach/","title":"Bob string approach","text":"Solution to Bob challenge using clojure.string
functions and Character class from Java.
"},{"location":"coding-challenges/exercism/bob/bob-string-approach/#asking-bob-a-question","title":"Asking Bob a question?","text":"The phrase passed to Bob is a question if the last alphanumeric character is a question mark.
Using a simple comparison we can check if the last character in the string a ?
(= \\? (last \"this is a question?\"))\n
However if there is whitespace after the question mark then the last
character is a whitespace and so the expression returns false
(= \\? (last \"this is still a question? \"))\n
clojure.string/trimr
will remove all the trailing whitespace from the right side of a string. Once trimmed, then our initial comparison code will work again.
(= \\? (last (clojure.string/trimr \"this is still a question? \")))\n
"},{"location":"coding-challenges/exercism/bob/bob-string-approach/#shouting-at-bob","title":"Shouting at Bob","text":"Unfortunately the clojure.string API does not have a function to check if a string is in capital letters. There is an upper-case
function, so a comparison can be made with the original string and the string returned from clojure.string/upper-case
.
Convert the string to uppercase
(clojure.string/upper-case \"watch out!\")\n
compare the uppercase version of the string with the original, if they are equal, then the original string must have been in upper case
(= \"WATCH OUT!\"\n (clojure.string/upper-case \"WATCH OUT!\"))\n
(= \"watch out!\"\n (clojure.string/upper-case \"watch out!\"))\n
There is a flaw in this approach thought, as it will give false positives for strings that should return the 'Whatever' response
(= \"1, 2, 3\"\n (clojure.string/upper-case \"1, 2, 3\"))\n
Refined rule to check that the phrase contains alphabetic characters, otherwise it is not shouting.
The java.lang.Character class has a method called isLetter that determines if a character is a letter.
The Classes and methods in java.lang
are always available within a Clojure project, without the need for specifically importing the library.
Character/isLetter
can be called as a function in Clojure, passing in a character type.
(Character/isLetter \\a)\n
To support all Unicode characters there is an isLetter method that takes an integer type. As there could be any kind of characters in the phrase, we will use the int version. This required conversing the character to an int first before calling Character/isLetter
(Character/isLetter (int \\a))\n
the some
function is used to iterate over all the characters in the phrase. As soon as a letter is found it returns true, so does not need to process the whole phrase unless no letter is found.
(some #(Character/isLetter (int %)) phrase)\n
"},{"location":"coding-challenges/exercism/bob/bob-string-approach/#silence-of-the-bob","title":"Silence of the Bob","text":"clojure.string/blank?
is a predicate function that returns true if a string is empty or contains only whitespace. It also returns true for a nil
value.
"},{"location":"coding-challenges/exercism/bob/bob-string-approach/#final-solution","title":"Final solution","text":"Each of the rules is bound to a name that represents either a true or false value returned from each expression.
The cond
expression then evaluates the local names to see if they are true or false. The first true value found returns the string associated with the name.
For the shouting question, the and
is used to check if two names are both true.
(defn response-for [phrase]\n (let [phrase (string/trimr phrase)\n silence? (string/blank? phrase)\n question? (= \\? (last phrase))\n letters? (some #(Character/isLetter (int %)) phrase)\n shouting? (and (= phrase (string/upper-case phrase))\n letters?)]\n (cond\n (and shouting? question?) \"Calm down, I know what I'm doing!\"\n silence? \"Fine. Be that way!\"\n shouting? \"Whoa, chill out!\"\n question? \"Sure.\"\n :else \"Whatever.\")))\n
The first let binding, phrase
over-rides the name of the argument to the function. This is not that common an approach as over-riding can lead to confusion. However, in this relatively simple example it feels okay to do. The over-ride is the first let binding and it is preparing the string for all the other let bindings to use.
Over-riding names of functions from the Clojure standard library is not recommended as this does lead to much confusion.
"},{"location":"coding-challenges/palindrome/","title":"Project Palindrome","text":"In this section we will create a simple Clojure project using Leiningen and build up a palindrome checker step by step.
We will start with the simplest possible thing we can create and steadily add
"},{"location":"coding-challenges/palindrome/#what-is-a-palindrome","title":"What is a Palindrome","text":"For this project it is assumed that a palindrome is a string of characters from the English alphabet and not any other language or an alphanumeric sequence.
It is assumed that a palindrome is at least 3 characters long, meaning a single character cannot be a palindrome. If a single character was a palindrome, then any valid sequence would contain at least as many palindromes as characters in that sequence.
"},{"location":"coding-challenges/palindrome/#challenge","title":"Challenge","text":"Write an algorithm to find the 3 longest unique palindromes in a string. For the 3 longest palindromes, report the palindrome, start index and length in descending order of length. Any tests should be included with the submission.
For example, the output for string, \"sqrrqabccbatudefggfedvwhijkllkjihxymnnmzpop\"
should be:
Text: hijkllkjih, Index: 23, Length: 10\nText: defggfed, Index: 13, Length: 8\nText: abccba, Index: 5 Length: 6\n
- Check for a palindrome
- Generate a series of palindromes
"},{"location":"coding-challenges/palindrome/simple-palindrome-test/","title":"Simple palindrome test","text":""},{"location":"continuous-integration/","title":"Continuous Integration","text":"Topics to be covered in this section include:
- Continuous Integration services
- Circle CI
- GitHub Workflow
- GitLab CI
- Configure deployment pipelines
- Manage environment variables
- Security & Secrets
- Deployment
- Amazon AWS
- Render.com
CircleCI example in Practicalli Clojure Web Services
Banking on Clojure is an example of Continuous Integration using CircleCI, with LambdaIsland/Kaocha as the test runner and Heroku as the deployment pipeline.
"},{"location":"continuous-integration/#12-factor-approach","title":"12 Factor approach","text":"Following the 12 factor principles, the deployment is driven by source code to multiple environments.
"},{"location":"continuous-integration/#circleci-service","title":"CircleCI service","text":"Use Yaml language to write CI workflows and tasks, using Docker images as a consistent run-time environment
A commercial service with a generous free Cloud plan - (6,000 minutes), providing highly optomises container images to run tasks efficiently. The CircleCI Clojure images contain Clojure CLI, Leiningen and Babashka pre-installed.
CircleCI Orbs package up common configuration and tools, greatly simplifying the configuration and maintenance required.
CircleCI Clojure language guide
"},{"location":"continuous-integration/#github-workflow","title":"GitHub Workflow","text":"Use Yaml language to write CI workflows and tasks.
A commercial service with a modest free plan (2,000 minutes) for open source projects. GitHub Marketplace contains a wide range of Actions, including Clojure related actions, simplifying the configuration of CI.
Setup Clojure provides Clojure CLI, Leinigen and boot tools for use within the CI workflow
GitHub Actions overview
"},{"location":"continuous-integration/circle-ci/","title":"Circle CI continuous integration service","text":"Circle CI is a service to build, test and deploy projects. CircleCI uses docker images to run its workflow, either in the cloud or locally.
Projects can be build, tests run, artifacts (uberjars) created and applications deployed to services such as AWS, Render.com, etc.
Integration will be supported by Git version control, a continuous integration service (CircleCI, GitLabs, GitHub Actions) and a deployment platform (Heroku).
"},{"location":"continuous-integration/circle-ci/#getting-started","title":"Getting Started","text":"Sign up using a GitHub or Bitbucket account and login to the CircleCI dashboard.
Add Project in the CircleCI dashboard to configure a shared Git repository and run build pipelines using a .circleci/config.yml
file in the root of the Clojure project.
Every time changes are pushed to the shared code repository (GitHub, Bitbucket), CirceCI will run the pipeline for the project and show the results.
"},{"location":"continuous-integration/circle-ci/#clojure-images","title":"Clojure images","text":"Clojure specific container images are available for several versions of Java and Clojure. Pre-configured images are typically faster than installing software on top a more generic image.
cimg/clojure:1.10
is the recommended image for Clojure projects. The image contains OpenJDK 17 and the latest version of Clojure CLI, Leiningen and Babashka
Add the following under the docker:
key in the config.yml
- image: cimg/clojure:1.10\n
The CircleCI Clojure Language guide walks through the sections of the yaml configuration in detail.
Check Clojure version clojure -Sdescribe
shows configuration information for the Clojure CLI tool as a hash-map, with the :version key associated with the exact install version.
lein version
shows the current version of Leiningen install on your development environment.
java -version
shows the current version of the Java installation.
"},{"location":"continuous-integration/circle-ci/#references","title":"References","text":"CircleCI Clojure Language guide CircleCI Clojure image tags - json CircleCI Clojure images CircleCI dockerfiles repository
"},{"location":"continuous-integration/circle-ci/circle-ci-sample-project/","title":"Circle CI example project","text":"The Circle CI language guide for Clojure provides an example project that is managed by the Leiningen build automation tool and based on the Luminus micro-framework template.
The project runs on the Undertow web server (wrapped by immutant), using ring to manage web requests and responses, with compojure for server-side routing. The application uses mount to manage the application lifecycle.
Fork the CircleCI-Public/circleci-demo-clojure-luminus project on your GitHub or GitLab account or organisation.
Go to the CircleCI dashboard and login. Select the GitHub / GitLab organisation you want to work with, this will list the repositories in that organisation.
Find the project in the list of repositories for that organisation
Click on the \"Set Up Project\" button and select the Clojure configuration from the drop-down menu.
This template seems to be older than the sample configuration on the Clojure language page. Copy the sample configuration and paste it into the editor. Then press Add Config to automatically add it to your code repository.
This will start a build and show the pipelines dashboard, with the project running the tasks defined in the configuration
Oh, it failed...
Clicking on the FAILED button shows details of that particular pipeline. This opens the build report for the pipeline.
The build report shows all the steps that have passed and the details of the step which has failed, in this case lein do test, uberjar
Edit the .circleci/config.yml
file in your fork and change the images used to openjdk-8-lein-2.9.3
.
Then commit the change to the code in the code repository. Another build will run automatically.
The dashboard shows the second build of this pipeline, showing the new commit just pushed
Success. Now development can continue knowing the configuration of the pipeline works.
"},{"location":"continuous-integration/circle-ci/circle-ci-sample-project/#hintfailing-on-java-11","title":"Hint::Failing on Java 11","text":"The example project only seems to run on Java 8. Running the project locally with either lein run
or lein test
"},{"location":"continuous-integration/circle-ci/circle-ci-sample-project/#hintcannot-edit-configuration-via-dashboard","title":"Hint::Cannot edit configuration via dashboard","text":"Apart from the initial creation of the configuration, its not possible to edit the configuration via the dashboard.
"},{"location":"continuous-integration/circle-ci/detailed-config/","title":"Circle CI detailed configuration","text":""},{"location":"continuous-integration/circle-ci/detailed-config/#orbs","title":"Orbs","text":"CircleCI Orbs are pre-packaged configurations for specific tasks, reducing the amount of configuration to maintain.
Orbs Description h-matsuo/github-release@0.1.3 GitHub release - package up releases ? Deploy to Heroku Kaocha test runner - unit and generative testing, junit-xml reports and test coverage"},{"location":"continuous-integration/circle-ci/detailed-config/#executors","title":"Executors","text":"Define an executor for the environment used to run the CircleCI jobs.
Executor environment Description machine Linux virtual machine docker Configuration for a Clojure CLI project
---\nversion: 2.1\n\norbs:\n github-release: h-matsuo/github-release@0.1.3\n\nexecutors:\n tools-deps-executor:\n docker:\n - image: circleci/clojure:openjdk-11-tools-deps-1.10.1.697\n working_directory: ~/repo\n environment:\n JVM_OPTS: -Xmx3200m\n\ncommands:\n setup:\n steps:\n - checkout\n - restore_cache:\n keys:\n - v1-dependencies-{{ checksum \"deps.edn\" }}\n - v1-dependencies-\n - save_cache:\n paths:\n - ~/.m2\n key: v1-dependencies-{{ checksum \"deps.edn\" }}\n\n acceptance-tests:\n steps:\n - run:\n name: check coverage\n command: clojure -M:test:coverage\n\n deploy-version:\n steps:\n - run:\n name: Update pom.xml\n command: clojure -Spom\n - run:\n name: Build\n command: clojure -M:jar\n - run:\n name: Deploy\n command: clojure -M:deploy\n\n store-artifact:\n steps:\n - run:\n name: Create jar\n command: clojure -M:jar\n - run:\n name: Zip up jar file\n command: zip --junk-paths github-api-lib github-api-lib.jar\n - run:\n name: install mvn\n command: |\n sudo apt-get update\n sudo apt-get -y install maven\n - run:\n name: extract version from pom\n command: |\n mvn help:evaluate -Dexpression=project.version -q -DforceStdout > current_version\n - persist_to_workspace:\n root: ~/repo\n paths:\n - github-api-lib.zip\n - current_version\n\n create-release:\n steps:\n - attach_workspace:\n at: ~/repo\n - github-release/create:\n tag: \"v$(cat ~/repo/current_version)\"\n title: \"Version v$(cat ~/repo/current_version)\"\n description: \"Github-related API calls.\"\n file-path: ~/repo/github-api-lib.zip\n\n\n\njobs:\n test:\n executor: tools-deps-executor\n steps:\n - setup\n - acceptance-tests\n deploy:\n executor: tools-deps-executor\n steps:\n - setup\n - acceptance-tests\n - deploy-version\n artifact:\n executor: tools-deps-executor\n steps:\n - setup\n - store-artifact\n release:\n executor: github-release/default\n steps:\n - setup\n - create-release\n\n\nworkflows:\n build-test-release:\n jobs:\n - test\n - deploy:\n context: clojars\n requires:\n - test\n filters:\n branches:\n only: main\n - artifact:\n requires:\n - test\n filters:\n branches:\n only: main\n - release:\n context: github\n requires:\n - test\n - deploy\n - artifact\n filters:\n branches:\n only: main\n
"},{"location":"continuous-integration/circle-ci/random-clojure-function/","title":"Random Clojure Function","text":"A Clojure command line application that shows a random function from the namespaces available in the Clojure Standard library, or a specific namespace from that library.
Random Clojure Function repository
This guide shows how to develop this project alongside CircleCI as the continuous integration service.
- Create a new project - using the Random Clojure Function guide
- Create a repository on GitHub
- Commit the project early and push changes to GitHub
- Add a .circleci/config.yml file and push to GitHub, choosing the relevant image
- Login to CircleCI dashboard and add project, choosing manual configuration
- Continue developing the random clojure function project, including tests
- After each push to GitHub, check the build status
- Add a CircleCI badge to the project readme
Video uses an older command to create projects
:project/create
alias from Practicalli Clojure CLI Config creates a new project
Arguments are key value pairs and can specify the :template
, project :name
and outpug directory :output-dir
.
clojure -T:project/new :template app :name practicalli/playground`\n
"},{"location":"continuous-integration/circle-ci/random-clojure-function/#create-a-new-project","title":"Create a new project","text":"Start following the guide to create the random clojure function project, using a deps.edn for the Clojure project configuration
clojure -T:project/new :template app :name practicalli/random-clojure-function\n
Version control the Clojure project using Git (or magit in Spacemacs)
"},{"location":"continuous-integration/circle-ci/random-clojure-function/#add-a-test-run-alias","title":"Add a test run alias","text":"Edit the deps.edn
file in the root of the project and add a :test/run
alias, to run the kaocha test runner which will stop if a failing test is detected. Stopping on a failed test saves running the full test suite and make the CI workflow more effective.
Project deps.edn:test/run\n{:extra-paths [\"test\"]\n :extra-deps {lambdaisland/kaocha {:mvn/version \"1.60.977\"}}\n :exec-fn kaocha.runner/exec-fn\n :exec-args {:randomize? false\n :fail-fast? true}}\n
"},{"location":"continuous-integration/circle-ci/random-clojure-function/#create-a-remote-repository","title":"Create a remote repository","text":"Add the remote repository URL to the local Git repository.
git remote add practicalli git@github.com:practicalli/random-clojure-function.git\n
"},{"location":"continuous-integration/circle-ci/random-clojure-function/#add-circleci-configuration","title":"Add CircleCI configuration","text":"Adding CircleCI early in the project development cycle ensures testing from the saved source code is successful and testing is consistently repeatable.
Create a new file called .circleci/config.yaml
in the root of the project.
Edit the file and add the following configuration.
Circe CI configuration for Clojure project
``yaml title=\".circleci/config.yaml\" version: 2.1 jobs: # basic units of work in a run build: # runs without Workflows must have a
buildjob as entry point working_directory: ~/random-clojure-function # directory where steps will run docker: # run the steps with Docker - image: cimg/clojure:1.10 # image is primary container where
stepsare run environment: # environment variables for primary container JVM_OPTS: -Xmx3200m # limit maximum JVM heap size to prevent out of memory errors steps: # commands that comprise the
build` job - checkout # check out source code to working directory - restore_cache: # restores cache if checksum unchanged from last run key: random-clojure-function-{{ checksum \"deps.edn\" }} - run: clojure -P - save_cache: # generate / update cache in the .m2 directory using a key template paths: - ~/.m2 - ~/.gitlibs key: random-clojure-function-{{ checksum \"deps.edn\" }} - run: clojure -X:test/run
```
run: clojure -P
step downloads dependencies for the project, including the :extra-deps
if aliases are also included.
run: clojure -X:test/run
adds the test directory to the class path and runs the Kaocha runner defined in the alias.
"},{"location":"continuous-integration/circle-ci/random-clojure-function/#connect-circle-ci-to-the-project","title":"Connect Circle CI to the project","text":"Commit and push the .circleci/config.yml
file to the GitHub repository.
Open the CircleCI dashboard and select Add Project. If your GitHub account has multiple organizations, choose the appropriate organization first.
Search the repository list for the GitHub repository and select ,,,
Select the Manual configuration as a .circleci/config.yml
file has already been added to the Git repository.
Press Start Building button to confirm that a config.yml
file has already been added and the build should start.
Now the first build runs with the config.yml
file.
Its failed. Okay lets investigate...
Thats okay, we have failing tests locally, so we know that the CircleCI build is working the same as on our local development environment.
The continuous integration is now working and tests are automatically run as soon as you push changes to the remote repository.
So the development of the project can continue with greater confidence
"},{"location":"continuous-integration/circle-ci/random-clojure-function/#adding-a-build-status-badge","title":"Adding a Build Status badge","text":"Generating a status badge documentation describes how to add a build status badge for your project, usually at the top of the README.md file in the project
[![CircleCI](https://circleci.com/gh/circleci/circleci-docs.svg?style=svg)](https://circleci.com/gh/practicalli/random-clojure-function)\n
Add this markdown to the top of the README.md file, underneath the title. Then commit and push the change to the GitHub repository.
NOTE: you might want to fix the unit tests first :)
"},{"location":"continuous-integration/circle-ci/status-monitor/","title":"Status Monitor Circle CI Continuous Integration","text":"practicalli/webapp-status-monitor is a Leiningen project create in October 2018.
The project uses ring and compojure as the basis of a web application.
Configured with a project.clj file.
(defproject status-monitor \"0.1.0-SNAPSHOT\"\n :description \"A server side website dashboard to collate monitoring information\"\n :url \"https://github.com/jr0cket/status-monitor\"\n :min-lein-version \"2.0.0\"\n :dependencies [[org.clojure/clojure \"1.9.0\"]\n [compojure \"1.6.1\"]\n [ring/ring-defaults \"0.3.2\"]\n [hiccup \"1.0.5\"]]\n :plugins [[lein-ring \"0.12.4\"]\n [lein-eftest \"0.5.3\"]]\n :ring {:handler status-monitor.handler/app}\n :profiles\n {:dev {:dependencies [[javax.servlet/servlet-api \"2.5\"]\n [ring/ring-mock \"0.3.2\"]]}})\n
"},{"location":"continuous-integration/circle-ci/status-monitor/#circleci-configuration","title":"CircleCI Configuration","text":"This configuration uses the CircleCI specific docker image with Java 17 and the latest version of Leiningen.
The configuration defines that the code will be check out, Leiningen will download the dependencies and then run unit tests.
version: 2\njobs:\n build:\n docker:\n - image: cimg/clojure:1.10\n\n working_directory: ~/repo\n\n environment:\n LEIN_ROOT: \"true\"\n # Customize the JVM maximum heap limit\n JVM_OPTS: -Xmx3200m\n\n steps:\n - checkout\n\n # Download and cache dependencies\n - restore_cache:\n keys:\n - v1-dependencies-{{ checksum \"project.clj\" }}\n # fallback to using the latest cache if no exact match is found\n - v1-dependencies-\n\n - run: lein deps\n\n - save_cache:\n paths:\n - ~/.m2\n key: v1-dependencies-{{ checksum \"project.clj\" }}\n\n # run tests!\n - run: lein test\n
"},{"location":"continuous-integration/circle-ci/status-monitor/#caching-dependencies","title":"Caching dependencies","text":"CircleCI create a cache of downloaded dependencies, to help speed up the running of the project.
The config.yml defines the path where the dependencies are saved. A unique key is used to identify the dependencies cache.
"},{"location":"continuous-integration/github-workflow/","title":"GitHub Workflows","text":"Automate tasks, such as running unit tests or lint code, whenever code is committed to a GitHub repository.
GitHub Actions can run one or more tasks after specific events, such as commits, raising issues or pull requests.
An event triggers a configured workflow which contains one or more jobs. A job contains a one or more steps which defines actions to run.
Practicalli GitHub Workflow Examples Practicalli recommended GitHub Actions
Introduction to GitHub Actions Understanding the workflow file
"},{"location":"continuous-integration/github-workflow/#anatomy-of-a-workflow","title":"Anatomy of a workflow","text":"Term Description Event Triggers a workflow, e.g. Create pull request, push commit, etc. Workflow Top level configuration containing one or more jobs, triggered by a specific event Job Set of steps executed in the same runner, multiple jobs execute in parallel within their own instance of a runner Step Individual task that runs commands (actions), sharing data with other steps Action Standalone commands defined within a step, custom commands or GitHub community Runner A GitHub Actions server, listening for available jobs"},{"location":"continuous-integration/github-workflow/#example-github-action","title":"Example GitHub Action","text":".github/workflows/workflow-name.yaml
is a file that contains the workflow definition.
Setup Java adds an OpenJDK distribution, i.e. Eclipse Temurin, at a specified version (Java 17 recommended).
Setup Clojure provides Clojure via Clojure CLI, Leiningen or Boot. Clojure CLI is recommended.
Cache is used to cache Clojure and Java libraries
- The example workflow runs on Ubuntu.
- The project code is checked out from the Git repository.
- Java and Clojure run-times are added to the environment
- Unit tests are run using the
:test/run
alias (this alias should run Kaocha or similar test runner) - Source code format and idioms are checked with cljstyle and clj-kondo
- The Clojure project is packaged into an Uberjar for deployment
Example GitHub workflow for Clojure CLI project
name: Test and Package project\non:\n pull_request:\n push:\n branches:\n - main\njobs:\n clojure:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Cache Clojure Dependencies\n uses: actions/cache@v3\n with:\n path:\n - ~/.m2\n - ~/.gitlibs\n key: cache-${{ hashFiles('**/deps.edn') }}\n restore-keys: clojure-deps-\n\n - name: Prepare java\n uses: actions/setup-java@v3\n with:\n distribution: 'temurin'\n java-version: '17'\n\n - name: Install clojure tools\n uses: DeLaGuardo/setup-clojure@9.5\n with:\n cli: 1.11.1.1165 # Clojure CLI based on tools.deps\n cljstyle: 0.15.0 # cljstyle\n clj-kondo: 2022.10.05 # Clj-kondo\n\n - name: Run Unit tests\n run: clojure -X:test/run\n\n - name: \"Lint with clj-kondo\"\n run: clj-kondo --lint deps.edn src resources test --config .clj-kondo/config-ci.edn\n\n - name: \"Check Clojure Style\"\n run: cljstyle check --report\n\n - name: Package Clojure project\n run: clojure -X:project/uberjar\n
"},{"location":"continuous-integration/github-workflow/#references","title":"References","text":" - Practicalli Blog - publish blog workflow - build publish a Cryogen project with Clojure CLI and publish the generated website with GitHub pages (also a Staging workflow that runs on pull requests)
- Practicalli Landing Page GitHub workflow - build a ClojureScript & Figwheel project with Clojure CLI and publish the generated site to GitHub pages
- Practicalli Clojure CLI config - lint with clj-kondo workflow - lint the
deps.edn
file with clj-kondo
"},{"location":"control-flow/cond/","title":"cond","text":""},{"location":"control-flow/if/","title":"if","text":""},{"location":"control-flow/when/","title":"when","text":""},{"location":"core.async/","title":"Introducing core.async","text":"The core.async
library provides a way to do concurrent programming using channels (eg. queues).
It minimises the need to use complex concurrent constructs and worry less about thread management.
core.async
is written in Clojure and can be used with both Clojure and ClojureScript.
- core.async getting started
- Introduction to asynchronous programming
- ClojureScript core.async and todos - Bruce Haurman
- core.async 101
- Mastering concurrent processes
- LispCast Clojure core.async: Channels - first lesson only.
- core.async introduction in ClojureScrpt unravelled
- core.async: Concurrency without Callbacks - Stuart Halloway
- David Nolan - core.async for asynchronous programming
- Timothy Baldridge - Core.Async
- core.async in Use - Timothy Baldridge
- Timothy Baldridge - core.async - pipelines - free video
- Timothy Baldridge - core.async - garbage collected go blocks - free video
"},{"location":"core.async/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"Pull requests are welcome
"},{"location":"core.async/#hint-coreasync-resources","title":"Hint:: core.async resources","text":""},{"location":"core.async/#hintcommunicating-sequential-processes","title":"Hint::Communicating Sequential Processes","text":"Communicating Sequential Processes (CSP) is a formalism for describing concurrent systems pioneered by C. A. R. Hoare in 1978. It is a concurrency model based on message passing and synchronization through channels
"},{"location":"core.async/#coreasync-on-clojurescript","title":"core.async on ClojureScript","text":"core.async is very widely used within ClojureScript applications and many libraries are built on top of it.
It\u2019s a good example of the syntactic abstractions that can be achieved by transforming code with ClojureScript macros.
JavaScript is single-threaded so you do not get the benefit of safe communication between threads, as there is only one.
"},{"location":"core.async/#concepts","title":"Concepts","text":""},{"location":"core.async/#channels","title":"Channels","text":"A channel is a queue with one or more publishers and one or more consumers. Producers put data onto the queue, consumers take data from the queue.
As data in Clojure is immutable, channels provide a safe way to communicate between threads.
"},{"location":"core.async/#chanel-size","title":"Chanel size","text":"Channels do not include a buffer by default, they use a producer (put!
) and consumer (take!
) to transfer a value through the channel. A maximum of 1024 put!
functions can be queued onto a single channel.
Specify a channel using (chan)
or a channel with a fixed buffer using (chan 12)
.
"},{"location":"core.async/#processes","title":"Processes","text":"Processes are independently running pieces of code that use channels for communication and coordination.
Calling put!
and take!
inside a process will stop that process until the operation completes.
Processes are launched using the go macro and puts and takes use the <! and >! placeholders. The go macro rewrites your code to use callbacks but inside go everything looks like synchronous code, which makes understanding it straightforward:
In ClojureScript, stopping a process doesn\u2019t block the single-threaded JavaScript environment, instead, the process will be resumed at a later time when it is no longer blocked.
"},{"location":"core.async/#important-functions","title":"Important functions","text":""},{"location":"core.async/#chan","title":"chan
","text":"The chan
function creates a new channel.
You can give a name to a channel using the def
function, eg. (def my-channel (chan))
A single channel can take a maximum of 1024 put requests. Once it has reached the maximum, then it is considered full.
A buffer of a fixed size can be specified when defining a new channel: (def channel-with-fixed-buffer (chan 12))
. This buffer increases the number of puts that can be sent to the channel. A dropping or sliding buffer can be used to discard messages added when the buffer is full.
A channel can also include a transducer, to manipulate the value on the channel eg. adding a timestamp (chan 32 (map (Date. now)))
eg. filtering messages (chan 12 (map even?))
Channels can be explicitly closed using (close channel-name)
or by adding a timeout that closes the channel after the specified number of milliseconds (timeout 6000)
.
"},{"location":"core.async/#put","title":"put!
","text":"The put!
function puts a value (message) on the channel.
You can put messages on the channel even if nothing is listening (no waiting take!
functions).
Evaluating put!
will always add a message on to the channel as long as the channel is open and not full.
"},{"location":"core.async/#take","title":"take!
","text":"The take!
function will take a single message from the queue.
If there are no messages on the queue when you evaluate take!
, then the function will wait to execute as soon as something is put on the channel
The take!
function needs an argument that is the channel and a function that will receive any message taken from a channel.
"},{"location":"core.async/#time","title":"time
","text":"This is a macro that executes an expression and prints the time it takes
Criterium is an excellent library for performance testing your code
"},{"location":"core.async/#go","title":"go
","text":"Referred to as a go block, the go
function creates a lightweight process, not bound to threads. Thousands of go blocks can be created efficiently and they can all have their own channel.
"},{"location":"core.async/#blocking-and-non-blocking","title":"blocking and non-blocking","text":"core.async offers two ways to write to and read from channels: blocking and non-blocking. A blocking write blocks the thread until the channel has space to be written to (the buffer size of a channel is configurable), a blocking read blocks a thread until a value becomes available on the queue to be read.
More interesting, and the only type supported in ClojureScript, are asynchronous channel reads and writes to channels, which are only allowed in \"go blocks\". Go blocks are written in a synchronous style, and internally converted to a state machine that executes them asynchronously.
Consider the following core.async-based code:
(let [ch (chan)]\n (go (while true\n (let [v (<! ch)]\n (println \"Read: \" v))))\n (go (>! ch \"hi\")\n (<! (timeout 5000))\n (>! ch \"there\")))\n
In this example, let introduces a new local variable ch, which is a new channel. Within the let's scope two go blocks are defined, the first is an eternal loop that reads (<!) a new value from channel ch into variable v. It then prints \"Read: \" followed by the read value to the standard out. The second go block writes (>!) two values to channel ch: \"hi\", it then waits 5 seconds and then writes \"there\" to the channel. Waiting for 5 seconds is implemented by reading from a timeout channel, which is a channel that closes itself (returns nil) after a set timeout. When running this code in the Clojure REPL (for instance), it will return instantly. It will then print \"Read: hi\", and 5 seconds later it will print \"Read: there\".
"},{"location":"core.async/#hint","title":"Hint::","text":"In JavaScript you cannot do blocking loops like this: the browser will freeze up for 5 seconds. The \"magic\" of core.async is that internally it converts the body of each go block into a state machine and turns the synchronous-looking channel reads and writes into asynchronous calls.
"},{"location":"core.async/bike-assembly-line/","title":"core.async scenario: Bike assembly line","text":"In this example we are going to use a bicycle assembly line as the process we want to make concurrent. The tasks involved in making our bicycle are:
- Making the frame
- Painting the wheels
- Making the rims
- Making the wheels (adding hub and spokes to wheels - different hub for front and rear wheels)
- Making the handlebars
- Fitting tyres to the rims (solid rims, so no tubes)
- Attaching the handlebars to the frame
- Attaching wheels to the frame
- Attaching crank to frame
- Attaching peddles to the crank
- Connecting control system wires (gears, breaks)
"},{"location":"core.async/bike-assembly-line/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"Pull requests are welcome
"},{"location":"core.async/bike-assembly-line/#current-build-process","title":"Current build process","text":"At the moment, each person creates one bicycle all by themselves. This means they need all sorts of different tools and are switching tasks all the way through assembling the bike.
We want to move to a more parallel approach, so as we automate this process we will evaluate what actions can be done in parallel and which must be done sequentially (i.e. painting the frame must come after making the frame).
"},{"location":"core.async/clacks-messages/","title":"Clacks Messenger with core.async","text":"In a previous exercise we built a simple encoder/decoder to send messages via the Clacks message service (as designed by Sir Terry Pratchett, RIP).
Now we will use core.async to create a processing queue between each Clack towers, so we can then model, monitor and visualise a Clacks messenger system with multiple Clacks towers. For additional fun we will enable new Clacks towers to come on line and connect to the existing system.
"},{"location":"core.async/clacks-messages/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"Pull requests are welcome
"},{"location":"core.async/toy-car-assembly-line/","title":"core.async example - toy car assembly line","text":""},{"location":"core.async/toy-car-assembly-line/#noteget-the-example-project-and-open-it-in-a-clojure-repl","title":"Note::Get the example project and open it in a Clojure REPL","text":"Clone or download the Lispcast: Clojure core-async Factory example
Open that project in your Clojure editor or run lein repl
in the top level directory of the project.
"},{"location":"core.async/toy-car-assembly-line/#the-toy-car-factory","title":"The toy car factory","text":"The toy car factory assembles car parts before distributing them. How can we make this process faster and more scalable?
"},{"location":"core.async/toy-car-assembly-line/#the-current-process","title":"The current process","text":"One worker picks out random parts of a car from the parts box until all the parts are collected to assemble the car.
"},{"location":"core.async/toy-car-assembly-line/#the-time-macro","title":"The time
macro","text":"We will use the time macro to see how long parts of our code takes to run and help find parts to optimise.
A simple example would be:
(time\n (map inc (range 0 10000)))\n
"},{"location":"core.async/toy-car-assembly-line/#timing-assembly-functions","title":"Timing assembly functions","text":"Investigate the time it takes to carry out the different assembly line tasks
(time (take-part))\n\n(time (attach-wheel :body :wheel))\n\n(time (box-up :body))\n\n(time (put-in-truck :body))\n
And to total time it takes to get a a whole car through the assembly line
(time (build-car))\n
The total time can be longer than the sum of the tasks, as the take-part
function does not always give the required part needed.
"},{"location":"core.async/toy-car-assembly-line/#hiring-more-workers","title":"Hiring more workers","text":"Adding 10 more workers is equivalent to adding 10 processes that run the assembly tasks.
Lets use a go block for a worker
(do\n (go\n (dotimes [number 10]\n (println \"First go block processing:\" number)\n (Thread/sleep 1200)))\n (go\n (dotimes [number 10]\n (println \"Second go block processing:\" number)\n (Thread/sleep 1200))))\n
These are two separate go blocks, so their is no co-ordination between the two. You can see the println statement from each go block intertwined.
"},{"location":"data-inspector/","title":"Clojure Data Browsers","text":"Clojure has a strong focus on using the built in data structures (list, vector, hash-map, set) to represent information in the system. Tools to inspect data and browse through nested or large data sets is invaluable in understanding what the system is doing.
There are many clojure.core
functions that can be used to explore data structures and transform them to produce specific views on data.
tap>
and datafy
are recent additions to Clojure that provide a more elegant way of exploring data than the classic println
statement.
New tools are being created to capture and visualize results from evaluated expressions (REBL, Reveal) as well as tools to specifically visualize tap>
expressions (Reveal, Portal).
"},{"location":"data-inspector/#common-approaches","title":"Common approaches","text":" - Editor data browsers - e.g. cider-inspect
- Portal - tool to navigate and visualise data via
tap>
- Clojure inspector - built-in Java Swing based inspector
- Reveal repl with data browser, also a
tap>
source (semi-commercial project)
"},{"location":"data-inspector/clojure-inspector/","title":"Clojure Inspector","text":"A visual browser for Clojure data using the Java UI libraries.
Require the clojure.inspector
namespace in the REPL or project namespace definitions to use the functions
ReplProject (require '[clojure.inspector :as inspector])\n
(ns practicalli.application\n (:require [clojure.inspector :as inspector]))\n
inspect
for flat data structures inspect-tree
for deeply nested / hierarchically data inspect-table
a sequence of data structures with the same shape
"},{"location":"data-inspector/clojure-inspector/#inspect","title":"inspect
","text":"View flat structures especially with non-trivial size data sets.
This example generated 10,000 random numbers. The Clojure inspector shows the values along with their index in the collection.
(inspector/inspect\n (repeatedly 10000 #(rand-int 101)))\n
"},{"location":"data-inspector/clojure-inspector/#inspect-tree","title":"inspect-tree
","text":"(inspect\n {:star-wars\n {:characters\n {:jedi [\"obiwan kenobi\" \"Yoda\" \"Master Wendoo\"]\n :sith [\"Palpatine\" \"Count Dukoo\"]}}})\n
"},{"location":"data-inspector/clojure-inspector/#inspect-table","title":"inspect-table
","text":"Inspect a sequence of data structures that share the same form, often found in data sets for machine learning and wider data science, eg. daily weather records.
This example generates mock data for a 20 day period for one or more locations. Each day contains the day, location and cumulative number of cases reported.
(defn mock-data-set\n \"Generates a set of mock data for each name\n Arguments: names as strings, names used in keys\n Returns: Sequence of maps, each representing confirmed cases\"\n [& locations]\n (for [location locations\n day (range 20)]\n {:day day\n :location location\n :cases (+ (Math/pow (* day (count location)) 0.8)\n (rand-int (count location)))}))\n\n(inspector/inspect-table\n (mock-data-set \"England\" \"Scotland\" \"Wales\" \"Northern Ireland\"))\n
"},{"location":"data-inspector/portal/","title":"Portal - navigate your data","text":"Portal inspector is a tool for exploration of Clojure data using a browser window to visualise and inspect Clojure, JSON, Transit, Logs, Yaml, etc.
Registered Portal as a tap source and wrap code with (tap> ,,,)
to see the results in Portal, providing a more advanced approach to debuging than println.
Send all evaluation results to Portal for a complete history using the portal-wrap nREPL middleware
Add a custom Mulog publisher to send all logs to Portal to help with debugging.
Open Portal from the REPL or configure Portal to open on REPL startup.
Practicalli Project Templates
Clojure projects created with Practicalli Project Templates include Portal configuration to recieve all evaluation results and Mulog event logs.
A custom dev/user.clj
file loads dev/portal.clj
and dev/mulog-events.clj
configurations on REPL startup, when the dev
directory is included on the path.
Use the :repl/reloaded
for a complete REPL reloaded workflow and tooling on REPL startup
Clojure 1.10 onward required tap sources and tap> tap is a shared, globally accessible system for distributing values (log, debug, function results) to registered tap sources.
add-tap
to register a source and receive all values sent. remove-tap
to remove a source.
tap>
form sends its contents to all registered taps. If no taps are registered, the values sent by tap> are discarded.
(deref (deref #'clojure.core/tapset))
will show the tap sources. tapset
is a Clojure set defined as private var and not meant to be accessed directly.
Online Portal demo
"},{"location":"data-inspector/portal/#add-portal","title":"Add Portal","text":"Clojure CLI user configuration aliases enable Portal to be used with any Clojure or ClojureScript project.
Practicalli Clojure CLI ConfigAlias Definition Practicalli Clojure CLI Config contains several aliases that support Portal, either to start a REPL process that can send all Clojure evaluated code to Portal or simply adding Portal as a library for manual use.
Run a REPL with portal and portal.nrepl/wrap-portal
to send every REPL evaluation to Portal over an nREPL connection
:repl/reloaded
- starts a rich terminal UI REPL with Portal nREPL middleware, including REPL Reloaded tools :repl/inspect
- starts a basic REPL with Portal nREPL middleware.
Or include the portal library in clojure
commands or when starting a REPL via an editor
dev/reloaded
- Portal, including REPL Reloaded tools inspect/portal-cli
- Clojure CLI (simplest approach) inspect/portal-web
- Web ClojureScript REPL inspect/portal-node
- node ClojureScript REPL
Create portal aliases to include the portal libraries for the Clojure, ClojureScript Web browser and ClojureScript Node server libraries
Portal aliases in Clojure CLI user configuration
:inspect/portal-cli\n{:extra-deps {djblue/portal {:mvn/version \"0.34.2\"}\n clj-commons/clj-yaml {:mvn/version \"0.7.0\"}}}\n\n:inspect/portal-web\n{:extra-deps {djblue/portal {:mvn/version \"0.34.2\"}\n org.clojure/clojurescript {:mvn/version \"1.10.844\"}}\n :main-opts [\"-m\" \"cljs.main\"]}\n\n:inspect/portal-node\n{:extra-deps {djblue/portal {:mvn/version \"0.34.2\"}\n org.clojure/clojurescript {:mvn/version \"1.10.844\"}}\n :main-opts [\"-m\" \"cljs.main\" \"-re\" \"node\"]}\n\n:repl/inspect\n{:extra-deps\n {cider/cider-nrepl {:mvn/version \"0.28.5\"}\n djblue/portal {:mvn/version \"0.33.0\"}\n clj-commons/clj-yaml {:mvn/version \"0.7.0\"}}\n :main-opts [\"-m\" \"nrepl.cmdline\"\n \"--middleware\"\n \"[cider.nrepl/cider-middleware,portal.nrepl/wrap-portal]\"]}\n
Practicalli Clojure CLI Config contains several aliases that support Portal.
YAML support for Portal - Clojure only clj-commons/clj-yaml adds YAML support to Portal for Clojure on the JVM
REPL Reloaded Aliases
REPL Reloaded section includes the :repl/reloaded
and :dev/reloaded
ailas definitions
"},{"location":"data-inspector/portal/#start-repl-with-portal","title":"Start REPL with Portal","text":"Run a REPL in a terminal and include the Portal library, using the Clojure CLI tools
REPL StarupEmacs Project configurationEmacs variable Start a REPL with namespace reloading, hotload libraries and portal data inspector
clojure -M:repl/reloaded\n
Or start the REPL with only portal
clojure -M:inspect/portal:repl/rebel\n
Add cider-clojure-cli-aliases
to a .dir-locals.el
in the root of the Clojure project with an alias used to add portal
.dir-locals.el
((clojure-mode . ((cider-preferred-build-tool . clojure-cli)\n (cider-clojure-cli-aliases . \":dev/reloaded\"))))\n
Or include an alias with only portal data inspector
.dir-locals.el
((clojure-mode . ((cider-preferred-build-tool . clojure-cli)\n (cider-clojure-cli-aliases . \":inspect/portal-cli\"))))\n
Set cider-clojure-cli-aliases
to the alias used to add portal, e.g. inspect/portal
Example
(setq cider-clojure-cli-aliases \":inspect/portal\")\n
Spacemacs: add to dotspacemacs/user-config
in the Spacemacs configuration file. Doom Emacs: add to config.el
Doom configuration file.
"},{"location":"data-inspector/portal/#open-portal","title":"Open Portal","text":"(require '[portal.api :as inspect])
once the REPL starts.
For inspector-portal-web
use (require '[portal.web :as inspect])
instead
(inspect/open)
to open the Portal inspector window in a browser (see portal themes)
(add-tap #'portal/submit)
to add portal as a tap target
"},{"location":"data-inspector/portal/#use-portal-from-repl","title":"Use Portal from REPL","text":"Portal functions can be called from the REPL prompt. When using Portal regularly, include code in a file, e.g. dev/user.clj
namespace to start a portal and add a tap source. Use a rich comment form, (comment ,,,)
to wrap the portal function calls if Portal should be launched manually by the developer.
user namespace and REPL commands
(ns user\n (:require [portal.api :as inspect]))\n\n(comment\n ;; Open a portal inspector window\n (inspect/open)\n ;; Add portal as a tap> target over nREPL connection\n (add-tap portal.api/submit)\n ;; Clear all values in the portal inspector window\n (inspect/clear)\n ;; Close the inspector\n (inspect/close)\n ) ;; End of rich comment block\n
"},{"location":"data-inspector/portal/#open-portal-on-repl-startup","title":"Open Portal on REPL startup","text":"Start the Portal inspector as soon as the REPL is started. This works for a terminal REPL as well as clojure aware editors.
Create a dev/user.clj
source code file which requires the portal.api library, opens the inspector window and adds portal as a tap source.
REPL ReloadedBasic Portal config When using namespace reloading tools (clojure tools.namespace.repl, Integrant, etc.) it is advisable to exclude dev
directory from the path to avoid launching multiple instances of Portal.
Example
(ns user\n (:require\n [portal.api :as inspect]\n [clojure.tools.namespace.repl :as namespace]))\n\n(println \"Set REPL refresh directories to \" (namespace/set-refresh-dirs \"src\" \"resources\"))\n
As a further precaution, check the Portal API sessions
value to ensure Portal is not already running, preventing Portal running multiple times
Example
(def portal-instance\n (or (first (inspect/sessions))\n (inspect/open {:portal.colors/theme :portal.colors/gruvbox})))\n
Example
(ns user\n (:require [portal.api :as inspect]))\n\n;; ---------------------------------------------------------\n;; Open Portal window in browser with dark theme\n(inspect/open {:portal.colors/theme :portal.colors/gruvbox})\n;; Add portal as a tap> target over nREPL connection\n(add-tap #'portal.api/submit)\n;; ---------------------------------------------------------\n(comment\n (inspect/clear) ; Clear all values in the portal inspector window\n (inspect/close) ; Close the inspector\n ) ; End of rich comment block\n
Start a REPL using the :repl/reloaded
or :dev/reloaded
alias from Practicalli Clojure CLI Config to include the dev
directory on the path and the portal library.
"},{"location":"data-inspector/portal/#basic-use","title":"Basic use","text":"The tap>
function sends data to Portal to be shown on the inspector window.
(tap> {:accounts\n [{:name \"jen\" :email \"jen@jen.com\"}\n {:name \"sara\" :email \"sara@sara.com\"}]})\n
Use portal to navigate and inspect the details of the data sent to it via tap>
.
(inspect/clear)
to clear all values from the portal inspector window.
(inspect/close)
to close the inspector window.
"},{"location":"data-inspector/portal/#editor-commands","title":"Editor Commands","text":"Control Portal from a Clojure Editor by wrapping the portal commands.
Emacs Add helper functions to the Emacs configuration and add key bindings to call them.
Emacs Configuration
;; def portal to the dev namespace to allow dereferencing via @dev/portal\n(defun portal.api/open ()\n (interactive)\n (cider-nrepl-sync-request:eval\n \"(do (ns dev)\n (def portal ((requiring-resolve 'portal.api/open)))\n (add-tap (requiring-resolve 'portal.api/submit)))\"))\n\n(defun portal.api/clear ()\n (interactive)\n (cider-nrepl-sync-request:eval \"(portal.api/clear)\"))\n\n(defun portal.api/close ()\n (interactive)\n (cider-nrepl-sync-request:eval \"(portal.api/close)\"))\n
- Spacemacs: add to
dotspacemacs/user-config
in the Spacemacs configuration file. - Doom emacs: add to
config.el
Doom configuration file.
Add key bindings to call the helper functions, ideally from the Clojure major mode menu.
SpacemacsDoom Emacs Add key bindings specifically for Clojure mode, available via the , d p
debug portal menu when a Clojure file (clj, edn, cljc, cljs) is open in the current buffer.
Spacemacs Key bindings for Portal
Add key bindings to Clojure major mode, e.g. , d p c to clear values from Portal Spacemacs Configuration - dotspacemacs/user-config
(spacemacs/declare-prefix-for-mode 'clojure-mode \"dp\" \"Portal\")\n(spacemacs/set-leader-keys-for-major-mode 'clojure-mode \"dpp\" 'portal.api/open)\n(spacemacs/set-leader-keys-for-major-mode 'clojure-mode \"dpc\" 'portal.api/clear)\n(spacemacs/set-leader-keys-for-major-mode 'clojure-mode \"dpD\" 'portal.api/close)\n
Or add user key bindings to user menu, SPC o
avoiding potential clash with Spacemacs Clojure layer key bindings. e.g. Space o p c to clear values from Portal Spacemacs Configuration - dotspacemacs/user-config
(spacemacs/declare-prefix \"op\" \"Clojure Portal\")\n(spacemacs/set-leader-keys \"opp\" 'portal.api/open)\n(spacemacs/set-leader-keys \"opc\" 'portal.api/clear)\n(spacemacs/set-leader-keys \"opD\" 'portal.api/close)\n
Use the map!
macro to add keys to the clojure-mode-map
, using :after
to ensure cider is loaded before binding the keys
Doom Configuration
(map! :map clojure-mode-map\n :n \"s-o\" #'portal.api/open\n :n \"C-l\" #'portal.api/clear)\n
Practicalli Doom Emacs configuration
Practicalli Doom Emacs config includes Portal key bindings in the Clojure major mode menu, under the debug menu. * , d p o
to open portal * , d p c
to clear results from portal
(map! :after cider\n :map clojure-mode-map\n :localleader\n :desc \"REPL session\" \"'\" #'sesman-start\n\n ;; Debug Clojure\n (:prefix (\"d\" . \"debug/inspect\")\n :desc \"debug\" \"d\" #'cider-debug-defun-at-point\n (:prefix (\"i\" . \"inspect\")\n :desc \"last expression\" \"e\" #'cider-inspect-last-sexp\n :desc \"expression\" \"f\" #'cider-inspect-defun-at-point\n :desc \"inspector\" \"i\" #'cider-inspect\n :desc \"last result\" \"l\" #'cider-inspect-last-result\n (:prefix (\"p\" . \"portal\")\n :desc \"Clear\" \"c\" #'portal.api/clear\n :desc \"Open\" \"D\" #'portal.api/close\n :desc \"Open\" \"p\" #'portal.api/open)\n :desc \"value\" \"v\" #'cider-inspect-expr))\n\n ; truncated...\n )\n
Practicalli Doom Emacs Config - +clojure.el
Portal Documentation - Editors
"},{"location":"data-inspector/portal/#editor-nrepl-middleware","title":"Editor nREPL middleware","text":"portal.nrepl/wrap-portal
sends every REPL evaluation to Portal over an nREPL connection, avoiding the need to wrap expressions with tap>
.
Practicalli Clojure CLI ConfigAlias Definition Start a REPL that includes the Portal nREPL middleware to send the result of every evaluation to portal.
:repl/reloaded
- rich terminal UI with portal and REPL Reloaded tools :repl/inspect
- basic terminal UI with portal
:repl/inspect
to start a terminal REPL with nREPL support for Clojure editor connection and portal libraries and middleware that will send all evaluations to portal once added as a tap source. !!!! EXAMPLE \"User deps.edn\"
:repl/inspect\n{:extra-deps\n {nrepl/nrepl {:mvn/version \"1.0.0\"}\n cider/cider-nrepl {:mvn/version \"0.30.0\"}\n djblue/portal {:mvn/version \"0.40.0\"}\n clj-commons/clj-yaml {:mvn/version \"0.7.0\"}}\n :main-opts [\"-m\" \"nrepl.cmdline\"\n \"--middleware\"\n \"[cider.nrepl/cider-middleware,portal.nrepl/wrap-portal]\"]}\n
Start a REPL with :repl/reloaded
or 'repl/inspect'
clojure -M:repl/reloaded\n
Start Portal User Interface and add portal as a tap target using the portal.api/submit
function to send all evaluated code to Portal
Clear results to keep history manageable
Use the Portal API clear
function to remove all existing results in Portal
"},{"location":"data-inspector/portal/#tap-logs-to-portal","title":"Tap Logs to Portal","text":"Using a custom mulog publisher, all event logs can be automatically sent to portal.
mulog tap publisher
;; ---------------------------------------------------------\n;; Mulog Custom Publishers\n;; - tap publisher for use with Portal and other tap sources\n;; ---------------------------------------------------------\n(ns mulog-publisher\n (:require\n ;; [com.brunobonacci.mulog :as mulog]\n [com.brunobonacci.mulog.buffer :as mulog-buffer]\n [portal.api :as p]))\n\n(deftype TapPublisher [buffer transform]\n com.brunobonacci.mulog.publisher.PPublisher\n (agent-buffer [_] buffer)\n (publish-delay [_] 200)\n (publish [_ buffer]\n (doseq [item (transform (map second (mulog-buffer/items buffer)))]\n (tap> item))\n (mulog-buffer/clear buffer)))\n\n(defn tap\n [{:keys [transform] :as _config}]\n (TapPublisher. (mulog-buffer/agent-buffer 10000) (or transform identity)))\n
Require the mulog-publisher
namespace and mulog library in the user
ns expression
Require mulog-publisher
namespace
(ns user\n \"Tools for REPL Driven Development\"\n (:require\n [com.brunobonacci.mulog :as mulog]\n [mulog-publisher]))\n
Start the publisher, optionally setting a global context for events first
Set values for all mulog events and start custom mulog publisher
;; ---------------------------------------------------------\n;; Mulog events and publishing\n\n;; set event global context - information added to every event for REPL workflow\n(mulog/set-global-context! {:app-name \"Practicalli Service\",\n :version \"0.1.0\", :env \"dev\"})\n\n(def mulog-tap-publisher\n \"Start mulog custom tap publisher to send all events to Portal\n and other tap sources\n `mulog-tap-publisher` to stop publisher\"\n (mulog/start-publisher!\n {:type :custom, :fqn-function \"mulog-publisher/tap\"}))\n;; ---------------------------------------------------------\n
Mulog events are now sent to portal when evaluated
(mulog/log ::repl-state ::ns (ns-publics *ns*))\n
Stop the mulog publisher by calling the reference it returns, i.e. mulog-tap-publisher
Function to stop mulog tap publisher
(defn mulog-tap-stop\n \"Stop mulog tap publisher to ensure multiple publishers are not started\n Recommended before using `(restart)` or evaluating the `user` namespace\"\n [] (mulog-tap-publisher))\n
"},{"location":"data-inspector/portal/#references","title":"References","text":" Portal Documentation - clj-docs
"},{"location":"data-structures/","title":"Data structures","text":"Clojure is a very data-centric language. clojure.core
contains a great number of functions for manipulating data structures, especially the immutable built in data structures, referred to generically as collections.
Collections can take any types of elements and types can be mixed. Collections can even have other collections as an element.
Collections are passed as arguments to function (either in part or in full) and functions often return collections as a result.
"},{"location":"data-structures/#built-in-collections","title":"Built-in collections","text":"Values can be represented as a collection of discrete pieces of data: number, string, boolean value.
Clojure has great facilities for working with collections of data, providing many types of data structures and a uniform way to use all of these data structures.
The 4 commonly used built-in data structures
Name syntax Description list ()
A linked list, optomised for sequential access from the front (head) vector []
An indexed array optimised for random access hash-map {:key \"value\"}
Associative collection of key / value pairs, keys must be unique. Keys are the index set #{}
A unique set of values Vector and hash-map are the most commonly collections used to model information with Clojure.
Lists are not explicitly used to model data, although data may be returned by a function as a list (referred to as a sequence)
"},{"location":"data-structures/#collection-characteristics","title":"Collection Characteristics","text":"Clojure data structure share the following characteristics:
- Immutable - once a data structure is defined it cannot be changed
- Persistent - functions may return an altered copy of a data structure which will share common values with the original data structure for efficient memory use (structural sharing)
- Dynamically typed - a data structure can contain any value, including functions (as they evaluate to a value) and other data structures (nested data structures)
This section will cover the Clojure built in persistent data structures in more detail.
"},{"location":"data-structures/#common-data-structures","title":"Common Data Structures","text":"Simple data
(def name value)
Sequential data
(list ...) sequence - always processed sequentially
(vector) sequencw with randon access
Dictionary
(key value key1 value key2 value)
Connverting data, data decoder/encoder, state machine, etc
Data set
(def name\n [{:identical-keys \"with evolving values\"}\n {:identical-keys \"values differ from each other\"}\n {:identical-keys \"values collectively provide meaning\"}])\n
Weather monitoring data, bank transactions, stock exchange rates, etc
"},{"location":"data-structures/#hierarchical-data","title":"Hierarchical data","text":"(def name\n {:simple-key value\n :composite-key {:nested-key value}\n :deeper-composite-key {:nested-key {:deeper-nested-key value}}})\n
representing state, structure of a website Starwars example,
walk the hierarchy to get the appropriate values
extract only the values required by a function and pass as arguments
hierachiecy can become too complex to manage, the flatest possible structure is usually simpler to work with (transform)
"},{"location":"data-structures/alternatives/","title":"Alternative data structures","text":"Whist list, vector, hash-map and set are by far the most commonly used data structures, Clojure has many others of interest.
"},{"location":"data-structures/alternatives/#variations-on-hash-maps","title":"Variations on hash-maps","text":""},{"location":"data-structures/alternatives/#variations-on-sets","title":"Variations on sets","text":"ordered-set
"},{"location":"data-structures/list/","title":"List","text":"The list is used extensively in Clojure, it is a List (List Processing) language after all. The unique thing about lists is that the first element is always evaluated as a function call, therefore lists are most commonly used for defining and calling functions.
Lists are sometimes used as a data structure and have a sequential lookup time. A list can hold any valid types of data, from numbers and strings to other data structures such as vectors, maps and sets. Types can be mix as Clojure will dynamically type each element as its evaluated.
Its more common to use vectors and maps which typically offer quicker access as they can be looked up via an index or key.
Note Explore the list data structure and discover which line of code fails. Try work out why that line of code fails.
(list 1 2 3 4)\n(list -1 -0.234 0 1.3 8/5 3.1415926)\n(list \"cat\" \"dog\" \"rabbit\" \"fish\")\n(list :cat 1 \"fish\" 22/7 (str \"fish\" \"n\" \"chips\"))\n(list 1 2 \"three\" [4] five '(6 7 8 9))\n(list )\n\n( 1 2 3 4)\n\n(quote (1 2 3 4))\n'(1 2 3 4)\n\n;; Duplicate elements in a list ?\n(list 1 2 3 4 1)\n(list \"one\" \"two\" \"one\")\n(list :fred :barney :fred)\n
We can create a list using the list
function
(list 1 2 3 4)\n
This evaluates to (1 2 3 4)
We can give this result a name
(def my-list (list 1 2 3 4))\n
Then when we evaluate my-list
it will return the list as a result
However, if we create a list directly by using (1 2 3 4)
, this will fail when evaluated as 1
is not a function. So when we define a data structure as a list we need to use the quote
function or ' syntax
(quote (1 2 3 4))\n'(1 2 3 4)\n
"},{"location":"data-structures/list/#hintfirst-element-of-a-list-is-a-function-call","title":"Hint::First element of a list is a function call","text":"The first element of a list is evaluated as a function call, unless the list is wrapped in a quote function
"},{"location":"data-structures/list/#testing-for-a-list","title":"Testing for a List","text":"When is a list not a list?
. Lists are sometimes created as other types if they are created in ways other than using the list
function. If you want to know if something is list like, then you can use the seq?
function. If you test with the list?
function and that returns false, you can use the type
function to see what its real type is.
See more about the types that list-like structures actually are in the article: What is a list? The ultimate predicate showdown
"},{"location":"data-structures/naming/","title":"Naming data structures","text":"We have seen that defining things is as simple as giving a name to a value using the def
function. It is the same for the Clojure data structures and any other values.
(def people [\"Jane Doe\" \"Samuel Peeps\"])\n
Names are of course case sensitive, so Person is not the same as person
(def Person \"James Doh\" \"Sam Stare\")\n
Clojure uses dynamic typing, this means its trivial to mix and match different kinds of data. Here we are defining a name for a vector, which contains numbers, a string and name of another def.
(def my-data [1 2 3 \"frog\" person])\n\nmy-data\n
"},{"location":"data-structures/naming/#data-structures-are-immutable-names-are-mutable","title":"Data structures are immutable, names are mutable","text":"You can dynamically re-define a name to points to a different value.
Hint This re-definition (or rebinding) of names to new values is typically used only during the development of your code, especially in REPL driven development.
(def my-data [1 2 3 4 5 \"frog\" person])\n
The original value that defined my-data remains unchanged (its immutable), so anything using that value remains unaffected. Essentially we are re-mapping my-data to a new value.
Lets define a name to point to a list of numbers
(def my-list '(1 2 3))\n
We are returned that list of numbers when we evaluate the name
my-list\n
We can use the cons function to add a number to our list, however because lists are immutable, rather than changing the original list, a new one is returned. So if we want to keep on referring to our \"changed\" list, we need to give it a name
(def my-list-updated (cons 4 my-list))\n
As you can see we have not changed the original list
my-list\n
;; The new list does have the change though.
my-list-updated\n
You could therefore give the impression of mutable state by applying a function to data structure and redefining the original name to point to the resulting data structure.
Hint In practice, the ability to redefine functions and data structures live helps you develop your application quickly in the REPL.
In production you typical do not redefine functions or data structures in a live running application. That could be part of a new release of your application though.
(def my-list (cons 5 my-list))\n
So now when we evaluate the original name, we get the updated list
my-list\n
"},{"location":"data-structures/naming/#naming-scope","title":"Naming Scope","text":"All def names are publicly available via their namespace. As def values are immutable, then keeping things private is of less concern than languages built around Object Oriented design.
Private definitions syntax can be used to limit the access to def names to the namespace they are declared in.
To limit the scope of a def, add the :private true metadata key value pair.
(def ^{:private true} some-var :value)\n\n(def ^:private some-var :value)\n
The second form is syntax sugar for the first one.
You could also define a macro for def-
(defmacro def- [item value]\n `(def ^{:private true} ~item ~value)\n)\n
You would then use this macro as follows:
(def- private-definition \"This is only accessible in the namespace\")\n
"},{"location":"data-structures/pretty-printing/","title":"Pretty Printing data structures","text":"Data structures containing small amounts of data are quite human readable, although can benefit from pretty printing to make them very easy for humans to read.
The larger a data structure becomes, or if a data structure is nested, then there are tools to print out ascii views of the data structures.
"},{"location":"data-structures/pretty-printing/#pretty-print-hash-maps","title":"Pretty print hash-maps","text":"(clojure.pprint/pprint\n {:account-id 232443344 :account-name \"Jenny Jetpack\" :balance 9999 :last-update \"2021-12-12\" :credit-score :aa} )\n
Each key is printed on a new line, making the hash-map easier to read, especially when there are a large number of keys
{:account-id 232443344,\n :account-name \"Jenny Jetpack\",\n :balance 9999,\n :last-update \"2021-12-12\",\n :credit-score :aa}\n
Clojure aware editors can also have an align option when formatting hash-maps, making the results easier to read
{:account-id 232443344,\n :account-name \"Jenny Jetpack\",\n :balance 9999,\n :last-update \"2021-12-12\",\n :credit-score :aa}\n
"},{"location":"data-structures/pretty-printing/#hintpretty-print-evaluation-results","title":"Hint::Pretty Print evaluation results","text":"Clojure aware editors should allow the pretty printing of the evaluation results.
"},{"location":"data-structures/pretty-printing/#print-table-of-nested-data-structures","title":"Print Table of nested data structures","text":"Nested data structures can also be shown as a table, especially the common approach of using a vector of hash-maps where each map has the same keys
(clojure.pprint/print-table\n [{:location \"Scotland\" :total-cases 42826 :total-mortality 9202}\n {:location \"Wales\" :total-cases 50876 :total-mortality 1202}\n {:location \"England\" :total-cases 5440876 :total-mortality 200202}])\n
| :location | :total-cases | :total-mortality |\n|-----------+--------------+------------------|\n| Scotland | 42826 | 9202 |\n| Wales | 50876 | 1202 |\n| England | 5440876 | 200202 |\n
"},{"location":"data-structures/pretty-printing/#references","title":"References","text":"Data browsers (Cider Inspector, Portal, Reveal Free) are very useful for larger and nested data structures.
"},{"location":"data-structures/set/","title":"Set","text":"A Clojure set is a persistent data structure that holds a unique set of elements. Again the elements can be of any type, however each element must be unique for a valid set.
Note Explore creating sets from existing collections. Notice what happens if you have duplicate values in the collection. Define sets directly using the #{}
notation and see what happens if there are duplicate values.
(set `(1 2 3 4))\n(set `(1 2 1 2 3 4))\n\n#{1 2 3 4}\n#{:a :b :c :d}\n;; duplicate key error\n#{1 2 3 4 1}\n
"},{"location":"data-structures/set/#unique-but-not-ordered","title":"Unique but not ordered","text":"A set is not ordered by the values it contains. If you need a sorted set then you can use the sorted-set
function when creating a new set. Or you can run
(sorted-set 1 4 0 2 9 3 5 3 0 2 7 6 5 5 3 8)\n\n(sort [9 8 7 6 5])\n(sort-by )\n
"},{"location":"data-structures/set/#looking-up-values-in-a-set","title":"Looking up values in a set","text":"(#{:a :b :c} :c)\n(#{:a :b :c} :z)\n
Sets can also use the contains?
function to see if a value exists in a set
(contains?\n #{\"Palpatine\" \"Darth Vader\" \"Boba Fett\" \"Darth Tyranus\"}\n \"Darth Vader\")\n
"},{"location":"data-structures/shared-memory/","title":"Shared memory with Persistent data structures","text":"The Clojure data structures are immutable, so they initially seem similar to constants rather than variables. Once a collection is created, it cannot be changed. Any functions that run on a collection do not change the collection, instead they return a new collection with the respective changes.
Creating a new collection each time may seem inefficient, however, the persistent collections use a sharing model. When a new collection is created, it links to all the relevant elements of the original collection and adds any new elements.
Hint Read the InfoQ article on An In-Depth Look at Clojure Collections.
"},{"location":"data-structures/vector/","title":"Vector","text":"Vectors are an indexed sequential collections of data, basically the same as arrays in other languages. However, there are several differences. The index for a vector starts at 0, just like arrays in other languages.
Vectors are written using square brackets []
with any number of pieces of data inside them, separated by spaces.
Note Experiment with creating vectors for your data structures
(vector 1 2 3 4)\n[1 2 3 4 5]\n[56.9 60.2 61.8 63.1 54.3 66.4 66.5 68.1 70.2 69.2 63.1 57.1]\n[]\n\n(def pi 3.1435893)\n[1 2.4 pi 11/4 5.0 6 7]\n[:cat :dog :rabbit :fish]\n[{:cat 1} \"fish\" \"potatoes\" \"oil\" (str \"who ate my\" \"fish n chips\")]\n\n;; Include other data structures in vectors, in this example a list is an element of the vector\n[1 2 3 '(4 5 6)]\n\n;; Are duplicate elements allowed ?\n[1 2 3 4 1]\n
Note What can you do with vectors? Vectors are easy to add more items to, delete items from, or pull arbitrary items out of. Here are some functions that operate on vectors.
(vector? [5 10 15])\n(= [] [])\n(= [] [1])\n\n(first [5 10 15])\n(rest [5 10 15])\n(nth [5 10 15] 1)\n(count [5 10 15])\n\n(conj [5 10] 15)\n
Hint When a function is effectively asking if a value is true or false, its referred to as a predicate function. Its common practice in Clojure to place a ?
at the end of that functions name.
"},{"location":"data-structures/vector/#lookup-data-from-a-vector","title":"Lookup data from a Vector","text":"([1 2 3] 1)\n\n;; ([1 2 3] 1 2) ;; wrong number of arguments, vectors behaving as a function expect one parameter\n\n;; ((1 2 3) 1) ;; you cant treat lists in the same way, there is another approach - assoc\n
"},{"location":"data-structures/vector/#changing-vectors","title":"Changing vectors","text":"The next two functions are used to make new vectors. The vector
function takes any number of items and puts them in a new vector.
conj
takes a vector and an item and returns a new vector with that item added to the end. The function name is taken from the verb \"conjugate\", meaning \"to join together.
Remember that collections in Clojure are immutable, so when we say that a function \"adds to\" or \"removes from\" a collection, what we mean is that the function returns a new collection with an item added or removed.
Note Using one or more vectors, create a data structure of the high temperatures for the next 7 days in your area. Use the nth
function to get the high temperature for next Friday
"},{"location":"data-structures/hash-maps/","title":"Data Structures: Hash-maps","text":"Associative collection of key value pairs
Useful for defining self-describing structured data (assuming meaningful key names are used)
A map is a key / value pair data structure. Keys are usually defined using a keyword, although they can be strings or anything else.
Keywords point to themselves, so using them for the keys makes it very easy to get values out of the map, or for updating existing values in the map.
Note Explore creating maps
{:key \"value\"}\n{:key :value}\n{\"key\" \"value\"}\n(\"key\" :value)\n(:meaining-of-life 42)\n{:a 1 :b 2 :c 3}\n{:monday 1 :tuesday 2 :wednesday 3 :thursday 4 :friday 5 :saturday 6 :sunday 7}\n{1 \"Monday\" 2 \"Tuesday\" 3 \"Wednesday\" 4 \"Thursday\" 5 \"Friday\" 6 \"Saturday\" 7 \"Sunday\"}\n
"},{"location":"data-structures/hash-maps/#hintcomma-characters-are-treated-as-white-space","title":"Hint::Comma characters are treated as white-space","text":"The comma character is rarely used in Clojure hash-maps as it is ignored by Clojure. When coming from other languages, it may be initially comforting to include commas.
"},{"location":"data-structures/hash-maps/#nested-data-models","title":"Nested data models","text":"nested maps to create a hierarchy or path for data. This can add more context to the overall design
various types of data
"},{"location":"data-structures/hash-maps/#hintone-data-structure-to-rule-them-all","title":"Hint::One data structure to rule them all","text":"It is preferred to have a single data structure to model the data of a system, which is them used by all the functions of that system. An example is in the state used for an application, e.g. Practicalli website practicalli.data namespace
If there is no logical connection between data across a system, then data should be grouped into one structure per namespace as a minimal approach.
"},{"location":"data-structures/hash-maps/#example-use-data-sets","title":"Example use: Data sets","text":"A collection of maps which have the same form, e.g. a vector of hash-maps with the same keys
Example: meteorological recordings
(def recording-station-876WA\n [{:timestamp \"2021-12-01T12:00\" :location {:latitude 24.3453434 :longitude 10.348888} :temperature 12.4 :rainfail 0.1 :uv-level 0.4}\n {:timestamp \"2021-12-01T12:10\" :location {:latitude 24.3453434 :longitude 10.348888} :temperature 12.6 :rainfail 0.1 :uv-level 0.45}\n {:timestamp \"2021-12-01T12:00\" :location {:latitude 24.3453434 :longitude 10.348888} :temperature 12.9 :rainfail 0.1 :uv-level 0.5}])\n
Providing a collection of consistent hash-map data structures is very easy to work with in Clojure.
reduce
, filter
and map
functions can easily process this form of data as part of algorithms to interpret the meaning from a data set.
As each recording station creates the same types of data, then they can be merged by including the recording station id in the map
"},{"location":"data-structures/hash-maps/access/","title":"Accessing hash-maps","text":"The values in a hash-map can be accessed in multiple ways
get
get-in
contains?
- using hash-map as a function
- use :keyword as a function
- threading hash-map through one or more keys
Clojure provides a get function that returns the value mapped to a key in a set or map.
"},{"location":"data-structures/hash-maps/access/#get-and-get-in-functions","title":"get and get-in functions","text":"get
is a very explicitly named function that makes its purpose very clear. The get
function works regardless of the type of keys used in the hash-map.
(get map key)
get-in
has the same quality, for use with nested hash-maps.
(get-in nested-map [:keys :path])\n
(get-in {\"timestamp\" 1291578985220 \"scores\" {\"FSU\" 31 \"UF\" 7}} [\"scores\" \"FSU\"])\n;;=> 31\n\n(get-in {\"timestamp\" 1291578985220 \"scores\" {\"FSU\" 31 \"UF\" 7}} [\"scores\"])\n;;=> {\"FSU\" 31, \"UF\" 7}\n\n(get-in {\"timestamp\" 1291578985220 \"scores\" {\"FSU\" 31 \"UF\" 7}} [])\n;;=> {\"timestamp\" 1291578985220, \"scores\" {\"FSU\" 31, \"UF\" 7}}\n\n(get-in {\"timestamp\" 1291578985220 \"scores\" {\"FSU\" 31 \"UF\" 7}} nil)\n;;=> {\"timestamp\" 1291578985220, \"scores\" {\"FSU\" 31, \"UF\" 7}}\n
"},{"location":"data-structures/hash-maps/access/#hintmissing-or-incorrect-key","title":"Hint::missing or incorrect key","text":"If the key in the path is missing or the path is missing (or nil) then get-in
will return more of the hash-map than expected.
"},{"location":"data-structures/hash-maps/access/#using-hash-map-as-a-function","title":"Using hash-map as a function","text":"A hash-map (and list, vector, set) can be called as a function with a key as the argument. This provides a more terse expression than using get
and also works irrespective of key types used.
Passing the key :star-wars
to the hash-map returns the value associated with that key
({:star-wars {:characters {:jedi [\"Luke\" \"Obiwan\"]}}} :star-wars)\n
A nested hash-map (containing other hash-maps) can be accessed via multiple nested calls to the returned values.
((({:star-wars {:characters {:jedi [\"Luke\" \"Obiwan\"]}}} :star-wars) :characters) :jedi)\n
"},{"location":"data-structures/hash-maps/access/#keyword-key-as-a-function","title":"keyword key as a function","text":"A keyword can be called as a function, taking a hash-map as an argument
(:star-wars {:star-wars {:characters {:jedi [\"Luke\" \"Obiwan\"]}}})\n
A nested hash-map (containing other hash-maps) can be accessed via multiple nested calls to the returned values.
(:jedi (:characters (:star-wars {:star-wars {:characters {:jedi [\"Luke\" \"Obiwan\"]}}})))\n
"},{"location":"data-structures/hash-maps/access/#threading-macro","title":"Threading macro","text":"Using keyword keys as functions, the thread macros provide a consistent approach to accessing hash-map data
The hash-map is passed through one or more keyword keys, so obtaining values from a flat or nested hash-map is just the same.
(-> hash-map\n :keyword1\n ,,,)\n
If the keys are a type other than keywords, then a get function would be required for accessing the hash-map.
(-> hash-maps (get \"scores\") (get \"FSU\"))\n
As part of a processing pipeline, taking specific values from a JSON file of association football match statistics
(-> match-statistics.json\n (clojure.data.json/read-str :key-fn keyword)\n :totals\n :goals-home-team)\n
"},{"location":"data-structures/hash-maps/access/#checking-a-key-or-value-exists-in-a-hash-map","title":"Checking a key or value exists in a hash-map","text":"keys
function will return a collection of the keys contained in a map. vals
returns a collection of the values
is in a map or set. In general I use the value returned from a map or set to determine if a key exists - the following snippet uses that pattern.
Check if a key has a specific value
(if (star-wars-map :space-ships)\n (do-true-behaviours)\n (do-false-behaviours))\n
Check a key has a specific value and also use that value
TODO: is this a good case for if-lets
This pattern fails if the value of :key is nil.
"},{"location":"data-structures/hash-maps/access/#contains-and-some","title":"contains? and some","text":"contains?
checks for the index of a collection. The index of a hash-map is the keys it contains
some
will check for a value in a collection
(def recipe-map {:ingredients \"tofu\"})\n\n(contains? recipe-map :ingredients)\n;; => true\n\n(some #{\"tofu\"} recipe-map)\n;; => nil\n\n(vals recipe-map)\n;; => (\"tofu\")\n\n(some #{\"tofu\"} (vals recipe-map))\n;; => \"tofu\"\n
The key is contained as part of the hash-map index, irrespective of the value associated with that key (so long as there is a legal value associate with the key).
(contains? {:totals nil} :totals)\n
"},{"location":"data-structures/hash-maps/accessing-nested-hash-maps/","title":"Accessing Nested Hash-maps","text":"Its also quite common to have maps made up of other maps, maps of vectors or vectors of maps.
Now we can refer to the characters using keywords. Using the get function we return all the information about Luke
(get star-wars-characters :luke)\n(get (get star-wars-characters :luke) :fullname)\n
By wrapping the get function around our first, we can get a specific piece of information about Luke. There is also the get-in function that makes the syntax a little easier to read
(get-in star-wars-characters [:luke :fullname])\n(get-in star-wars-characters [:vader :fullname])\n
Or if you want the data driven approach, just talk to the map directly
(star-wars-characters :luke)\n(:fullname (:luke star-wars-characters))\n(:skill (:luke star-wars-characters))\n\n(star-wars-characters :vader)\n(:skill (:vader star-wars-characters))\n(:fullname (:vader star-wars-characters))\n
And finally we can also use the threading macro to minimise our code further
(-> star-wars-characters\n :luke)\n\n(-> star-wars-characters\n :luke\n :fullname)\n\n(-> star-wars-characters\n :luke\n :skill)\n
This technique is called destructuring. Find out more on Destructuring
Duplicate keys in a map are not allowed, so the following maps...
{\"fish\" \"battered\" \"chips\" \"fried\" \"fish\" \"battered and fried\"}\n{:fish \"battered\" :chips \"fried\" :fish \"battered & fried\"}\n\n;; ...throw duplicate key errors\n\n;; Duplicate values are okay though\n{:fish \"fried\" :chips \"fried\" :peas \"mushy\"}\n
"},{"location":"data-structures/hash-maps/create/","title":"Creating Hash-maps","text":"Hash-maps can be defined literally using {}
and including zero or more key / value pairs. Keys and values can be any legal Clojure type.
Keywords are very commonly used for keys as they provide a convenient way to look up values.
"},{"location":"data-structures/hash-maps/create/#literal-hash-map-examples","title":"Literal hash-map examples","text":"A hash-map defining Obi-wan, a character from the Star Wars universe.
{:name \"Obi-wan Kenobi\" :homeworld \"Stewjon\"}\n
A hash-map defining Leia, another character from the Star Wars with additional information
{:name \"Leia Skywalker\" :homeworld \"Alderaan\" :birthplace \"Polis Massa\"}\n
Use def
to bind a name to a hash-map, making it easier to pass the map to a function as an argument.
(def luke {:name \"Luke Skywalker\" :homeworld \"Tatooine\" :birthplace \"Polis Massa\"})\n
"},{"location":"data-structures/hash-maps/create/#data-set-of-maps","title":"Data set of maps","text":"Create a data set by defining a vector of hash-maps
[{:name \"Obi-wan Kenobi\" :homeworld \"Stewjon\" :occupation \"Jedi\"}\n {:name \"Jyn Erso\" :homeworld \"Vallt\" :occupation \"Soldier\"}\n {:name \"Leia Skywalker\" :homeworld \"Alderaan\" :occupation \"Senator\"}\n {:name \"Luke Skywalker\" :homeworld \"Tatooine\" :occupation \"Jedi\"}\n {:name \"Qui-Gon Jinn\" :homeworld \"Coruscant\" :occupation \"Jedi\"}\n {:name \"Padm\u00e9 Amidala\" :homeworld \"Naboo\" :occupation \"Senator\"}\n {:name \"Sheev Palpatine\" :homeworld \"Naboo\" :occupation \"Supreme Chancellor\"}]\n
"},{"location":"data-structures/hash-maps/create/#example-nested-hash-maps","title":"Example: nested hash-maps","text":"Create a map to represent the world of Star Wars, including various characters & ships, indicating the factions that characters and ships belong to.
Individual Star Wars characters can be defined using a map of maps
{:luke {:name \"Luke Skywalker\" :skill \"Targeting Swamp Rats\"}\n :vader {:name \"Darth Vader\" :skill \"Breaking the rules and peoples hearts\"}\n :jarjar {:name \"JarJar Binks\" :skill \"Failing upwards\"}}\n
Hash-maps can also use other collections as values
{:characters\n {:jedi [\"Luke Skywalker\" \"Obiwan Kenobi\"]\n :sith [\"Darth Vader\" \"Darth Sideous\"]\n :droids [\"C3P0\" \"R2D2\" \"BB8\"]}\n :ships\n {:rebel-alliance [\"Millennium Falcon\" \"X-wing fighter\"]\n :imperial-empire [\"Intergalactic Cruiser\" \"Destroyer\"\n \"Im just making these up now\"]}}\n
Use the def function to bind a name to the Star Wars character information, making it easier to pass to several functions
(def star-wars-characters\n {:luke {:fullname \"Luke Skywalker\" :skill \"Targeting Swamp Rats\"}\n :vader {:fullname \"Darth Vader\" :skill \"Breaking the rules and peoples hearts\"}\n :jarjar {:fullname \"JarJar Binks\" :skill \"Failing upwards\"}})\n
"},{"location":"data-structures/hash-maps/create/#generating-hash-maps","title":"Generating hash-maps","text":"hash-map
is a clojure.core function that returns a hash-map of the given arguments, or an empty hash-map, {}
, if no arguments are given.
Arguments should be key-value pairs, otherwise the function will return nil
"},{"location":"data-structures/hash-maps/create/#converting-collections-to-hash-maps","title":"Converting collections to hash-maps","text":"(apply hash-map [:a 1 :b 2])\n;;=> {:b 2 :a 1}\n
Order of keys in a hash-map is not guaranteed. However, order of keys should be irrelevant as the keys are unique within a map.
(into {} ,,,)\n
map
reduce
merge returns a hash-map that is a merging of the key value pairs from all maps, for any duplicate keys the value from the last key (left to right) is used
"},{"location":"data-structures/hash-maps/create/#setting-default-values","title":"Setting default values","text":"Calling a function with a hash-map as an argument is a flexible way to design the API of a namespace and Clojure application in general.
As functions are talking a map, a function call with fewer or more keys than needed will still result in a successful call (alhtough results could be interesting)
If fewer keys are passed then defaults can be set.
merge
can be used to ensure any required keys with default values are always present, and still over-ridden by keys passed in as an argument
merge
should passed the default key values first, with the argument map merged on top. This ensures all keys are present and that the argument values are used if duplicate keys exist between the maps.
(merge {:option1 \"default-value\" :option2 \"default-value\"}\n {:option1 \"custom-value\"})\n;;=> {:option1 \"custom-value\" :option2 \"default-value\"}\n
The merge
function can be used in a function to return the merged map of default and argument values When a function has a number of options with default values.
(defn parse-cli-tool-options\n \"Return the merged default options with any provided as a hash-map argument\"\n[arguments]\n (merge {:replace false :report true :paths [\".\"]}\n arguments))\n\n(parse-cli-tool-options {:paths [\"src\" \"test\"] :report false})\n;; => {:replace false, :report false, :paths [\"src\" \"test\"]}\n
If an empty hash-map is sent as an argument, the default values are returned
(parse-cli-tool-options {})\n;; => {:replace false, :report true, :paths [\".\"]}\n
zipmap
(zipmap [:a :b :c] [1 2 3])\n;; => {:a 1, :b 2, :c 3}\n
"},{"location":"data-structures/hash-maps/create/#custom-merging-with-a-function","title":"Custom merging with a function","text":""},{"location":"data-structures/hash-maps/create/#create-a-sub-set-of-existing-map","title":"Create a sub-set of existing map","text":""},{"location":"data-structures/hash-maps/create/#filter","title":"filter","text":"Create a sub-set of an existing map
;; #61 - Map Construction ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Difficulty: Easy ;; Topics: core-functions ;; Special Restrictions: zipmap
;; Write a function which takes a vector of keys and a vector of values and constructs a map from them.
;; Tests (= ([:a :b :c] [1 2 3]) {:a 1, :b 2, :c 3}) (= ( [1 2 3 4] [\"one\" \"two\" \"three\"]) {1 \"one\", 2 \"two\", 3 \"three\"}) (= (__ [:foo :bar] [\"foo\" \"bar\" \"baz\"]) {:foo \"foo\", :bar \"bar\"})
;; If we could use zipmap then the answer would be simple
(zipmap [:a :b :c] [1 2 3]) ;; => {:a 1, :b 2, :c 3}
(= (zipmap [:a :b :c] [1 2 3]) {:a 1, :b 2, :c 3}) ;; => true
;; So now we have to figure out the algorithm that zipmap uses
;; Analyse the problem ;; We want to create a paring of values from the first and second vectors ;; Then each pair should be made into a key value pair within a map data structure.
;; The map function will work over multiple collections, returning a single collection
;; A simple example of map function in action: (map str [:a :b :c] [1 2 3]) ;; => (\":a1\" \":b2\" \":c3\")
;; In stead of string, we could use hash-map
(map hash-map [:a :b :c] [1 2 3]) ;; => ({:a 1} {:b 2} {:c 3})
;; now we just need to put all the maps into one map, so perhaps merge will work
(merge (map hash-map [:a :b :c] [1 2 3])) ;; => ({:a 1} {:b 2} {:c 3})
(conj (map hash-map [:a :b :c] [1 2 3])) ;; => ({:a 1} {:b 2} {:c 3})
(reduce conj (map hash-map [:a :b :c] [1 2 3])) ;; => {:c 3, :b 2, :a 1}
;; (reduce conj (map vectork ks vs))
((fn [key-sequence value-sequence] (into {} (map vector key-sequence value-sequence))) [:a :b :c] [1 2 3]) ;; => {:a 1, :b 2, :c 3}
"},{"location":"data-structures/hash-maps/update/","title":"Update Hash-maps","text":""},{"location":"defining-behaviour-with-functions/","title":"Defining behaviours with functions","text":"Clojure has functions, rather than methods for defining behaviour / \"algorithms\"
Clojure design at its most basic comprises:
- one or more data structures
- functions that process those data-structures
There is a common saying in Clojure: \"Its better to have one data structure and many functions, than many data structures and many functions\"
"},{"location":"defining-behaviour-with-functions/anonymous-functions/","title":"Anonymous Functions","text":"clojure.core/fn
is a function for defining custom functions.
fn
is called the anonymous function as it has no external name by which it can be referred by. They are used within the scope of another function call, as having no name they cannot be called from another part of the code.
(map (fn [args] ,,,) [1 2 3])\n((fn [args] ,,,))\n
The value of using anonymous functions comes when there is a short, specific piece of behaviour required which is unlikely to be needed elsewhere in the code. An anonymous function can always be refactored into a defn
expression if used in multiple places.
"},{"location":"defining-behaviour-with-functions/anonymous-functions/#definition-of-an-anonymous-function","title":"Definition of an anonymous function","text":"(fn [argument] (str \"some behaviour, typically using the arguments passed:\" argument ))\n
This expression is a function call to fn
which has the arguments called argument
"},{"location":"defining-behaviour-with-functions/anonymous-functions/#calling-an-anonymous-function","title":"Calling an anonymous function","text":"To get a value from evaluating this function you need to pass it a value (or another function) as an argument, as well as calling it as a function by placing the anonymous function as the first element of a list.
((fn [arguments] (str \"behaviour, typically using the arguments passed: \" arguments )) \"Is this the right room for an argument\")\n
"},{"location":"defining-behaviour-with-functions/anonymous-functions/#binding-a-local-names","title":"Binding a local names","text":"fn
can have a local name which can be used to write a recursive function (a fn that calls itself).
Adding a name also helps with debugging code, as the name will be used to identify that function call if it appears in a stack trace of an exception.
A recursive function that counts the elements in a collection
(fn -count [xs]\n (if (empty? xs)\n 0\n (inc (-count (rest xs)))))\n
(fn meaningful-name\n []\n (str \"If I fail, you will know my name\"))\n
"},{"location":"defining-behaviour-with-functions/anonymous-functions/#anonymous-function-syntactic-sugar","title":"Anonymous function Syntactic Sugar","text":"There is a short form of the function definition using the #( ,,, )
syntax.
For example, if we want to increment an argument we could start to define an anonymous function as follows:
#(inc %)\n
The %
represents a placeholder for an argument that is passed into the anonymous function. This argument is anonymous as well as the value is simply swapped into the position of %
.
To evaluate this anonymous function we need to give it an argument to work on. Anything after the anonymous function is taken as its argument. So in the following expression we pass the value 1 as the argument and we should get the result of incrementing 1 as a result
( #(inc %) 1 )\n\n;; => 2\n
The %
placeholder can also refer to a specific argument by adding an index number. The index numbers refer to the position of the arguments supplied to the anonymous function.
Here we will add two different arguments together
( #(+ %1 %2) 20 22)\n
So %1
will represent the first argument and the %2
will represent the second argument passed to this function.
Sometimes position can be important as the following two versions of code demonstrate
( #(/ %1 %2) 24 6)\n\n( #(/ %2 %1) 24 6)\n
These two expressions give different values (and return different types, Integer and Ratio) as the positions of the arguments have been reversed.
"},{"location":"defining-behaviour-with-functions/calling-functions/","title":"Calling Functions","text":"To call a function in Clojure you use the name of the function as the first element of a list.
In this simple example, a function is defined that takes no arguments, then that function is called.
(defn my-function []\n (str \"I only return this string\"))\n\n(my-function)\n
Functions can be defined to take arguments.
"},{"location":"defining-behaviour-with-functions/calling-functions/#arity","title":"Arity","text":"This is the term to describe the number of arguments a function takes. This can be a fixed number or variable number of arguments.
Simple polymorphism can also be used to have one function take different numbers of arguments, as with the multi-arity
function in the examples below.
(defn single-arity [] \n (str \"I do not take any arguments\"))\n\n(defn single-arity [argument] \n (str \"I take 1 argument only\"))\n\n(defn triple-arity [argument1 argument2 argument3] \n (str \"I take 3 arguments only\"))\n\n(defn multi-arity \n ([argument] \n (str \"I match 1 argument only\"))\n ([argument1 argument2]\n (str \"I match when 2 arguments are used\")))\n\n(defn variable-arity [argument & more-arguments]\n (str \"I assign the first argument to argument, \n all other arguments to more-arguments\"))\n
"},{"location":"defining-behaviour-with-functions/examples/","title":"Examples","text":""},{"location":"defining-behaviour-with-functions/parameters/","title":"Parameters","text":""},{"location":"defining-behaviour-with-functions/syntax/","title":"Syntax","text":"Defining functions is done with the fn
function
We have already seen the def
function to assign names to values. We can also use the same function to give a name to our functions.
"},{"location":"designing-data-structures/","title":"Designing Data Structures","text":"Some common design guides for data structures in Clojure
"},{"location":"designing-data-structures/#the-basics-design-approach","title":"The Basics design approach","text":"Most data structures in Clojure seem to be created from either vectors or maps or a combination of both. Sets are used where uniqueness of values is important and lists are often used for their lazy properties.
Vectors are the most flexible data structure in Clojure and support none-sequential access as they are indexed.
Maps are really useful for defining semantic meaning to your data structures, helping you create data structures that express the context of the model they represent. Maps give you unordered, arbitrary index arrangement. Access is iteration of key/value pairs or getting a value for a given key.
Lists give you sequential, one-at-a-time arrangement. They allow for efficient iteration, lazy generation, and stack discipline.
Sets give you unordered, unique constraint arrangement. Access is iteration of elements or checking containment.
"},{"location":"designing-data-structures/modeling-alphabet-codes/","title":"Model alphabet codes","text":"Maps in Clojure are used to model key and value pairs.
- Keys must be unique within a map.
- A key can be a number, string or keyword.
Vectors in Clojure are a general data structure that are good for handing any kind of information.
Note Define a data structure where each letter of the alphabet is represented by a 6 digit binary code
Lets define a name called alphabet
that is bound to a map. Each key in the map is a character of the alphabet and each value is a vector of numbers that represent a binary code.
The map also includes a binary code for a full stop and space character
(def alphabet {\"A\" [0 1 0 0 0 1]\n \"B\" [0 0 1 0 1 0]\n \"C\" [0 1 0 0 1 0]\n \"D\" [1 0 1 0 0 0]\n \"E\" [1 0 1 1 0 0]\n \"F\" [1 1 0 1 0 0]\n \"G\" [1 0 0 1 1 0]\n \"H\" [1 0 1 0 0 1]\n \"I\" [1 1 1 0 0 0]\n \"J\" [0 0 1 1 1 1]\n \"K\" [0 1 0 1 0 1]\n \"L\" [1 1 1 0 0 1]\n \"M\" [1 1 1 0 1 1]\n \"N\" [0 1 1 1 0 1]\n \"O\" [1 1 0 1 1 0]\n \"P\" [1 1 1 1 1 0]\n \"Q\" [1 0 1 1 1 0]\n \"R\" [1 1 1 1 0 0]\n \"S\" [0 1 1 1 1 0]\n \"T\" [1 0 0 1 1 1]\n \"U\" [0 0 1 0 1 1]\n \"V\" [0 1 1 0 0 1]\n \"W\" [1 1 0 1 0 1]\n \"X\" [1 0 1 0 1 0]\n \"Y\" [1 0 0 0 1 1]\n \"Z\" [1 1 0 0 1 1]\n \".\" [1 0 1 1 0 1]\n \" \" [0 0 1 0 0 0]})\n
"},{"location":"designing-data-structures/modeling-name-generation-map/","title":"Design a map for name generation","text":"Imagine you are writing a simple name generator that takes your name and creates an alternative version. For example this could be a generator of your \"Posh\" or \"Hipster\" name.
Note Define a data structure to model sloane names that has three names for every letter of the alphabet. For name suggestions, see the Tattler sloane name generator.
The following seems to be the simplest way to model the sloane names. This follows the representation in the original source material.
(def sloane-first-name\n {\"a\" \"Ally-Pally\"\n \"b\" \"Bongo\"\n \"c\" \"Chipper\"})\n\n(def slone-second-name\n {\"a\" \"Anstruther\"\n \"b\" \"Beaufort\"\n \"c\" \"Cholmondeley\"})\n\n(def slone-third-name\n {\"a\" \"Arbuthnot\"\n \"b\" \"Battenburg\"\n \"c\" \"Coutts\"})\n
The following alternative data structure design is very simple and more concise, however it does loose some of the semantic meaning. The position of the names is not defined in terms of the context of the problem.
(def slone-names\n {:a [\"Ally-Pally\" \"Anstruther\" \"Arbuthnot\"]})\n
This next design removes some of the redundancy in defining each letter of the alphabet several times. Apart from less typing and therefore reading by the development team, it also explicitly defines the semantic meaning of each name within the context of this problem.
(def slone-names\n {:a {:first \"Ally-Pally\" :second \"Anstruther\" :third \"Arbuthnot\"}})\n
For extra points you could try and implement a function that generated your sloane name.
"},{"location":"designing-data-structures/modeling-name-generation-map/#creating-the-algorithm-to-construct-your-sloane-name","title":"Creating the algorithm to construct your sloane name","text":" - The first sloane name is chosen from the first character of the first name
- The second sloane name chosen from the first character of the second name
- The third sloane name is chosen from the second character of the second name
You can get the first element of a string by treating it just like a collection. However this returns a character
(first \"Strings also act as collections\")\n
A string can be converted to a keyword, a character cannot
(keyword \"a\")\n
A character can be converted to a string using the str function
(str (first \"Strings also act as collections\"))\n
The keywords need to be the same case, so convert the first character to lower case (which returns a string, so the explicit str function is no longer required.)
(clojure.string/lower-case (first \"Strings also act as collections\"))\n
Putting it all together.
(keyword (clojure.string/lower-case (first \"Strings also act as collections\")))\n
"},{"location":"designing-data-structures/modeling-name-generation-map/#create-a-function-to-calculate-your-sloane-name","title":"Create a function to calculate your sloane name","text":"Putting all this together in a function to generate your sloan name, given your a string with your first and last name.
(defn sloane-name\n \"Given a first and last name as a string, returns your equivalent Sloane name as a string\"\n [name]\n (let [first-name (keyword (clojure.string/lower-case (first (first (clojure.string/split name #\" \")))))\n middle-name (keyword (clojure.string/lower-case (first (second (clojure.string/split name #\" \")))))\n last-name (keyword (clojure.string/lower-case (second (second (clojure.string/split name #\" \")))))]\n (str (get-in slone-names [first-name :first])\n \" \"\n (get-in slone-names [middle-name :second])\n \" \"\n (get-in slone-names [last-name :third]))))\n
Supply a name that will test if the sloane-name
function works
(sloane-name \"Billy Abstainer\")\n;; => \"Bongo Anstruther Battenburg\"\n
"},{"location":"designing-data-structures/with-maps-of-maps/","title":"With Maps of Maps","text":"Note Define a collection of star-wars characters using a map of maps. Each character should have an name that they are typically referred to, along with their fullname and skill
(def star-wars-characters\n {:luke {:fullname \"Luke Skywalker\" :skill \"Targeting Swamp Rats\"}\n :vader {:fullname \"Darth Vader\" :skill \"Crank phone calls\"}\n :jarjar {:fullname \"JarJar Binks\" :skill \"Upsetting a generation of fans\"}})\n
Now we can refer to the characters using keywords. Using the get function we return all the information about Luke
(get star-wars-characters :luke)\n
By wrapping the get function around our first, we can get a specific piece of information about Luke
(get (get star-wars-characters :luke) :fullname)\n
There is also the get-in function that makes the syntax a little easier to read
(get-in star-wars-characters [:luke :fullname])\n(get-in star-wars-characters [:vader :fullname])\n
Or you can get really concise by just talking to the map directly
(star-wars-characters :luke)\n(:fullname (:luke star-wars-characters))\n(:skill (:luke star-wars-characters))\n\n(star-wars-characters :vader)\n(:skill (:vader star-wars-characters))\n(:fullname (:vader star-wars-characters))\n
And finally we can also use the threading macro to minimise our code further
(-> star-wars-characters\n :luke)\n\n(-> star-wars-characters\n :luke\n :fullname)\n\n(-> star-wars-characters\n :luke\n :skill)\n
Note* Create a slightly data structure holding data around several developer events. Each event should have a website address, event type, number of attendees, call for papers.
(def dev-event-details\n {:devoxxuk {:URL \"http://jaxlondon.co.uk\"\n :event-type \"Conference\"\n :number-of-attendees 700\n :call-for-papers \"open\"}\n :hackthetower {:URL \"http://hackthetower.co.uk\"\n :event-type \"hackday\"\n :number-of-attendees 99\n :call-for-papers \"closed\"}})\n
This data structure is just a map, with each key being the unique name of the developer event.
The details of each event (the value to go with the event name key) is itself a map as there are several pieces of data associated with each event name. So we have a map where each value is itself a map.
Call the data structure and see what it evaluates too, it should not be a surprise
dev-event-details\n
We can ask for the value of a specific key, and just that value is returned
(dev-event-details :devoxxuk)\n
In our example, the value returned from the :devoxxuk key is also a map, so we can ask for a specific part of that map value by again using its key
(:URL (dev-event-details :devoxxuk))\n
"},{"location":"designing-data-structures/with-maps/","title":"With Maps","text":"Maps allow you to model data with its contextual meaning. The keys of a map can give the context and the values are the specific data.
Note Define a shopping list of items you want, including how many of each item you want to buy
(def shopping-list\n {\"cat food\" 10\n \"soya milk\" 4\n \"bread\" 1\n \"cheese\" 2})\n
Note Define a star-wars characters, eg. luke skywalker, jarjar binks. The star-wars character should include a name and a skill (it doesn't matter what these are).
Use the 'get' function to return the value of a given key, eg. name. Use keywords to return a given value if you used keywords for the map keys.
In this answer we have defined three different star-wars characters, all using the same map keys.
(def luke {:name \"Luke Skywalker\" :skill \"Targeting Swamp Rats\"})\n(def darth {:name \"Darth Vader\" :skill \"Crank phone calls\"})\n(def jarjar {:name \"JarJar Binks\" :skill \"Upsetting a generation of fans\"})\n
Lets see what the specific skill luke has
(get luke :skill)\n
When you use a keyword, eg. :name, as the key in a map, then that keyword can be used as a function call on the map to return its associated value. Maps can also act as functions too.
(:name luke)\n(luke :name)\n
There are also specific functions that work on maps that give all the keys
of a map and all the values
of that map
(keys luke)\n(vals luke)\n
"},{"location":"designing-data-structures/with-vectors-of-maps/","title":"A Vector of Maps","text":"Vectors are good for holding any information whether that be simple values or other collections.
Maps are good for defining data with semantic meaning, using the keys to express the context of the values.
*Note Define a simple data structure for a collection of stocks in a portfolio. This would contain a collection of stock information, with each stock holding the ticker name, last trading monetary value and opening monetary value.
This is a vector of maps, as there will be one or more company stocks to track. Each map represents the stock information for a company.
(def portfolio [ { :ticker \"CRM\" :lastTrade 233.12 :open 230.66}\n { :ticker \"AAPL\" :lastTrade 203.25 :open 204.50}\n { :ticker \"MSFT\" :lastTrade 29.12 :open 29.08 }\n { :ticker \"ORCL\" :lastTrade 21.90 :open 21.83 }])\n
We can get the value of the whole data structure by referring to it by name
portfolio\n
As the data structure is a vector (ie. array like) then we can ask for a specific element by its position in the array using the nth
function
Lets get the map that is the first element (again as a vector has array-like properties, the first element is referenced by zero)
(nth portfolio 0)\n
The vector has 4 elements, so we can access the last element by referencing the vector using 3
(nth portfolio 3)\n
As portfolio is a collection, also known as a sequence, then we can use a number of functions that provide common ways of getting data from a data structure
(first portfolio)\n(rest portfolio)\n(last portfolio)\n
We can get specific information about the share in our portfolio, or as the keys in each map are defined with Clojure keywords, we can also use the keywords to return the specific values they pair with.
(get (second portfolio) :ticker)\n;; => \"AAPL\"\n\n(:ticker (first portfolio))\n;; => \"CRM\"\n
If we want to get specific share information across the whole portfolio, then we can simply map
the :ticker
keyword over each share in portfolio
(map :ticker portfolio)\n;; => (\"CRM\" \"AAPL\" \"MSFT\" \"ORCL\")\n\n(mapv :ticker portfolio)\n;; => [\"CRM\" \"AAPL\" \"MSFT\" \"ORCL\"]\n
"},{"location":"designing-data-structures/with-vectors-of-vectors/","title":"With Vectors of Vectors","text":"The most frequent use of you will see is in the project.clj
file, where a vector of vectors is used to model the library dependencies for a project
[[org.clojure/clojure \"1.8.0\"]\n [org.clojure/core.match \"0.3.0-alpha4\"]]\n\n[[org.clojure/clojure \"1.6.0\"]\n [ring \"1.4.0-beta2\"]\n [compojure \"1.3.4\"]\n [hiccup \"1.0.5\"]]\n
Fixme Think of an exercise to create a vector of vectors as a data model
"},{"location":"designing-data-structures/with-vectors/","title":"With Vectors","text":"Vectors as the simplest data structure in Clojure to work with. They are very similar to an array in other languages, although they have additional qualities in Clojure.
Vectors
- can be of any length
- are indexed so have fast random access
- can contain any types
- are immutable
Define a data structure for a simple shopping list with any items you would typically want to buy.
(def shopping-list [\"Cerial\" \"Baked Beans\" \"Cat food\" \"Quorn chicken pieces\" ])\n
"},{"location":"development-environments/","title":"Development Environments","text":"This workshop encourages LightTable & Leiningen as the development environment, as they are the easiest tools to set up.
Leiningen is the build automation tool used to manage Clojure projects. It will create projects from templates and run our Clojure environment (REPL).
LightTable is a Clojure aware editor that supports the dynamic workflow of Clojure development in a REPL. LightTable is also written in Clojure (and ClojureScript).
The following pages will show you how to set up LightTable and Leiningen.
"},{"location":"development-environments/java/","title":"Java","text":""},{"location":"development-environments/java/#java-a-host-platform-for-clojure","title":"Java - a host platform for Clojure","text":"You will need to have a Java Runtime Edition (usually installed on most computers by default) to run any Clojure applications. Version 8 is recommended (although version 6 & 7 should work).
To test if you have Java on your computer, open a command line window and run the command
java -version\n
"},{"location":"development-environments/java/#installing-the-java-runtime-edition","title":"Installing the Java Runtime Edition","text":"Download and install the latest Oracle Java SDK (version 1.8 at time of writing).
Alternatively, install OpenJDK or Zulu build of OpenJDK
"},{"location":"development-environments/java/#ubuntu","title":"Ubuntu","text":"The OpenJDK is available as a package on Ubuntu and can be installed via the Ubuntu software center or via the command line:
sudo apt-get install openjdk-8-jre\n
"},{"location":"development-environments/java/#why-is-java-required","title":"Why is Java Required","text":"Clojure was designed as a hosted language, which means it is developed and run on top of Java's Virtual Machine (JVM). However, its not necessary to learn the Java language to use Clojure.
Clojure is compiled into Java bytecode when you evaluate the code. This compilation happens in the background so you dont usually see it happening. For example, if you are using the Clojure REPL then each time you evaluate an expression it is compiled into Java bytecode and then injected into the running REPL and the results are then returned. This all happens pretty instantaneously.
Most of the current Clojure tooling was developed for Clojure on the JVM, for example Leiningen.
As Clojure runs on Java you can also use all the other libraries that run on the Java Virtual machine, regardless of whether those libraries were written in Java, Clojure, Scala, JRuby, jython, Groovy, etc.
"},{"location":"development-environments/leiningen/","title":"Leiningen Build tool","text":"leiningen.org (pronounced line-ing-en) is a very powerful build automation tool for automating Clojure projects. With Leiningen you can:
- Create Clojure Projects with templates
- Define and manage dependencies
- Run an interactive Clojure environment (REPL)
- Run unit tests using Clojure.test
- Run your Clojure application
- Create a deployable Clojure application, as Java Jar file
- Deploy a Clojure library to a remote repository
"},{"location":"development-environments/leiningen/#install-leiningen","title":"Install Leiningen","text":"Download the install script from leiningen.org and run the Leiningen script in a terminal
On Linux and MacOSX, make the script executable first
chmod a+x lein\n./lein\n
Hint I put the lein
script in ~/bin
directory which is part of my operating system execution path ($PATH). To include the ~/bin
directory in the system path, I add the following code to the ~/.profile
file
"},{"location":"development-environments/leiningen/#testing-leiningen-is-working","title":"Testing Leiningen is working","text":"Test that Leiningen is installed with the following command
lein version\n
Output should look similar to:
Leiningen 2.6.1 on Java 9-internal OpenJDK 64-Bit Server VM\n
"},{"location":"development-environments/lighttable/","title":"LightTable","text":"LightTable is a simple development tool that supports Clojure, ClojureScript, JavaScript and Python languages. The tool is open source and written in Clojure & ClojureScript (with a little JavaScript & CSS)
"},{"location":"development-environments/lighttable/#install-lighttable","title":"Install Lighttable","text":"Download lighttable.com and follow the suggested instructions:
MacOSX Install the lighttable.dmg
file just as any other MacOSX package
Linux Extract the contents of the downloaded lighttable file to a suitable directory (/usr/local
or ~/apps
). Add LightTable
to the system $PATH
, or add the following script to the system $PATH
.
Windows Download the windows zip file for LightTable and extract the installer, following the instructions inside the installer.
"},{"location":"development-environments/lighttable/#lighttable-configuration","title":"LightTable configuration","text":"Lighttable configuration is in the file user.behaviours
. Open the user behaviours file, Ctrl-space
and type user behaviors
. When you save the file, Ctrl-s
, changes are applied immediately.
Sample User Behaviours file
Here is a sample of user behaviours file for LightTable
"},{"location":"development-environments/lighttable/#using-lighttable","title":"Using LightTable","text":"LightTable has an online tutorial entitled Getting started with LightTable
I create a project first with Leiningen, open the project directory in the LightTable workspace and open any files I want to work with. I then connect the open editor window for the file by pressing Ctrl-Enter
at the end of an expression.
Hint my approach is documented in the quick demo section of my Clojure & LightTable slides from JAXLondon 2013.
"},{"location":"development-environments/other-tools/","title":"Other Development tools for Clojure","text":"There are several development tools you can use to support your Clojure development.
My current choice of development environment is Spacemacs, a feature rich configuration for Emacs. See my article on Spacemacs for Clojure development
Some common setups I have seen in use for Clojure development are:
- Modern - LightTable, Leiningen, Git
- Modern Classic - Spacemacs with Clojure layer, Leiningen, magit
- Classic - Emacs with Cider, Leiningen, magit
- Java (IntelliJ) - Cursive Clojure
- Java (Eclipse) - Counterclockwise documentation site
- Ubiquitous - Vim, nailgun, Leiningen, Git
- Simple - Nightcode, Leiningen, Git
- Lightweight - Atom, Protorepl, Leiningen, Git
There may be many more variations, however you should find a development environment with at minimum the following features:
- starting & using a REPL, with in-line evaluation
- syntax highlighting & coloured brackets (eg. rainbow-delimiters in Emacs)
- autocomplete of names (functions, symbols, keywords, etc)
- snippets / templates
"},{"location":"development-environments/other-tools/#tools-for-developers-with-a-java-background","title":"Tools for developers with a Java background","text":"Clojure runs on the Java Virtual Machine so its not surprising that there is good support for Clojure in the major Java IDEs.
"},{"location":"development-environments/other-tools/#eclipse","title":"Eclipse","text":"Counterclockwise is an Eclipse IDE plugin to provide an integrated development environment for Clojure. Take a look at the Counterclockwise documentation site for installation instructions
"},{"location":"development-environments/other-tools/#intellij","title":"IntelliJ","text":"Cursive is a Clojure IDE that aims to understands your code. Advanced structural editing, refactor, VCS integration and much more, all out of the box. It is currently a standalone tool, although will eventually become an IntelliJ plugin.
La Clojure is a plugin for IntelliJ IDEA. Provides Clojure language support: syntax and error highlighting, completion, navigation and refactor.
"},{"location":"development-environments/other-tools/#netbeans","title":"Netbeans","text":"Netbeans did have great support for Clojure, but unfortunately at the time of writing the Clojure plugin has been unmaintained for so long it is not a viable tool to use for Clojure development.
"},{"location":"games/","title":"Writing Games with Clojure","text":"Games are driven by events and require state to be managed, so are a good way to explore how to manage state with immutable values.
For games in Clojure the events are simply function calls and we prefer to pass the state around rather than have a central mutable container for our state.
This section will contain several games that have been built using a functional approach with immutable data structures.
- TicTacToe on the command line
"},{"location":"games/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"Pull requests are welcome
"},{"location":"games/#hintgames-in-clojurescript","title":"Hint::Games in ClojureScript","text":"There is a section on games in the Practicalli ClojureScript book, including a TicTacToe game using Reagent (react.js style library) and Scalable Vector Graphics (SVG).
"},{"location":"games/tictactoe-cli/","title":"TicTacToe on the command line","text":"Tic-tac-toe is a paper-and-pencil game for two players, X and O, who take turns marking the spaces in a 3\u00d73 grid. The player who succeeds in placing three of their marks in a horizontal, vertical, or diagonal row wins the game
The code for this section is published on GitHub at: practicalli/tictactoe-cli
A TicTacToe game that you run on the command line. The game takes input from a human player and the program is the second player.
Output from the game appears in the REPL
Current board:\n1 | 2 | 3\n---------\n4 | 5 | 6\n---------\n7 | 8 | 9\nX: Select your move (press a number between 1 and 9 then press enter)\nCurrent board:\nX | 2 | 3\n---------\n4 | 5 | 6\n---------\n7 | 8 | 9\nO: Select your move (press a number between 1 and 9 then press enter)\nCurrent board:\nX | O | 3\n---------\n4 | 5 | 6\n---------\n7 | 8 | 9\nX: Select your move (press a number between 1 and 9 then press enter)\nCurrent board:\nX | O | X\n---------\n4 | 5 | 6\n---------\n7 | 8 | 9\nO: Select your move (press a number between 1 and 9 then press enter)\nCurrent board:\nX | O | X\n---------\nO | 5 | 6\n---------\n7 | 8 | 9\nX: Select your move (press a number between 1 and 9 then press enter)\nCurrent board:\nX | O | X\n---------\nO | X | 6\n---------\n7 | 8 | 9\nO: Select your move (press a number between 1 and 9 then press enter)\nCurrent board:\nX | O | X\n---------\nO | X | O\n---------\n7 | 8 | 9\nX: Select your move (press a number between 1 and 9 then press enter)\nCurrent board:\nX | O | X\n---------\nO | X | O\n---------\nX | 8 | 9\nPlayer X wins!\n
"},{"location":"games/tictactoe-cli/#references","title":"References","text":" - TicTacToe game created by Brian Will.
"},{"location":"games/tictactoe-cli/create-project/","title":"Create a Clojure project","text":"Create a project for our game.
{% tabs deps=\"deps.edn projects\", lein=\"Leiningnen projects\" %}
{% content \"deps\" %} Create a new project using clj-new
alias, found in Practicalli Clojure CLI Config
clojure -M:new practicalli/tictactoe-cli\n
Open the project in a Clojure aware editor or run a rebel REPL
clojure -M:repl/rebel\n
Once the rebel REPL is running, load the project and change to the main namespace
(require 'practicalli/tictactoe-cli)\n\n(in-ns 'practicalli/tictactoe-cli)\n
{% content \"lein\" %} The default Leiningen template is suitable fine for the project as no additional libraries are used.
lein new tictactoe-cli\n
git clone https://github.com/practicalli/tictactoe-cli.git\n
"},{"location":"games/tictactoe-cli/create-project/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"games/tictactoe-cli/create-project/#hintalternatively-clone-the-github-repository","title":"Hint::Alternatively clone the github repository","text":"You can also clone the tictactoe-cli game from GitHub
"},{"location":"games/tictactoe-cli/create-project/#updating-clojure-version-and-licence","title":"Updating Clojure version and licence","text":"In the project.clj
file I have updated Clojure to version 1.10.0 and changed the licence to be the more open Creative Commons license.
(defproject tictactoe-cli \"0.1.0-SNAPSHOT\"\n :description \"TicTacToe game played on the command line\"\n :url \"https://github.com/practicalli/tictactoe-cli\"\n :license {:name \"Creative Commons Attribution Share-Alike 4.0 International\"\n :url \"https://creativecommons.org\"}\n :dependencies [[org.clojure/clojure \"1.10.0\"]])\n
I also removed the license
file and added a brief description of the project to the README.md
file
{% endtabs %}
"},{"location":"install/","title":"Install Clojure","text":"Clojure CLI provides the foundation for Clojure development, providing a declarative approach to:
- Run Clojure programs and tools
- Run a REPL process (Read-Eval-Print Loop) and provides a basic interactive terminal UI
- Manage packaged dependencies from Maven (jars) and use Git repositories as dependencies
Practicalli Clojure Config community tools
Practicalli Clojure CLI Config is a user configuration providing aliases for a wide range of community tools which extends the features of Clojure CLI. The aliases include tools to create, develop, build and deploy Clojure code. Aliases are used heavily in the Practicalli books.
If the Practicalli Clojure CLI config is not used, review the deps.edn
file from the GitHub repository and add relevant aliases definitions to your own Clojure CLI configuration.
"},{"location":"install/#pre-requisites","title":"\"Pre-requisites\"","text":"A Java Virtual Machine hosts Clojure. Java 21 is the current Long Term Support version providing a stable platform to run Clojure
"},{"location":"install/#additional-tools","title":"Additional tools","text":"Clojure connected editor
A Clojure connected editor provides the most effective way to write and maintain Clojure projects. The editor connects to (or starts) a Clojure REPL and code can be evaluated as its typed, showing the results instantly in line with the code.
Clojure LSP server generates static analysis of code which editors can surface as code diagnostics. Analysis supports effective code navigate and refactor tools. Practicalli Clojure LSP config configures
Data Inspectors
Data inspectors visualize results of Clojure code evaluation and allow navigation of nested data or paging through large data sets.
Portal is highly recommended data inspector and included in projects generated with Practicalli Project Templates.
Alternative development tools Leiningen is the long-standing development tool for Clojure. All the code examples in this book should work with Leiningen when a correctly configured project.clj
file is created which includes all the necessary library dependencies. Libraries included via aliases should be added as either :dev-dependencies
or :aliases
in the Leiningen project.clj
file.
"},{"location":"install/clojure-cli/","title":"Install Clojure CLI","text":"Clojure CLI is a command line tool for running a Clojure REPL, project or tool.
Clojure CLI automatically downloads required library dependencies, including the Clojure Standard library.
Clojure distributed as a library Clojure is distributed as a library (.jar
Java ARchive) via Maven Central.
A deps.edn
file specifies the version of Clojure to be used with a project.
:deps {org.clojure/clojure {:mvn/version \"1.12.0\"}}\n
The Clojure CLI tool provides a default Clojure library version if not specified in the project or user deps.edn
files.
Clojure releases
Practicalli Clojure CLI Config extends the Clojure CLI with a range of development tools as well as configuration for Clojure LSP and cljstyle code format tool.
LinuxHomebrewWindows Use the Linux script installer from Clojure.org - Getting Started to install or update to the latest stable release
curl -L -O https://github.com/clojure/brew-install/releases/latest/download/linux-install.sh && \\\nchmod +x linux-install.sh && \\\nsudo ./linux-install.sh\n
The installation creates /usr/local/bin/clojure
, /usr/local/bin/clj
wrapper and /usr/local/lib/clojure
directory.
Use alternative location - unattended install --prefix
option specifies an alternative lolcation for the Clojure CLI install.
When permissions are not available or for automating the install without password prompt, use a local user specific install, e.g.
curl -L -O https://github.com/clojure/brew-install/releases/latest/download/linux-install.sh && \\\nchmod +x linux-install.sh && \\\n./linux-install.sh --prefix $HOME/.local/\n
Include version number for specific release Each Clojure CLI version is a number that represents the version of Clojure used and the build version of the Clojure CLI tool, e.g. 1.11.1.1413
.
Clojure CLI Releases page
Include the version in the script name for repeatable environments, e.g. in Dockerfile configuration and Continuous Integraion workflows. Clojure CLI install specific version
curl -L -O https://github.com/clojure/brew-install/releases/1.11.1.1413/download/linux-install.sh && \\\nchmod +x linux-install-1.11.1.1413.sh\nsudo ./linux-install-1.11.1.1413.sh\n
Practically recommends setting XDG_CONFIG_HOME
to the .config
directory, to avoid creating another dot directory in the root of the user account. Add the following to ~/.bashrc
for the bash shell or ~/.zshenv
for Zsh.
export XDG_CONFIG_HOME=\"$HOME/.config\"\n
Use the Homebrew command with the clojure/tools tap, as defined in the Clojure.org Getting started guide
brew install clojure/tools/clojure\n
Use Homebrew to update an install of Clojure CLI to the latest release
brew upgrade clojure/tools/clojure\n
Homebrew on Linux or Windows with WSL
For Windows 10 use Windows Subsystem for Linux and Windows Terminal are recommended if you have administrative privileges and are comfortable using a Unix system on the command line.
Alternatively install scoop.sh, a command line installer for windows. Powershell 5 or greater is required. Follow the scoop-clojure getting started guide, summarized here:
Open \"Windows PowerShell\" and enter the following commands to configure the shell:
iwr -useb get.scoop.sh | iex\nSet-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force\n
Then in the same PowerShell window, install the Clojure related tools using the following commands: scoop bucket add extras\nscoop bucket add java\nscoop bucket add scoop-clojure https://github.com/littleli/scoop-clojure\nscoop install git 7zip pshazz temurin-lts-jdk clj-deps leiningen clj-kondo vscode coreutils windows-terminal\n
Reference: Clojure CLI Install - Clojure.org Getting Started - official guide
"},{"location":"install/clojure-cli/#practicalli-clojure-cli-config","title":"Practicalli Clojure CLI Config","text":"Add a wide range of community tools to extend the capabilities of Clojure CLI via the aliases.
Clone Practicalli Clojure CLI Config GitHub repository, first removing the $XDG_CONFIG_HOME/clojure
and $HOME/.clojure
directory if they exist.
User configuration locations If XDG_CONFIG_HOME
environment variable is set, then the user configuration is $XDG_CONFIG_HOME/clojure/deps.edn
Otherwise the user configuration is $HOME/.clojure/deps.edn
.
CLJ_CONFIG
environment variable can be used to set a custom location, overriding any other location.
Practicalli recommends FreeDesktop XDG location
Practically recommends setting XDG_CONFIG_HOME
to the .config
directory to simplify versioning of configuration.
Configure ~/.bashrc
for the bash shell Bash .bashrc file
export XDG_CONFIG_HOME=\"$HOME/.config\"\n
Configure ~/.zshenv
for Zsh
# Set XDG_CONFIG_HOME for clean management of configuration files\nexport XDG_CONFIG_HOME=\"${XDG_CONFIG_HOME:=$HOME/.config}\"\nexport XDG_DATA_HOME=\"${XDG_DATA_HOME:=$HOME/.local/share}\"\nexport XDG_CACHE_HOME=\"${XDG_CACHE_HOME:=$HOME/.cache}\"\nexport ZDOTDIR=\"${ZDOTDIR:=$XDG_CONFIG_HOME/zsh}\"\n
Free Desktop XDG CONFIGClassic Config If XDG_CONFIG_HOME
environment variable is set, clone the repository to $XDG_CONFIG_HOME/clojure
Via SSH
git clone git@github.com:practicalli/clojure-cli-config.git $XDG_CONFIG_HOME/clojure\n
Via HTTPS:
git clone https://github.com/practicalli/clojure-cli-config.git $XDG_CONFIG_HOME/clojure\n
Clojure CLI will look for its configuration in $HOME/.clojure
directory if $XDG_CONFIG_HOME
and CLJ_CONFIG
environment variables not set. Via SSH ```shell git clone git@github.com:practicalli/clojure-cli-config.git $HOME/.clojure
```
Via HTTPS\n```shell\ngit clone https://github.com/practicalli/clojure-cli-config.git $HOME/.clojure\n```\n
"},{"location":"install/clojure-cli/#check-configuration","title":"Check Configuration","text":"clojure -Sdescribe
shows the version of Clojure CLI installed and configuration locations used.
clojure -Sdescribe\n
The output of the command includes the version of Clojure CLI in the :version
key
{:version \"1.11.1.1386\"\n :config-files [\"/usr/local/lib/clojure/deps.edn\" \"/home/practicalli/.config/clojure/deps.edn\" ]\n :config-user \"/home/practicalli/.config/clojure/deps.edn\"\n :config-project \"deps.edn\"\n :install-dir \"/usr/local/lib/clojure\"\n :config-dir \"/home/practicalli/.config/clojure\"\n :cache-dir \"/home/practicalli/.cache/clojure\"\n :force false\n :repro false\n :main-aliases \"\"\n :repl-aliases \"\"}\n
clojure -Sversion
will shows the version of Clojure CLI being when the clojure
command is used to run a REPL or other Clojure command.
"},{"location":"install/clojure-cli/#optional-rlwrap-readline","title":"Optional rlwrap readline","text":"The rlwrap
binary is a basic readline tool that provides a history of commands entered into a terminal UI when running a Clojure REPL with the clj
wrapper script.
Pressing the Up and Down keys will scroll through the code previously entered in the REPL.
rlwrap
is available with most Linux systems. Look for install instructions by searching for rlwrap in a web browser or build from source from the rlwrap GitHub repository.
Use Rebel Readline for a rich terminal UI experience
rebel readline is an advanced readline tool providing auto-completion, documentation, signature help and multi-line editing, all within a terminal UI
Rebel is a much richer experience than the clj
wrapper with rlwrap
. Rebel should not be used with clj
.
Rebel Readline is part of the Practicalli Clojure CLI config.
"},{"location":"install/java/","title":"Java Host","text":"Java is a host platform for Clojure, on which Clojure projects and tools run. Java provides a virtual machine which runs the bytecode generated when Clojure code is compiled.
Java virtual machine includes a Just In Time (JIT) compiler that optimises running of bytecode.
Practicalli recommends OpenJDK version 21
"},{"location":"install/java/#install-java","title":"Install Java","text":"Check to see if there is an appropriate version of Java already installed.
Open a terminal and run the command
java --version\n
If Java is installed and on the execution path, the version infomation is returned
Debian PackagesHomebrewWindowsManual Install Java development kit (JDK) using the apt
package manager (login as su -
or prefix the command with sudo
)
apt install openjdk-21-jdk\n
Check available versions of OpenJDK Long terms support versions should include OpenJDK 17 and may include OpenJDK 21. Check versions available via the apt
package management tool.
apt search --names-only openjdk\n
Optionally include Java docs and sources Install the openjdk-21-doc
locally to provide Javadocs to support Java Interop code.
Install the openjdk-21-source
package to support navigation of Java Object and Method source code, especially useful when using Java Interoperability from within Clojure code.
sudo apt install openjdk-21-doc openjdk-21-source\n
Practicalli Clojure CLI Config provides the :src/java17
alias to include the Java sources in the classpath when running a REPL.
If openjdk-21-jdk
package is not available, add the Ubuntu OpenJDK personal package archive
sudo add-apt-repository ppa:openjdk-r/ppa\nsudo apt-get update\n
When multiple versions of Java are installed, set the version using the update-alternatives
command in a terminal
sudo update-alternatives --config java\n
Available java versions will be listed. Enter the list number for the version you wish to use.
Using Homebrew, run the following command in a terminal to install Java 17:
brew install openjdk@21\n
Switching between Java versions More than one version of Java can be installed on MacOSX. Set the Java version by opening a terminal and using one of the following commands
Show the Java versions installed
/usr/libexec/java_home -V\n
Switch to Java version 21
export JAVA_HOME=$(/usr/libexec/java_home -v 21)\n
Alternatively, install JEnv Java version manager
For Windows 10 use Windows Subsystem for Linux and Windows Terminal are recommended if you have administrative privileges and are happy to use a Unix system on the command line.
Alternatively use scoop.sh, a command line installer for windows. Powershell 5 or greater is required.
Follow the scoop-clojure install instructions, summarized here:
scoop bucket add java\nscoop install temurin-lts-jdk\n
scoop can also be used to install clojure
If neither Scoop or Windows Subsystem for Linux work, try the Chocolatey package manager. Install the Java Runtime (JRE) using the following command in a command line window
choco install javaruntime\n
If Chocolatey does not work, then try the manual Java install.
Download OpenJDK from Adoptium - pre-build OpenJDK binaries freely available for multiple operating systems.
Run the file once downloaded and follow the install instructions.
"},{"location":"install/java/#multiple-versions-of-java","title":"Multiple versions of Java","text":" jenv provides a simple way to switch between multiple installed versions of Java. jenv can be used to set the java version globally, for the current shell or for a specific project by adding .java-version
file containing the Java version number in the root of the project.
"},{"location":"install/java/#a-little-java-knowledge","title":"A little Java Knowledge","text":"Very little knowledge of the Java language or the Java Virtual Machine is required.
It is quite simple to call Java methods from Clojure, although there are a wealth of functions and libraries provided by Clojure and its community to minimise the need for Java Interoperability.
Reading stack traces may benefit from some Java experience, although its usually the first couple of lines in a stack trace that describe the issue.
Clojure uses its own build tools (Leiningen, Clojure CLI tools) and so Java build tool knowledge is not required.
When libraries are added to a project, they are downloaded to the $HOME/.m2
directory. This is the default Maven cache used by all JVM libraries.
clojure -Spom
will generate a Maven pom.xml file used for deployment. Understanding of a minimal Maven POM (pom.xml) file is useful when managing issues with packaging and deployment.
Maven in 5 minutes
The Java Virtual Machine is highly optimised and does not usually require any options to enhance its performance. The most likely configuration to supply to the JVM are to manage the amount of memory assigned, specifically for resource constrained environments.
"},{"location":"introduction/clojure-in-15-minutes/","title":"Clojure in 15 minutes","text":"A quick tour of the Clojure syntax and common functions, which is so terse you can read through this page in around 15 minutes and have a basic understanding of the language.
Try the code out in the REPL
Start a Clojure REPL or use a Clojure aware editor connected to a REPL and experiment with these code examples.
Using the REPL provides instant feedback on each expression as they are evaluated, greatly increasing your understanding.
"},{"location":"introduction/clojure-in-15-minutes/#comments","title":"Comments","text":";;
two semi-colons for a line comment, ;
single semi-colon to comment the rest of the line
#_
comment reader macro to comment out the next form
(comment ,,,)
form to comment all the containing forms, useful to separate experimental and established code in a namespace.
"},{"location":"introduction/clojure-in-15-minutes/#clojure-expressions","title":"Clojure expressions","text":"Clojure is mostly written with \"expressions\", a lists of elements inside parentheses, ()
, separated by space characters.
Clojure evaluates the first element in an expression as a function call. Additional elements in the expression are passed as value arguments to the called function.
Function call with value and expression as arguments
(+ 2007 (* 1 16))\n
Functions can be passed as an argument
(map inc (range 0 99))\n
"},{"location":"introduction/clojure-in-15-minutes/#organising-clojure","title":"Organising Clojure","text":"Clojure code is organised into one or more namespaces. The namespace represents the directory path and file name that contains the code of the particular namespace.
A company name or community repository name is often used making the namespace unique and easier to share & reuse.
ns form returns nil value The (ns namespace.,,,)
expression returns a nil
value, as its work is done behind the scenes.
All Clojure functions must return a value and nil
is a value that means 'no value'.
Define a namespace
src/practicalli/game_board.clj(ns practicalli.game-board)\n
Define a longer namespace
src/com/company/product/component_name.clj(ns com.company.product.component-name)\n
Namespaces use dash, directory and file names use underscore Clojure uses kebab-case
for names (common in Lisp dialects)
Unfortunately the Java Virtual Machine that hosts Clojure does not support dash, -
, in file and directory names, so an underscore, -
, character is used
"},{"location":"introduction/clojure-in-15-minutes/#string-manipulation","title":"String manipulation","text":"The str
function creates a new string from all the arguments passed
Combine strings into a single string value
(str \"Hello\" \" \" \"World\")\n
\"Hello World\"
is returned from evaluating the expression.
clojure.string library for manipulating strings
clojure.string
library functions manipulate values and return string values (other clojure.core functions my return characters as results, e.g. map
)
"},{"location":"introduction/clojure-in-15-minutes/#math-truth-prefix-notation","title":"Math, Truth & prefix notation","text":"Functions use prefix notation, so you can do math with multiple values very easily
Prefix syntax takes multiple arguments
(+ 1 2 3 5 7 9 12) ; => 40\n
Math in Clojure is very precise, no need for operator precedence rules (as there are no operators)
Nesting forms defined a very precise calculation
Parentheses used instead of operator preceedence rules
(* 1 2 (- 24 (* 7 3)))\n
6
is returned as the value. Nested expressions are typically read inside out. (* 7 3)
is 21
, giving (- 24 21)
expression resulting in 3
. Finally the expression becomes (* 1 2 3)
, resulting in a value of 6
Maintain precision for calculations using a Ratio type in Clojure
Clojure Ratio value
(/ 27 7) ; => 27/7\n
22/7
is returned as the value, rather than a floating point value (double) which may loose some precision due to rounding.
"},{"location":"introduction/clojure-in-15-minutes/#equality","title":"Equality","text":"=
function provides a test for equality
Equal values return a boolean true
(= 1 1) ; => true\n
Unequals values return a boolean false
(= 2 1) ; => false\n
true
and false
are Boolean values and can be used literally in Clojure.
"},{"location":"introduction/clojure-in-15-minutes/#predicates","title":"Predicates","text":"A predicate is a function that returns a boolean true
or false
value and by convention the function name ends in ?
, e.g. true?
, false?
, seq?
, even?
, uuid?
.
and
& or
functions can be used to chain the results of predicates together for more interesting conditional tests.
All predicates are true, returning true
(and (true? true) (not false)) ; => true\n
One of the predicates or values is true
(or nil (not= true false) (true? (complement true?)) ) ; => true\n
Truthy and Falsy values in Clojure
false
boolean value and nil
value are considered false in Clojure.
All other values are consider true.
Clojure Standard Library Predicate Functions
"},{"location":"introduction/clojure-in-15-minutes/#collections-sequences","title":"Collections & Sequences","text":"The most common data collections in Clojure:
(1 2 \"three\")
or (list 1 2 \"three\")
- a list of values read from start to end (sequential access) [1 2 \"three\"]
or (list 1 2 \"three\")
- a vector of values with index (random access) {:key \"value\"}
or (hash-map :key \"value\")
- a hash-map with zero or more key value pairs (associative relation) #{1 2 \"three\"}
or (set 1 2 \"three\")
- a unique set of values
A list ()
is evaluated as a function call. The first element of the list the name of the function to call and additional values are arguments to the function.
The '
quote function informs the Clojure reader to treat the list as data only.
A quoted list is treated as data
'(1 2 3) ; => (1 2 3)\n
Lists and vectors are collections
(and (coll? '(1 2 3)) (coll? [1 2 3])) ; => true\n
Only lists are sequences
(seq? '(1 2 3)) ; => true\n(seq? [1 2 3]) ; => false\n
Sequences are an interface for logical lists, which can be lazy. \"Lazy\" means that a sequence of values are not evaluated until accessed.
A lazy sequence enables the use of large or even an infinite series, like so:
Lazy sequences
(range) ; => (0 1 2 3 4 ...) - an infinite series\n(take 4 (range)) ; (0 1 2 3) - lazyily evaluate range and stop when enough values are taken\n
Use cons to add an item to the beginning of a list or vector
(cons 4 [1 2 3]) ; => (4 1 2 3)\n(cons 4 '(1 2 3)) ; => (4 1 2 3)\n
Use conj to add an item relative to the type of collection, to the beginning of a list or the end of a vector
(conj [1 2 3] 4) ; => [1 2 3 4]\n(conj '(1 2 3) 4) ; => (4 1 2 3)\n
Use concat to add lists or vectors together
(concat [1 2] '(3 4)) ; => (1 2 3 4)\n
Use filter, map to interact with collections
(map inc [1 2 3]) ; => (2 3 4)\n(filter even? [1 2 3]) ; => (2)\n
Use reduce to reduce them
(reduce + [1 2 3 4])\n; = (+ (+ (+ 1 2) 3) 4)\n; => 10\n
Reduce can take an initial-value argument too
(reduce conj [] '(3 2 1))\n; => [3 2 1]\n
Equivalent of (conj (conj (conj [] 3) 2) 1)
"},{"location":"introduction/clojure-in-15-minutes/#annonymous-functions","title":"Annonymous Functions","text":"Use fn
to create new functions that defines some behaviour. fn
is referred to as an anonymous fuction as it has no external name to be referenced by and must be called within a list form.
(fn hello [] \"Hello World\")\n
Wrap a (fn ,,,)
form in parens to call it and return the result.
Call an anonymous function
((fn hello [] \"Hello World\")) ; => \"Hello World\"\n
Normally the anonymous function is used inline with other code
Use anonymous function within other code
(map (fn [x] (* x 2)) [1 2 3 4 [1 2 3 4 5]5])\n
Make the anonymous function reusable by binding it to a shared name (var
) using def
.
The var
name bound to the function can now be called anywhere in the namespace.
As def
creates a var
(variable) name, the developer can changed the expression the name is bound to and re-evaluated to use the changed behaviour.
Bind a name to the anonymous function
(def hello-world\n (fn hello [] \"Hello World\"))\n
Evaluate annonymous function by evaluating its name
hello-world\n
NOTE: hello-world
is a name and not a function call, so parentheses are not required.
"},{"location":"introduction/clojure-in-15-minutes/#shared-functions","title":"Shared Functions","text":"It is more common to use the defn
macro to define a function. This is the same as defining the fn
function and the def
name within the same expression
Define a function with defn macro
(defn hello-world\n \"I am a humble doc-string, please describe the function purpose\"\n []\n \"Hello World\")\n
#'user/hello-world
is the value returned from evaluating the expression, showing the fully qualified name of the function. Note: the fully qualified name will be different when defined in a differnt namespace than user
.
A defn
function has the scope of the current namespace, so can be called anywhere in the namespace or in a namepace that has used require
to include this namespace.
Call a function
(hello-world)\n
The []
vector is used to define the argument names for the function. There can be zero or more arguments.
Call function with arguments
(defn hello [name]\n (str \"Hello \" name))\n
The correct number of arguments must be used when calling a function, or an error will be returned.
Call function with arguments
(hello \"Steve\") ; => \"Hello Steve\"\n
Pass a hash-map as an argument Simplify the design of a function signature by passing all arguments as a hash-map.
(defn data-processing\n [data]\n (let [body (get data :body)])\n (transform body))\n
Associative Destructuring can be used to automatically create local variables from the desired keys contained in the map, giving access to the value of each key. (defn data-processing\n [{:keys [body]}]\n (transform body))\n
Clojure supports multi-variadic functions, allowing one function definition to respond to a function call with different number of arguments. This provides a simple form of polymorphism based on the number of arguments.
(defn hello-polly\n ([] \"Hello World\") ; (1)!\n ([name] (str \"Hello \" name))) ; (2)!\n
-
Call hello-polly
with one argument
(hello-polly \"Jake\") ; => \"Hello Jake\"\n
-
Call hello-polly
with zero arguments
(hello-polly) ; => \"Hello World\"\n
Functions can pack extra arguments up in a seq for you
(defn count-args [& args]\n (str \"You passed \" (count args) \" args: \" args))\n(count-args 1 2 3) ; => \"You passed 3 args: (1 2 3)\"\n
You can mix regular and packed arguments
(defn hello-count [name & args]\n (str \"Hello \" name \", you passed \" (count args) \" extra args\"))\n(hello-count \"Finn\" 1 2 3)\n; => \"Hello Finn, you passed 3 extra args\"\n
"},{"location":"introduction/clojure-in-15-minutes/#hash-map-collections","title":"Hash-map collections","text":"(class {:a 1 :b 2 :c 3}) ; => clojure.lang.PersistentArrayMap\n
Keywords are like strings with some efficiency bonuses
(class :a) ; => clojure.lang.Keyword\n
Maps can use any type as a key, but usually keywords are best
(def stringmap (hash-map \"a\" 1, \"b\" 2, \"c\" 3))\nstringmap ; => {\"a\" 1, \"b\" 2, \"c\" 3}\n\n(def keymap (hash-map :a 1 :b 2 :c 3))\nkeymap ; => {:a 1, :c 3, :b 2} (order is not guaranteed)\n
Commas are whitespace commas are always treated as whitespace and are ignored by the Clojure reader
Retrieve a value from a map by calling it as a function
(stringmap \"a\") ; => 1\n(keymap :a) ; => 1\n
Keywords can be used to retrieve their value from a map. Strings cannot be used.
(:b keymap) ; => 2\n\n(\"a\" stringmap)\n; => Exception: java.lang.String cannot be cast to clojure.lang.IFn\n
Retrieving a non-present value returns nil
(stringmap \"d\") ; => nil\n
Use assoc to add new keys to hash-maps
(assoc keymap :d 4) ; => {:a 1, :b 2, :c 3, :d 4}\n
But remember, clojure types are immutable!
keymap ; => {:a 1, :b 2, :c 3}\n
Use dissoc to remove keys
(dissoc keymap :a :b) ; => {:c 3}\n
"},{"location":"introduction/clojure-in-15-minutes/#sets","title":"Sets","text":"(class #{1 2 3}) ; => clojure.lang.PersistentHashSet\n(set [1 2 3 1 2 3 3 2 1 3 2 1]) ; => #{1 2 3}\n
Add a member with conj
(conj #{1 2 3} 4) ; => #{1 2 3 4}\n
Remove one with disj
(disj #{1 2 3} 1) ; => #{2 3}\n````\n\nTest for existence by using the set as a function:\n\n```clojure\n(#{1 2 3} 1) ; => 1\n(#{1 2 3} 4) ; => nil\n
There are more functions in the clojure.sets namespace.
"},{"location":"introduction/clojure-in-15-minutes/#useful-forms","title":"Useful forms","text":"Logic constructs in clojure are just macros, and look like everything else
(if false \"a\" \"b\") ; => \"b\"\n(if false \"a\") ; => nil\n
Use let to create temporary bindings
(let [a 1 b 2]\n (> a b)) ; => false\n
Group statements together with do
(do\n (print \"Hello\")\n \"World\") ; => \"World\" (prints \"Hello\")\n
Functions have an implicit do
(defn print-and-say-hello [name]\n (print \"Saying hello to \" name)\n (str \"Hello \" name))\n(print-and-say-hello \"Jeff\") ;=> \"Hello Jeff\" (prints \"Saying hello to Jeff\")\n
So does let
(let [name \"Urkel\"]\n (print \"Saying hello to \" name)\n (str \"Hello \" name)) ; => \"Hello Urkel\" (prints \"Saying hello to Urkel\")\n
"},{"location":"introduction/clojure-in-15-minutes/#namespaces-and-libraries","title":"Namespaces and Libraries","text":"Namespaces are used to organise code into logical groups. The top of each Clojure file has an ns
form that defines the namespace name. The domain part of the namespace name is typically the organisation or community name (e.g. GitHub user/organisation)
(ns domain.namespace-name)\n
All Practicalli projects have namespace domains of practicalli
(ns practicalli.service-name)\n
require
allows code from one namespace to be accessed from another namespace, either from a the same Clojure project or from a library added to the project classpath.
The :as
directive with require
is used to specify an alias name, a short-hand for the full library name
Or :refer [function-name var-name]
can be used to specify specific functions and data (vars) that are available directly
A required directive is typically added to a namespace form
(ns practicalli.service-name\n (require [clojure.set :as set]))\n
The functions from clojure.set can be used via the alias name, rather than the fully qualified name, i.e. clojure.set/intersection
(set/intersection #{1 2 3} #{2 3 4}) ; => #{2 3}\n(set/difference #{1 2 3} #{2 3 4}) ; => #{1}\n
:require
directive can be used to include multiple library namespaces
(ns test\n (:require\n [clojure.string :as string]\n [clojure.set :as set]))\n
require
can be used by itself, usually within a rich code block
(comment\n (require 'clojure.set :as set))\n
"},{"location":"introduction/clojure-in-15-minutes/#strong-dynamic-types","title":"Strong Dynamic Types","text":"Clojure is strongly typed, so everything is a type in Clojure.
Clojure is dynamically typed, so Clojure infers the type. A type does not need to be specified in the code, making the code simpler and more concise.
Clojure is a hosted language and uses the type system of the platform it runs upon. For example, Clojure uses Java object types for booleans, strings and numbers under the covers.
Use class
or type
function to inspect the type of some code in Clojure.
(type 1) ; Integer literals are java.lang.Long by default\n(type 1.); Float literals are java.lang.Double\n(type \"\"); Strings always double-quoted, and are java.lang.String\n(type false) ; Booleans are java.lang.Boolean\n(type nil); The \"null\" value is called nil\n
Vectors and Lists are java classes too!
(type [1 2 3]); => clojure.lang.PersistentVector\n(type '(1 2 3)); => clojure.lang.PersistentList\n
Type hints
Type hints can be used to avoid reflection look-ups where performace critical issues have been identified. Type hints are not required in general. Clojure Type Hints
"},{"location":"introduction/clojure-in-15-minutes/#java-interop","title":"Java Interop","text":"Java has a huge and useful standard library, so you'll want to learn how to get at it.
Use import to load a java package
(import java.util.Date)\n
Or import from a java package name
(ns test\n (:import\n java.util.Date\n java.util.Calendar))\n
Use the class name with a \".\" at the end to make a new instance
(Date.) ; <a date object>\n
Use .
to call methods. Or, use the \".method\" shortcut
(. (Date.) getTime) ; <a timestamp>\n(.getTime (Date.)) ; exactly the same thing.\n
Use / to call static methods
(System/currentTimeMillis) ; <a timestamp> (system is always present)\n
Use doto to make dealing with (mutable) classes more tolerable
(import java.util.Calendar)\n(doto (Calendar/getInstance)\n (.set 2000 1 1 0 0 0)\n .getTime) ; => A Date. set to 2000-01-01 00:00:00\n
"},{"location":"introduction/contributing/","title":"Contributing to Practicalli","text":"By submitting content ideas and corrections you are agreeing they can be used in any work by Practicalli under the Creative Commons Attribution ShareAlike 4.0 International license. Attribution will be detailed via GitHub contributors.
Please raise an issue before creating a pull request
Raising an issue or post on the #practicalli channel of Clojurians Slack community avoids disappointment if the contribution would not be accepted and saves time for all.
Practicalli books are written in markdown and use MkDocs to generate the published website via a GitHub workflow. MkDocs can also run a local server using the make docs
target from the Makefile
All content and interaction with any persons or systems must be done so with respect and within the Practicalli Code of Conduct.
"},{"location":"introduction/contributing/#book-status","title":"Book status","text":""},{"location":"introduction/contributing/#submit-and-issue-or-idea","title":"Submit and issue or idea","text":"If something doesnt seem quite right or something is missing from the book, please raise an issue via the GitHub repository explaining in as much detail as you can.
Raising an issue before creating a pull request will save you and the maintainer time.
Alternatively, reach out to Practicalli via the #practicalli
channel of the Clojurians Slack community.
Clojurians Slack community
"},{"location":"introduction/contributing/#considering-a-pull-request","title":"Considering a Pull request?","text":"Pull Request Commits must be cryptographically signed
All commits contributed to Practicalli must be signed via a legitimate SSH or GPG key to avoid the risk of commit spoofing.
Configure commit signing with SSH key - Practicalli Engineering
All pull requests must include an entry in CHANGELOG.md or will not be merged. A changelog entry allows the community to follow the changes to the book.
Each pull request will have a number of CI workflows run against the contribution, checking the format of the content and if a changelog entry has been provided.
Please keep pull requests small and focused, as they are much quicker to review and easier to accept. Ideally PR's should be for a specific page or at most a section.
A PR with a list of changes across different sections will be closed without merging as these take considerable time to review.
Issues such as grammar improvements are typically a sign of a rushed section that requires a rewrite, so a pull request to fix a typeographic error will probably not be merged. Raise an issue, or post a thread in the Clojurians Slack #practicall channel
"},{"location":"introduction/contributing/#thank-you-to-everyone-that-has-contributed","title":"Thank you to everyone that has contributed","text":"A huge thank you to Rich Hickey and the team at Cognitect for creating and continually guiding the Clojure language.
The Clojure community has been highly supportive of everyone using Clojure and I'd like to thank everyone for the feedback and contributions. I would also like to thank everyone that has joined in with the London Clojurins community, ClojureBridgeLondon, Clojurians Slack community, Clojurians Zulip community and Clojureverse community.
Thank you to everyone who sponsors the Practicalli websites and videos and for the Clojurists Together sponsorship, it helps me continue the work at a much faster pace.
Special thanks to Bruce Durling for getting me into Cloure in the first place.
"},{"location":"introduction/first-taste-of-clojure/","title":"Clojure Quick Reference","text":"The basic Clojure syntax and a few common functions you should probably learn first.
The examples are editable (using an embedded REPL) so feel free to experiment and watch as the return value changes as you change the code. Reload the page if you want to reset all the code back to the starting point.
Install Clojure on your computer if you want to experiment even further.
Want to go deeper already?
Watch the Clojure language video series by Brian Will for a detailed introduction to key parts of the language. Or discover Clojure core functions by completing challenges on 4Clojure.org and then watching how Practicalli solved them.
"},{"location":"introduction/first-taste-of-clojure/#calling-functions","title":"Calling functions","text":"The first element in a list, ()
, is a call to a function. Any other elements are passed to the function as arguments. The examples show how to call functions with multiple arguments.
(+ 1 2)\n
(+ 3 (* 2 (- 7 2) 4) (/ 16 4))\n
(str \"Clojure is \" (- 2021 2007) \" years old\")\n
(inc 1)\n
(map inc [1 2 3 4 5])\n
(filter odd? (range 11))\n
Prefix notation and parens
Hugging code with ()
is a simple syntax to define the scope of code expressions. No additional ;
, ,
or spaces are required.
Treating the first element of a list as a function call is referred to as prefix notation, which greatly simplifies Clojure syntax. Prefix notation makes mathematical expressions completely deterministic, eliminating the need for operator precedence.
"},{"location":"introduction/first-taste-of-clojure/#understanding-functions","title":"Understanding functions","text":"clojure.repl/doc
function returns the doc-string of the given function. A doc-string should be part of all public function definitions.
Clojure editors should provide commands to view doc-strings and the ability to jump to function definitions to view their source code
(clojure.repl/ddoc doc)\n
"},{"location":"introduction/first-taste-of-clojure/#modeling-data-with-collection-types","title":"Modeling data with Collection types","text":"Clojure has 4 main collection types, all immutable (cannot change once created) and can contain any Clojure types.
A list, ()
, used for calling functions and representing sequences. A linked list for sequential access.
(str \"lists used mainly \" (* 2 2) \" \" :code)\n
A vector, []
, used for simple collections of values. An indexed data structure for random access
[0 \"indexed\" :array (* 2 2) \"random-access\" 4 :data]\n
A map, {}
, use for descriptive data collections. An associative data structure for value lookup by unique keys (also known as a dictionary).
{ :hash-map :associative-collection :pairs {:key \"value\"} :aka \"dictionary\"}\n
A set, #{}
, use as a unique set of values. Sets are used to test if a value is contained within, i.e. predicates.
#{1 2 3 4 \"unique\" \"set\" \"of\" \"values\" \"unordered\" (* 3 9)}\n
Persistent data types
Values are immutable so when a function changes a value a new immutable value is created. When creating new collection values, unchanged values are shared with the original collection. This sharing model is called persistent data types and enables immutable data to be used efficiently.
"},{"location":"introduction/first-taste-of-clojure/#using-data-structures","title":"Using data structures","text":"Using the map
and inc
function, increment all the numbers in a vector
(map inc [1 2 3 4 5])\n
The above map
function is roughly equivalent to the following expression
(conj [] (inc 1) (inc 2) (inc 3) (inc 4) (inc 5))\n
The conj
function creates a new collection by combining a collection and one or more values.
map
reduce
filter
are common functions for iterating through a collection / sequence of values
(map * [1 3 5 8 13 21] [3 5 8 13 21 34])\n
(filter even? [1 3 5 8 13 21 34])\n
(reduce + [31 28 30 31 30 31])\n
(empty? [])\n
Many Clojure core functions for collections
map
, reduce
, apply
, filter
, remove
are just a few examples of Clojure core functions that work with data structures.
"},{"location":"introduction/first-taste-of-clojure/#defining-custom-functions","title":"Defining custom functions","text":"(defn square-of\n \"Calculates the square of a given number\"\n [number]\n (* number number))\n\n(square-of 9)\n
Function definitions can also be used within other expressions, useful for mapping custom functions over a collection
(map (fn [number] (* number number)) [1 2 3 4 5])\n
"},{"location":"introduction/first-taste-of-clojure/#defining-local-names","title":"Defining local names","text":"Use the let
function as a simple way to experiment with code designs
(let [data (range 24 188)\n total (reduce + data)\n values (count data)]\n (str \"Average value: \" (/ total values)))\n
Define local names to remove duplication in function definitions, or to simplify algorithms
(defn square-of\n \"Calculates the square of a given number\"\n [number]\n (* number number))\n\n(square-of 9)\n
"},{"location":"introduction/first-taste-of-clojure/#defining-names-for-values-vars","title":"Defining names for values (vars)","text":"A name bound to a value can be used to represent that value throughout the code. Names can be bound to simple values (numbers, strings, etc.), collections or even function calls.
def
binds a name to a value with the scope of the current namespace. def
is useful for data that is passed to multiple functions within a namespace.
Evaluating a name will return the value it is bound to.
(def public-health-data\n [{:date \"2020-01-01\" :confirmed-cases 23814 :recovery-percent 15}\n {:date \"2020-01-02\" :confirmed-cases 24329 :recovery-percent 14}\n {:date \"2020-01-03\" :confirmed-cases 25057 :recovery-percent 12}])\n\npublic-health-data\n
def for shared values, let for locally scoped values
let
function is used to bind names to values locally, such as within a function definition. Names bound with def
have namespace scope so can be used with any code in that namespace.
"},{"location":"introduction/first-taste-of-clojure/#iterating-over-collections","title":"Iterating over collections","text":"map
iterates a function over a collection of values, returning a new collection of values
(map inc (range 20))\n
reduce
iterates a function over the values of a collection to produce a new result
(reduce + (range 101))\n
Reducing functions are function definitions used by the reduce
function over a collection
(reduce (fn [[numerator denominator] accumulator]\n [(+ numerator accumulator)\n (inc denominator)])\n [0 0]\n (range 1 20))\n
Functions can call themselves to iterate over a collection. Using a lazy sequence means only the required numbers are generated, ensuring efficiency of operation and making the function usable in many different scenarios.
(defn fibonacci-sequence\n [current-number next-number]\n (lazy-seq\n (cons current-number\n (fibonacci-sequence next-number (+ current-number next-number)))))\n\n(take 10 (fibonacci-sequence 0 1))\n
"},{"location":"introduction/first-taste-of-clojure/#host-interoperability","title":"Host Interoperability","text":"The REPL in this web page is running inside a JavaScript engine, so JavaScript functions can be used from within ClojureScript code (ClojureScript is Clojure that runs in JavaScript environments).
In the box below, replace ()
with (js/alert \"I am a pop-up alert\")
()\n
Java libraries in Clojure
java.lang library is available in Clojure by default and many other Java methods can be included by using their full name, e.g. (java.lang.Date.)
will return the current date.
"},{"location":"introduction/first-taste-of-clojure/#next-steps","title":"Next steps","text":"Install Clojure on your computer if you want to experiment even further or keep on reading more about Clojure.
"},{"location":"introduction/five-steps-to-clojure/","title":"5 Steps to Clojure","text":""},{"location":"introduction/five-steps-to-clojure/#set-up-your-environment","title":"Set up your environment","text":"Install Clojure and a build tool
Setup a Clojure aware editor
- Emacs & CIDER - Spacemacs, Doom, Prelude
- Neovim & Conjure
- VSCode & Clover or Calva
- Sublime Text & SublimedClojure
"},{"location":"introduction/five-steps-to-clojure/#learn-the-syntax","title":"Learn the syntax","text":""},{"location":"introduction/five-steps-to-clojure/#practice-the-core-functions","title":"Practice the core functions","text":""},{"location":"introduction/five-steps-to-clojure/#def-defn-let","title":"def / defn / let","text":""},{"location":"introduction/five-steps-to-clojure/#map-reduce-apply","title":"map / reduce / apply","text":""},{"location":"introduction/five-steps-to-clojure/#for-while-loop-recur","title":"for / while / loop / recur","text":""},{"location":"introduction/five-steps-to-clojure/#adopt-functional-programming-practices","title":"Adopt functional programming practices","text":""},{"location":"introduction/five-steps-to-clojure/#learn-the-commonly-used-libraries","title":"Learn the commonly used libraries","text":""},{"location":"introduction/five-steps-to-clojure/#server-side-websites","title":"Server-side websites","text":""},{"location":"introduction/five-steps-to-clojure/#ring-compojure-reitit-hiccup-selma","title":"Ring / Compojure / Reitit / Hiccup | Selma","text":""},{"location":"introduction/five-steps-to-clojure/#react-client-side-single-page-apps","title":"React client-side single page apps","text":""},{"location":"introduction/five-steps-to-clojure/#reactjs-om-next-reagent-re-frame","title":"React.js / Om-next / Reagent / Re-frame","text":""},{"location":"introduction/five-steps-to-clojure/#coreasync","title":"core.async","text":""},{"location":"introduction/five-steps-to-clojure/#full-stack-apps","title":"Full Stack apps","text":""},{"location":"introduction/five-steps-to-clojure/#kit-framework","title":"Kit Framework","text":""},{"location":"introduction/learning-clojure/","title":"Learning Clojure","text":"Learning the syntax of Clojure is really quick (its very small and simple). Learning to think functionally and discovering the 700+ functions in the Clojure API can take a little longer. I recommend you find someone with a bit of Clojure experience to guide you.
Here is my suggested path to learning Clojure and thinking functionally. Many of the tasks can be done in parallel.
"},{"location":"introduction/learning-clojure/#simple-rather-than-complex-the-foundation-of-clojure","title":"Simple rather than complex - the foundation of Clojure","text":"Gaining an appreciation that systems should be simple is a crucial step truly understanding Clojure. So early in your journey into Clojure, spend an hour watching Rich Hickey talk about Simple made Easy - (transcript of talk).
"},{"location":"introduction/learning-clojure/#experience-the-clojure-syntax","title":"Experience the Clojure syntax","text":"Take a quick look at the Syntax of Clojure. The syntax is very small, so this will take about 15 minutes to 1 hour (dependent on your own experiences with coding). Don't try to remember all the syntax, it will come through practise.
- eg. Clojure in 15 minutes
"},{"location":"introduction/learning-clojure/#set-up-an-enjoyable-environment-to-work-in","title":"Set up an enjoyable environment to work in","text":"Find how to use Clojure with your favourite editor or IDE. Configure this tool so you can easily run a REPL and evaluate some expressions.
- repl.it - web based repl you can share / fork
- Spacemacs - for the ultimate Emacs & Vim experience
- IntelliJ and Cursive
- Leiningen & any editor you like
"},{"location":"introduction/learning-clojure/#building-a-frame-of-reference-for-functional-programming","title":"Building a frame of reference for functional programming","text":"Find an introductory book that you like which provides lots of example code to help you feel more comfortable with the syntax and more importantly the major concepts of functional programming with Clojure. Type in the exercises as you read and don't be afraid to play around with code examples
- Clojure for the Brave and the True
- Living Clojure - includes a training guide
- Practicalli Clojure - you are already here :)
- ClojureBridge London workshop - aimed at those new to coding
- PurelyFunctional - Introduction to Clojure
"},{"location":"introduction/learning-clojure/#practice-clojure-standard-library-clojurecore","title":"Practice Clojure standard library (clojure.core)","text":"Practice Clojure. Write lots of small and relatively simple examples in Clojure and experiment with the code in the REPL and try break things. This will start helping you learn the Clojure API
You should become comfortable in your understanding of:
- basic values (strings, numbers, etc) and persistent collections (list, vector, map, set)
- binding names to values and their scope (def, defn, let)
- calling functions, defining functions, arity options for functions
- Higher order functions and basics of functional composition (map, reduce, filter, etc)
- Designing with data, Extensible Data Notation (EDN), data manipulation
Activities to help practice Clojure include:
- 4Clojure.org - aim to complete the first 50 exercises, the first 10 are relatively easy
- Coding Kata exercises
- Awesome Kata collection
- Alice In Wonderland inspired Katas
- Attend coding dojo events - e.g. London Clojurians
"},{"location":"introduction/learning-clojure/#solidify-some-of-the-basics-you-have-learned-so-far","title":"Solidify some of the basics you have learned so far","text":"Work on a relatively small project that you care about enough to work on
- eg. a tool to help you at work
"},{"location":"introduction/learning-clojure/#learn-more-tools-to-help-you-think-functionally","title":"Learn more tools to help you think functionally","text":" - mostly using immutable values and pure functions
- functional composition, sequences and transducers
- atoms for managing mutable state changes (with immutable values)
"},{"location":"introduction/learning-clojure/#get-a-broader-view-of-clojure-and-learn-some-common-practices","title":"Get a broader view of Clojure and learn some common practices","text":"Start reading a book which is aimed at intermediate
Watch Video's about Clojure on subjects that are relevant to work or projects you want to work on.
Follow tutorials on Clojure, especially those that introduce the amazing libraries available in Clojure
- Lambda Island
- PurelyFunctional.tv
- Practical.li
Work with some of the most common libraries in Clojure
- Ring / Compojure for web development (server side)
- ClojureScript for web UI or native mobile apps (client side)
- Reagent - reactjs style single page apps
- Reagent deep dive - excellent tutorial
- core.async - for asynchronous programming
- clojure.string - specific functions for string manipulation
- tools.logging
- java.jdbc - database access
- data.zip - manipulating trees of data, e.g. XML
"},{"location":"introduction/repl-workflow/","title":"REPL Driven Development","text":"Always be REPL'ing
Coding without a REPL feels limiting. The REPL provides fast feedback from code as its crafted, testing assumptions and design choices every step of the journey to a solution - John Stevenson, Practical.li
Clojure is a powerful, fun and highly productive language for developing applications and services. The clear language design is supported by a powerful development environment known as the REPL (read, evaluate, print, loop). The REPL gives you instant feedback on what your code does and enables you to test either a single expression or run the whole application (including tests).
REPL driven development is the foundation of working with Clojure effectively
An effective Clojure workflow begins by running a REPL process. Clojure expressions are written and evaluated immediately to provide instant feedback. The REPL feedback helps test the assumptions that are driving the design choices.
- Read - code is read by the Clojure reader, passing any macros to the macro reader which converts those macros into Clojure code.
- Evaluate - code is compiled into the host language (e.g. Java bytecode) and executed
- Print - results of the code are displayed, either in the REPL or as part of the application.
- Loop - the REPL is a continuous process that evaluates code, either a single expression or the whole application.
Design decisions and valuable data from REPL experiments can be codified as specifications and unit tests
Practicalli REPL Reloaded Workflow
The principles of REPL driven development are implemented in practice using the Practicalli REPL Reloaded Workflow and supporting tooling. This workflow uses Portal to inspect all evaluation results and log events, hot-load libraries into the running REPL process and reloads namespaces to support major refactor changes.
"},{"location":"introduction/repl-workflow/#evaluating-source-code","title":"Evaluating source code","text":"A REPL connected editor is the primary tool for evaluating Clojure code from source code files, displaying the results inline.
Source code is automatically evaluated in its respective namespace, removing the need to change namespaces in the REPL with (in-ns
) or use fully qualified names to call functions.
Evaluate Clojure in a Terminal UI REPL Entering expressions at the REPL prompt evaluates the expression immediately, returning the result directly underneath
"},{"location":"introduction/repl-workflow/#rich-comment-blocks-living-documentation","title":"Rich Comment blocks - living documentation","text":"The (comment ,,,)
function wraps code that is only run directly by the developer using a Clojure aware editor.
Expressions in rich comment blocks can represent how to use the functions that make up the namespace API. For example, starting/restarting the system, updating the database, etc. Expressions provide examples of calling functions with typical arguments and make a project more accessible and easier to work with.
Clojure Rich Comment to manage a service
(ns practicalli.gameboard.service)\n\n(defn app-server-start [port] ,,,)\n(defn app-server-start [] ,,,)\n(defn app-server-restart [] ,,,)\n\n(defn -main\n \"Start the service using system components\"\n [& options] ,,,)\n\n(comment\n (-main)\n (app-server-start 8888)\n (app-server-stop)\n (app-server-restart 8888)\n\n (System/getenv \"PORT\")\n (def environment (System/getenv))\n (def system-properties (System/getProperties))\n ) ; End of rich comment block\n
Rich comment blocks are very useful for rapidly iterating over different design decisions by including the same function but with different implementations. Hide clj-kondo linter warnings for redefined vars (def
, defn
) when using this approach.
;; Rich comment block with redefined vars ignored\n#_{:clj-kondo/ignore [:redefined-var]}\n(comment\n (defn value-added-tax []\n ;; algorithm design - first idea)\n\n (defn value-added-tax []\n ;; algorithm design - second idea)\n\n ) ;; End of rich comment block\n
The \"Rich\" in the name is an honourary mention to Rich Hickey, the author and benevolent dictator of Clojure design.
"},{"location":"introduction/repl-workflow/#design-journal","title":"Design Journal","text":"A journal of design decisions makes the code easier to understand and maintain. Code examples of design decisions and alternative design discussions are captured, reducing the time spent revisiting those discussions.
Journals simplify the developer on-boarding processes as the journey through design decisions are already documented.
A Design Journal is usually created in a separate namespace, although it may start as a rich comment at the bottom of a namespace.
A journal should cover the following aspects
- Relevant expressions use to test assumptions about design options.
- Examples of design choices not taken and discussions why (saves repeating the same design discussions)
- Expressions that can be evaluated to explain how a function or parts of a function work
The design journal can be used to create meaningful documentation for the project very easily and should prevent time spent on repeating the same conversations.
Example design journal
Design journal for TicTacToe game using Reagent, ClojureScript and Scalable Vector Graphics
"},{"location":"introduction/repl-workflow/#viewing-data-structures","title":"Viewing data structures","text":"Pretty print shows the structure of results from function calls in a human-friendly form, making it easier for a developer to parse and more likely to notice incorrect results.
Tools to view and navigate code
- Cider inspector is an effective way to navigate nested data and page through large data sets.
- Portal Inspector to visualise many kinds of data in many different forms.
"},{"location":"introduction/repl-workflow/#code-style-and-idiomatic-clojure","title":"Code Style and idiomatic Clojure","text":"Clojure aware editors should automatically apply formatting that follows the Clojure Style guide.
Live linting with clj-kondo suggests common idioms and highlights a wide range of syntax errors as code is written, minimizing bugs and therefore speeding up the development process.
Clojure LSP is build on top of clj-kondo
Clojure LSP uses clj-kondo static analysis to provide a standard set of development tools (format, refactor, auto-complete, syntax highlighting, syntax & idiom warnings, code navigation, etc).
Clojure LSP can be used with any Clojure aware editor that provides an LSP client, e.g. Spacemacs, Doom Emacs, Neovim, VSCode.
Clojure Style Guide
The Clojure Style guide provides examples of common formatting approaches, although the development team should decide which of these to adopt. Emacs clojure-mode
will automatically format code and so will Clojure LSP (via cljfmt). These tools are configurable and should be tailored to the teams standard.
"},{"location":"introduction/repl-workflow/#data-and-function-specifications","title":"Data and Function specifications","text":" Clojure spec is used to define a contract on incoming and outgoing data, to ensure it is of the correct form.
As data structures are identified in REPL experiments, create data specification to validate the keys and value types of that data.
;; ---------------------------------------------------\n;; Address specifications\n(spec/def ::house-number string?)\n(spec/def ::street string?)\n(spec/def ::postal-code string?)\n(spec/def ::city string?)\n(spec/def ::country string?)\n(spec/def ::additional string?)\n\n(spec/def ::address ; Composite data specification\n (spec/keys\n :req-un [::street ::postal-code ::city ::country]\n :opt-un [::house-number ::additional]))\n;; ---------------------------------------------------\n
As the public API is designed, specifications for each functions arguments are added to validate the correct data is used when calling those functions.
Generative testing provides a far greater scope of test values used incorporated into unit tests. Data uses clojure.spec to randomly generate data for testing on each test run.
"},{"location":"introduction/repl-workflow/#test-driven-development-and-repl-driven-development","title":"Test Driven Development and REPL Driven Development","text":"Test Driven Development (TDD) and REPL Driven Development (RDD) complement each other as they both encourage incremental changes and continuous feedback.
Test Driven Development fits well with Hammock Time, as good design comes from deep thought
- RDD enables rapid design experiments so different approaches can easily and quickly be evaluated .
- TDD focuses the results of the REPL experiments into design decisions, codified as unit tests. These tests guide the correctness of specific implementations and provide critical feedback when changes break that design.
Unit tests should support the public API of each namespace in a project to help prevent regressions in the code. Its far more efficient in terms of thinking time to define unit tests as the design starts to stabilize than as an after thought.
clojure.test
library is part of the Clojure standard library that provides a simple way to start writing unit tests.
Clojure spec can also be used for generative testing, providing far greater scope in values used when running unit tests. Specifications can be defined for values and functions.
Clojure has a number of test runners available. Kaocha is a test runner that will run unit tests and function specification checks.
Automate local test runner
Use kaocha test runner in watch mode to run tests and specification check automatically (when changes are saved)
clojure -X:test/watch\n
"},{"location":"introduction/repl-workflow/#continuous-integration-and-deployment","title":"Continuous Integration and Deployment","text":"Add a continuous integration service to run tests and builds code on every shared commit. Spin up testable review deployments when commits pushed to a pull request branch, before pushing commits to the main deployment branch, creating an effective pipeline to gain further feedback.
- CircleCI provides a simple to use service that supports Clojure projects.
- GitHub Workflows and GitHub actions marketplace to quickly build a tailored continuous integration service, e.g. Setup Clojure GitHub Action.
- GitLab CI
"},{"location":"introduction/repl-workflow/#live-coding-with-data-stuart-halloway","title":"Live Coding with Data - Stuart Halloway","text":"There are few novel features of programming languages, but each combination has different properties. The combination of dynamic, hosted, functional and extended Lisp in Clojure gives developers the tools for making effective programs. The ways in which Clojure's unique combination of features can yield a highly effective development process.
Over more than a decade we have developed an effective approach to writing code in Clojure whose power comes from composing many of its key features. As different as Clojure programs are from e.g. Java programs, so to can and should be the development experience. You are not in Kansas anymore!
This talk presents a demonstration of the leverage you can get when writing programs in Clojure, with examples, based on my experiences as a core developer of Clojure and Datomic.
"},{"location":"introduction/who-uses-clojure/","title":"Who uses Clojure","text":"Hundreds of companies actively advertised their Clojure adoption. Given the broad participation in user groups there are clearly many more organizations using Clojure at some level in their technology stack.
A quick scan of various job sites shows Clojure positions at companies like Walmart, Facebook, Staples, Consumer Reports, Salesforce, and Amazon. It doesn't get much more mainstream than that.
If you are looking for a new role using Clojure or other functional programming languages, visit Functional Works, the only Functional Recruiters in the world.
Here is just a small and diverse set of example companies that I am aware of that use Clojure for development.
Company Type of applications Boeing Boeing 737 MAX - onboard maintenance Puppet Labs DevOps apps & services e.g. trapperkeeper Cisco Malware analysis & threat intelligence platform (expert system with core.logic) Deuche Bank (UK) Processing event streams from Apache Storm Atlassian Collaborative editing platform for all their products Netflix Map-Reduce languages for writing apps for Hadoop / Pig USwitch (UK) Creating meaningful data from multiple sources Daily Mail Online (UK) Publishing pipeline Circle CI (USA) Front-end of CI server in ClojureScript & test suite CitiGroup Financial Trading Student Loans company (UK) Loans management system written in Clojure LinkedIn Powers the LinkedIn social graph Walmart (USA) eReceipts project to process every purchase from 5,000+ stores SwiftKey (UK) Predictive intelligence platform (possibly used with Microsoft Cortana) Roomkey.co.uk Hotel booking system to rival Expedia (with a tiny development team) Funding Circle (UK & USA) Adopting Clojure as their main language (from Ruby, etc) Braintree Payment processing pipeline with Kafka Mastodon C Data centre analysis (Incanta, Storm) Thoughtworks Agile development for Client projects world wide Vero Insurance (AUS) Rebuilt policy management system in Clojure with Thoughworks Meta-X Performance art (Overtone, Quil) Salesforce (USA) Build & deployment with Puppet & Application Routing with nginx-clojure There are many more examples of projects on the HackerNews thread: Ask HN: Who's using Clojure in Production
"},{"location":"introduction/who-uses-clojure/#tech-radar","title":"Tech Radar","text":"Clojure is also defined as a technology that can be adopted since 2014, according to the Thoughtworks technology radar.
JUXT also created its own Clojure specific technology radar as there is such an encompassing ecosystem of libraries and services.
"},{"location":"introduction/writing-tips/","title":"Writing tips for MkDocs","text":"Making the docs more engaging using the mkdocs-material theme reference guide
Configuring Colors Material for MkDocs - Changing the colors lists the primary and accent colors available.
HSL Color Picker for codes to modify the theme style, overriding colors in docs/assets/stylesheets/extra.css
"},{"location":"introduction/writing-tips/#hypertext-links","title":"Hypertext links","text":"Links open in the same browser window/tab by default.
Add {target=_blank}
to the end of a link to configure opening in a new tab
[link text](url){target=_blank}\n
"},{"location":"introduction/writing-tips/#buttons","title":"Buttons","text":"Convert any link into a button by adding {.md-button}
class names to end of the markdown for a link, which uses .md-button-primary
by default. Include target=_blank
for buttons with links to external sites.
[link text](http://practical.li/blog){.md-button target=_blank}\n
Or specify a different class
[link text](http://practical.li/blog){.md-button .md-button-primary}\n
Add an icon to the button
Practicalli Issues Practicalli Blog
[:fontawesome-brands-github: Practicalli Issues](http://practical.li/blog){ .md-button .md-button-primary }\n[:octicons-heart-fill-24: Practicalli Blog](http://practical.li/blog){ .md-button .md-button-primary }\n
Search all supported icons
"},{"location":"introduction/writing-tips/#youtube-video","title":"YouTube video","text":"Use an iframe element to include a YouTube video, wrapping in a paragraph tag with center alignment to place the video in a centered horizontal position
<p style=\"text-align:center\">\n<iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/rQ802kSaip4\" title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>\n</p>\n
mkdocs material does not have direct support for adding a YouTube video via markdown.
"},{"location":"introduction/writing-tips/#admonitions","title":"Admonitions","text":"Supported admonition types
Note
Use !!!
followed by NOTE
Adding a title
Use !!!
followed by NOTE
and a \"title in double quotes\"
Shh, no title bar just the text... Use !!!
followed by NOTE
and a \"\"
empty double quotes
Abstract
Use !!!
followed by ABSTRACT
Info
Use !!!
followed by INFO
Tip
Use !!!
followed by TIP
Success
Use !!!
followed by SUCCESS
Question
Use !!!
followed by QUESTION
Warning
Use !!!
followed by WARNING
Failure
Use !!!
followed by FAILURE
Danger
Use !!!
followed by DANGER
Bug
Use !!!
followed by BUG
Example
Use !!!
followed by EXAMPLE
Quote
Use !!!
followed by QUOTE
"},{"location":"introduction/writing-tips/#collapsing-admonitions","title":"Collapsing admonitions","text":"Note Collapse those admonitions using ???
instead of !!!
Replace with a title Use ???
followed by NOTE
and a \"title in double quotes\"
Expanded by default Use ???+
, note the +
character, followed by NOTE
and a \"title in double quotes\"
"},{"location":"introduction/writing-tips/#inline-blocks","title":"Inline blocks","text":"Inline blocks of text to make a very specific callout within text
Info
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.
Adding something to then end of text is probably my favourite
Info
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.
"},{"location":"introduction/writing-tips/#code-blocks","title":"Code blocks","text":"Code blocks include a copy icon automatically
Syntax highlighting in code blocks
(defn my-function ; Write a simple function\n \"With a lovely doc-string\"\n [arguments]\n (map inc [1 2 3]))\n
Give the code block a title using title=\"\"
after the backtics and language name
src/practicalli/gameboard.clj(defn my-function\n \"With a lovely doc-string\"\n [arguments]\n (map inc [1 2 3]))\n
We all like line numbers, especially when you can set the starting line
src/practicalli/gameboard.clj(defn my-function\n \"With a lovely doc-string\"\n [arguments]\n (map inc [1 2 3]))\n
Add linenums=42
to start line numbers from 42 onward
clojure linenums=\"42\" title=\"src/practicalli/gameboard.clj\"\n
"},{"location":"introduction/writing-tips/#annotations","title":"Annotations","text":"Annotations in a code block help to highlight important aspects. Use the comment character for the language followed by a space and a number in brackets
For example, in a shell code block, use # (1)
where 1 is the number of the annotation
Use a number after the code block to add the text for the annotation, e.g. 1.
. Ensure there is a space between the code block and the annotation text.
ls -la $HOME/Downloads # (1)\n
- I'm a code annotation! I can contain
code
, formatted text, images, ... basically anything that can be written in Markdown.
Code blocks with annotation, add !
after the annotation number to suppress the #
character
(defn helper-function\n \"Doc-string with description of function purpose\" ; (1)!\n [data]\n (merge {:fish 1} data)\n )\n
- Always include a doc-string in every function to describe the purpose of that function, identifying why it was added and what its value is.
GitHub action example with multiple annotations
name: ci # (1)!\non:\n push:\n branches:\n - master # (2)!\n - main\npermissions:\n contents: write\njobs:\n deploy:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-python@v4\n with:\n python-version: 3.x\n - run: pip install mkdocs-material # (3)!\n - run: mkdocs gh-deploy --force\n
-
You can change the name to your liking.
-
At some point, GitHub renamed master
to main
. If your default branch is named master
, you can safely remove main
, vice versa.
-
This is the place to install further [MkDocs plugins] or Markdown extensions with pip
to be used during the build:
pip install \\\n mkdocs-material \\\n mkdocs-awesome-pages-plugin \\\n ...\n
"},{"location":"introduction/writing-tips/#highlight-lines-in-code-blocks","title":"Highlight lines in code blocks","text":"Add highlight line meta data to a code block after the opening backticks and code block language.
hl_lines=\"2\"
highlights line 2 in the codeblock
(defn my-function\n \"With a lovely doc-string\"\n [arguments]\n (map\n inc\n [1 2 3]))\n
"},{"location":"introduction/writing-tips/#embed-external-files","title":"Embed external files","text":"--8<--
in a code block inserts code from a source code file or other text file
Specify a local file from the root of the book project (the directory containing mkdocs.yml)
Scheduled Version Check GitHub Workflow from source code file scheduled version check\n
Practicalli Project Templates Emacs project configuration - .dir-locals.el((clojure-mode . ((cider-preferred-build-tool . clojure-cli)\n (cider-clojure-cli-aliases . \":test/env:dev/reloaded\"))))\n
Code example reuse
Use an embedded local or external file (URL) when the same content is required in more than one place in the book.
An effective way of sharing code and configuration mutliple times in a book or across multiple books.
"},{"location":"introduction/writing-tips/#content-tabs","title":"Content tabs","text":"Create in page tabs that can also be
Setting up a project
Clojure CLILeiningen clojure -T:project/new :template app :name practicalli/gameboard\n
lein new app practicalli/gameboard\n
Or nest the content tabs in an admonition
Run a terminal REPL
Clojure CLILeiningen clojure -T:repl/rebel\n
lein repl\n
"},{"location":"introduction/writing-tips/#diagrams","title":"Diagrams","text":"Neat flow diagrams
Diagrams - Material for MkDocs
graph LR\n A[Start] --> B{Error?};\n B -->|Yes| C[Hmm...];\n C --> D[Debug];\n D --> B;\n B ---->|No| E[Yay!];
UML Sequence Diagrams
sequenceDiagram\n Alice->>John: Hello John, how are you?\n loop Healthcheck\n John->>John: Fight against hypochondria\n end\n Note right of John: Rational thoughts!\n John-->>Alice: Great!\n John->>Bob: How about you?\n Bob-->>John: Jolly good!
state transition diagrams
stateDiagram-v2\n state fork_state <<fork>>\n [*] --> fork_state\n fork_state --> State2\n fork_state --> State3\n\n state join_state <<join>>\n State2 --> join_state\n State3 --> join_state\n join_state --> State4\n State4 --> [*]
Class diagrams - not needed for Clojure
Entity relationship diagrams are handy though
erDiagram\n CUSTOMER ||--o{ ORDER : places\n ORDER ||--|{ LINE-ITEM : contains\n LINE-ITEM {\n customer-name string\n unit-price int\n }\n CUSTOMER }|..|{ DELIVERY-ADDRESS : uses
"},{"location":"introduction/writing-tips/#keyboard-keys","title":"Keyboard keys","text":"Represent key bindings with Keyboard keys. Each number and alphabet character has their own key.
- 1
++1++
for numbers - l
++\"l\"++
for lowercase character - U
++u++
for uppercase character or ++\"U\"++
for consistency
Punctionation keys use their name
- Space
++spc++
- ,
++comma++
- Left
++arrow-left++
For key sequences, place a space between each keyboard character
- Space g s
++spc++ ++\"g\"++ ++\"s\"++
For key combinations, use join they key identifies with a +
- Meta+X
++meta+x++
- Ctrl+Alt+Del
++ctrl+alt+del++
MkDocs keyboard keys reference
"},{"location":"introduction/writing-tips/#images","title":"Images","text":"Markdown images can be appended with material tags to set the size of the image, whether to appear on light or dark theme and support lazy image loading in browsers
SizeLazy LoadingAlignTheme SpecificAll Image Attributes {style=\"height:150px;width:150px\"}
specifies the image size
![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png#only-dark){style=\"height:150px;width:150px\"}\n
{loading=lazy}
specifies an image should lazily load in the browser
![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png){loading=lazy}\n
{aligh=left}
or {aligh=right}
specifies the page alignment of an image.
![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png#only-dark){align=right}\n![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-dark.png#only-light){align=right}\n
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.
![Kitty Logo](image/kitty-light.png#only-dark)
or ![Kitty Logo](image/kitty-light.png#only-light)
specifies the theme the image should be shown, allowing different versions of images to be shown based on the theme.
![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png#only-dark){style=\"height:150px;width:150px\"}\n![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-dark.png#only-light){style=\"height:150px;width:150px\"}\n
Use the theme toggle in the top nav bar to see the icon change between light and dark. Requires the color pallet toggle
Alight right, lazy load and set image to 150x150
![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png#only-dark){align=right loading=lazy style=\"height:64px;width:64px\"}\n![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-dark.png#only-light){align=right loading=lazy style=\"height:64px;width:64px\"}\n
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.
"},{"location":"introduction/writing-tips/#lists","title":"Lists","text":"Task lists
- Lorem ipsum dolor sit amet, consectetur adipiscing elit
- Vestibulum convallis sit amet nisi a tincidunt
- In hac habitasse platea dictumst
- In scelerisque nibh non dolor mollis congue sed et metus
- Praesent sed risus massa
- Aenean pretium efficitur erat, donec pharetra, ligula non scelerisque
Task List example
- [x] Lorem ipsum dolor sit amet, consectetur adipiscing elit\n- [ ] Vestibulum convallis sit amet nisi a tincidunt\n * [x] In hac habitasse platea dictumst\n * [x] In scelerisque nibh non dolor mollis congue sed et metus\n * [ ] Praesent sed risus massa\n- [ ] Aenean pretium efficitur erat, donec pharetra, ligula non scelerisque\n
"},{"location":"introduction/writing-tips/#tooltips","title":"Tooltips","text":"The humble tool tip
Hover me
with references
Hover me
Icon tool tip with a title
"},{"location":"introduction/writing-tips/#abreviations","title":"Abreviations","text":"The HTML specification is maintained by the W3C.
[HTML]: Hyper Text Markup Language [W3C]: World Wide Web Consortium
"},{"location":"introduction/writing-tips/#magic-links","title":"Magic links","text":"MagicLink can auto-link HTML, FTP, and email links. It can auto-convert repository links (GitHub, GitLab, and Bitbucket) and display them in a more concise, shorthand format.
Email Practicalli
Practicalli Neovim
"},{"location":"introduction/concepts/","title":"Clojure concepts","text":"Clojure is an elegant language for a more civilized development experience.
Clojure supports the creation of simple software systems using immutable values and encouraging a pragmatic approach to pure functional design.
A simple syntax means Clojure is quick to learn and a wide range of open source libraries provides a rapid way to build any kind of software. Designed as a hosted language, Clojure runs on many platforms including the Java Virtual Machine, GraalVM, Microsoft.Net, JavaScript engines. Simple host language interoperability provides access to libraries from a wide range of programming languages, further extending the reach of Clojure.
Experiment with the Clojure language to help understand concepts
Spend some time eevaluating code in the REPL and then revisit this section to get a deeper understanding of the design and philosophy of the Clojure approach to functional programming.
Clojure concepts are easier to relate to whist practicing with Clojure and building Clojure software solutions.
"},{"location":"introduction/concepts/#ten-big-ideas-plus-one","title":"Ten Big Ideas plus one","text":"The key to understanding Clojure is ideas, not language constructs but the concepts that shape the language.
Each of these ideas is valuable by itself, not only in Clojure. Taken together, however, they Begin to fill in the picture of why Clojure is changing the way many programmers think about software development.
- Extensible Data Notation
- Persistent Data Structures
- Sequences
- Transducers
- Specification
- Dynamic Development
- Async Programming
- Protocols
- ClojureScript
- Logic query / Logic Programming
- Atomic Succession Model
Stuart Halloway presents Clojure in 10 big ideas (plus one) in the following video, also see presentation Content
- 2013 RuPy slides
- 2017 Chicago JUG slides
"},{"location":"introduction/concepts/#antithesis-of-clojure-and-simple-software-design","title":"Antithesis of Clojure and simple software design","text":"In Narcissistic Design by Stuart Halloway, the antithesis of the Clojure view of software development is presented as a description of how unproductive and valueless much of the software industry has been in the past.
Its essentially a guide on what to avoid if you are a responsible and professional software developer.
"},{"location":"introduction/concepts/all-bytecode-in-the-end/","title":"Its all Bytecode in the end","text":"The REPL is your compiler
As soon as you evaluate your code in the REPL it is also being compiled in the background into Java Bytecode. So there is no need for a separate build and run phase.
Injecting code into the running environment provides the means for fast iterative development of code.
"},{"location":"introduction/concepts/clojure-made-simple/","title":"Clojure from the Author","text":"A series of important videos from Rich Hickey, the author of Clojure who spent over 2 years designing core of Clojure around the concept of simplicity. Since then Rich has stewarded the continued design and development of Clojure, ensuring it stays true to is founding principles.
You do not need to watch these videos to start coding in Clojure, but they are very useful to adopt the approach and design idioms that make Clojure a highly effective language for software development.
"},{"location":"introduction/concepts/clojure-made-simple/#expert-to-expert-rich-hickey-and-brian-beckman-inside-clojure","title":"Expert to Expert: Rich Hickey and Brian Beckman - Inside Clojure","text":"Discussing some of the key characteristics of the Clojure language and why those decisions were taken
"},{"location":"introduction/concepts/clojure-made-simple/#clojure-made-simple","title":"Clojure made simple","text":"Covers the major problems with software development and the challenges most programming languages fail to tackle completely.
Discusses a simple approach to software development and the big picture view of how Clojure solves these problems
"},{"location":"introduction/concepts/clojure-made-simple/#simplicity-matters","title":"Simplicity Matters","text":"!!! QUOTE Rich Hickey, Clojure Benevolent Dictator for Life As we move forward we have to take what we already have and make that [software] do more, make it do things differently, make it do things better, and as we try to take on manipulating software we are going to be challenged to understand it in order to make that happen. And I'll contend that you will completely be dominated by complexity. I don't care what processes you are using, I don't care how well you test or anything else. Complexity will dominate what you do.
"},{"location":"introduction/concepts/clojure-made-simple/#discussing-design","title":"Discussing Design","text":""},{"location":"introduction/concepts/clojure-made-simple/#the-value-of-values","title":"The value of values","text":"Rich Hickey provides analysis of the changing way we think about values (not the philosophical kind) in light of the increasing complexity of information technology and the advent of Big Data
Also see the related video: Database as a value by Rich Hickey
"},{"location":"introduction/concepts/clojure-made-simple/#understanding-clojure-as-a-programming-language","title":"Understanding Clojure as a programming language","text":""},{"location":"introduction/concepts/design/","title":"Clojure Design","text":"Clojure leads to a very component based approach to development. There are no huge and bloated frameworks in Clojure. The core is very small. Hundreds of focused libraries to use in collaboration.
Boiled down to the most simplest structure, Clojure applications you write typically look like this:
;; define a namespace\n(ns name-space.name)\n\n;; define one or more immutable data structures - the fewer the better typically\n(def my-data-structure [[{}{}]])\n\n;; define behaviour that acts on data structures inside one or more functions\n(defn my-function [parameter]\n (my-behaviour parameter))\n\n;; Call those functions to make your application do something\n(my-behaviour data)\n
Hint As functions always evaluate to a value, a function can be used as an argument to another function (or itself if you get recursive !!)
"},{"location":"introduction/concepts/design/#data-focused-design-maps-vectors","title":"Data focused design - Maps & Vectors","text":"Maps (hash-map) and vectors are two more built-in persistent data structures that are more commonly used to represent data within a Clojure application.
A vector is similar to an array in that its an indexed collection and so has fast random access. Vectors are a catch all data structure that can hold any type of information, including other data structures and function calls.
A hash-map is an associative data structure with key value pairs. The keys are most commonly represented with Clojure keywords, although keys can be strings, numbers, collections or functions so long as all the keys are unique.
Hash-maps are a collection of key / value pairs that provide an easy way to reference data by keys. Its common to use a Clojure keyword
type as the keys as keywords are self-referential (they point to themselves). Using keywords in a map means you can use a specific keyword as a function call on the map that returns its associated value.
Some examples of using these data structures this are:
;; A map of maps of maps with occasional vectors\n\n{:star-wars {\n :characters {\n :jedi [\"Luke Skywalker\"\n \"Obiwan Kenobi\"]\n :sith [\"Darth Vader\"\n \"Darth Sideous\"]\n :droids [\"C3P0\"\n \"R2D2\"]}\n :ships {\n :rebel-alliance [\"Millennium Falcon\"\n \"X-wing fighter\"]\n :imperial-empire [\"Intergalactic Cruiser\"\n \"Destroyer\"\n \"Im just making these up now\"]}}}\n
"},{"location":"introduction/concepts/design/#hintbasic-design-principle","title":"Hint::Basic design principle","text":"\u201cIt is better to have 100 functions operate on one data structure than 10 functions on 10 data structures.\u201d \u2014Alan Perlis
"},{"location":"introduction/concepts/design/#extensibility-via-macros","title":"Extensibility via Macros","text":"You can extend the language and define your own constructs using Macros.
The first example of this you see is from Leiningen. The defproject
function is a macro that helps you easily define the configuration of a Clojure project.
An example of a macro that is part of the core Clojure language is defn
. When you define a function with defn
it is syntactic sugar for defining a thing that is a function.
(defn my-function [argument] (my-behaviour argument) )\n\n (def my-function\n (fn [argument] (my-behaviour argument)))\n
"},{"location":"introduction/concepts/design/#special-forms-the-building-blocks-of-clojure","title":"Special forms - the building blocks of Clojure","text":"The following are the building blocks of Clojure, everything else is either a macro or a function
The Clojure / LISP special forms
def, do, if, let, loop, fn, quote, recur, set!, var\n
The forms added for Interoperability with the host platform (mainly Java / JVM)
monitor-enter, monitor-exit,\ncatch, dot ('.'), finally, new, throw, try\n
"},{"location":"introduction/concepts/features/","title":"Features of Clojure","text":""},{"location":"introduction/concepts/features/#dynamic-language","title":"Dynamic language","text":"A problem space can quickly be explored through code to test your assumptions. The design of code is easy to change as you are not managing type changes, Clojure is very good at managing data that would otherwise lead to exceptions.
As a dynamic language the code is quite terse and developers are encouraged to write very modular code, therefore it is easy to refactor.
"},{"location":"introduction/concepts/features/#dynamic-development-repl","title":"Dynamic Development - REPL","text":"Clojure has a REPL (Read Evaluate Print Loop), this is the Clojure run-time environment. You can define functions and data structures, then evaluate them to run either all your code or just a single expression. You can even change code and re-evaluate it whilst your application is still running and immediately see the effect that change has.
So the REPL is a very fast way to explore your problem domain with code
You could even connect to the REPL of a live system and change its behaviour without any down time (unless of course you write code that crashes).
"},{"location":"introduction/concepts/features/#pure-functional-programming","title":"'Pure' Functional Programming","text":"Functions return a value (even if that value is nil) and you can therefore use a function as an argument to another function. This is termed as first order functions.
Clojure encourages a relatively pure approach to functional programming and Clojure can be considered immutable by default
"},{"location":"introduction/concepts/features/#immutability","title":"Immutability","text":" - immutable data structures at its core, everything is immutable by default
- in imperative programming, we change state where ever we like
- in functional programming we avoid changing state as much as possible
- if a function does not change state it is referentially transparent, always returning the same result when given the same input (arguments) - often returned as a pure function
- impure functions can affect other functions and therefore has to be very mindful of the changes it makes and when it makes them
- pure functions are truly modular as they do not affect any other part of the system
"},{"location":"introduction/concepts/features/#persistent-data-structures","title":"Persistent Data Structures","text":"List, Map, Vector and Set are all built in data structures that are immutable.
If you run a function that seems to change a data structure, its actually returning a new data structure. Via a shared-memory model, new data structures are created cheaply as they share the common data elements from the original data structure and only include additional elements.
"},{"location":"introduction/concepts/features/#homoiconicity","title":"Homoiconicity","text":"One thing that keeps Clojure a small language is the fact that the same syntax is used to represent data and behaviour. For example, a function call is defined using a list, data structures and functions are defined using a list. In fact everything is a list, although we use a little syntactic sugar here and there to make the code quicker for a human to parse.
"},{"location":"introduction/concepts/features/#clojure-is-an-implementation-of-lisp","title":"Clojure is an implementation of Lisp","text":"Lisp stands for LISt Processing, so its no surprise that all Clojure code is defined in a list.
The open Parenthesis (
denotes the start of a list, the first element of that list is evaluated as a function call, everything else in the list is data.
The evaluation of the first element of a list can be behaviour of (
can be over-ridden using quote
or its short form the quote character, ', so the list elements are all treated as data.
"},{"location":"introduction/concepts/features/#runtime-polymorphism","title":"Runtime Polymorphism","text":"See Clojure arity and multi-methods for more information
"},{"location":"introduction/concepts/features/#concurrent-programming-parallelism","title":"Concurrent Programming & Parallelism","text":"Concurrent code is much safer when you data does not change state (eg. immutable values). Clojure encourages an immutable approach with its built in persistent data structures (list, Map, Vector, Set). Using Pure Functions that are not affected by or cause side effects also make writing concurrent code trivial.
Clojure helps you scale your applications by with a parallel processing approach, as you can run functions over immutable data-structures without conflict.
"},{"location":"introduction/concepts/features/#hosted-on-the-jvm","title":"Hosted on the JVM","text":"Clojure is compiled to bytecode that runs on the Java Virtual Machine. This helps Clojure run at a very high performance (close to Java, C++, etc.)
Clojure has a concise and easy to use Java Interoperability, enabling you to use any libraries that run on the JVM (Java, Groovy, Scala, Jruby, Jython, etc).
- many parts of the Clojure standard library, Clojure.core defer to the Java Standard library, for example for I/O (reading,writing files)
- Clojure makes invoking Java very convenient and provides special primitive constructs in the Clojure language to do so (new .javaMethodName javaClassName. etc)
ClojureScript generated JavaScript that will run in a browser. ClojureCLR will compile to bytecode that runs on the Microsoft .Net platform.
"},{"location":"introduction/concepts/features/#managed-state-changes","title":"Managed State Changes","text":"Using atoms
or refs
in clojure you can have mutable data. Changes are done safely within Software Transactional Memory (STM), like having an in-memory ACID database managing access
"},{"location":"introduction/concepts/features/#extend-the-language-with-macros","title":"Extend the language with Macros","text":"Clojure uses macros
Fixme Review the following content to see if its relevant ?
** Input & output with functional programming
- other fp languages like haskel & Scala use monads to encapsulate data changes whilst appearing stateless to the rest of the program - monads allow us to sneak in impure code into the context of pure code.
- Clojure doesn't try and enforce functional purity, so any function can include impure code
- most functions should be pure though or you loose the benefits of functional programming
- Clojure encourages minimal state changes / mutable state - so its up to the developer to keep the ratio of mutable data small
-
Clojure uses reference types to manage threads and mutable state. References provide synchronisation of threads without using locks (notoriously cumbersome). See STM
-
Supporting concurrency
-
atoms etc
- automatic management of state changes via Software transactional memory - like having an ACID database in memory, managing requests to change values over time.
-
by having immutable data structures - if your values do not change then its trivial to have massive parallelism.
-
A modern LISP
-
leaner syntax and not as many brackets as LISP
- clean data structure syntax at the core of the language
- LiSP was the first language to introduce first class functions, garbage collection and dynamic typing, which are common in languages used today
Macros
- a function that takes in source code and returns source code, replacing the macro code
- use macros to take out repetition / boilerplate code
- as LISP syntax is extremely simple it is much easier to write macros that work compared to non-LISP languages
Clojure emphasizes safety in its type system and approach to parallelism, making it easier to write correct multi-threaded programs.
Clojure is very concise, requiring very little code to express complex operations.
Data centric design - a well constructed data structure helps define and clarify the purpose of the code
Modularity - Clojure and its community build things in modules / components that work together (in a similar design approach to the Unix file system, for example).
It offers a REPL and dynamic type system: ideal for beginners to experiment with, and well-suited for manipulating complex data structures.
A consistently designed standard library and full-featured set of core data types rounds out the Clojure toolbox.
Clojure is close to the speed of Java
"},{"location":"introduction/concepts/features/#constraints","title":"Constraints","text":"Clojure relies on the JVM so there can be a longer boot time than a scripting language like Javascript. However, as you can connect to the Clojure runtime (the REPL) of a live system and because Clojure is dynamic, you can make changes to that live system without any downtime.
If you require more performance from Clojure, you can specify ahead of time compilation.
"},{"location":"introduction/concepts/functional-reactive-programming/","title":"Functional Reactive Programming","text":"Functional Reactive programming is used in ClojureScript with libraries including reagent, re-frame, rum, etc.
Functional Reactive Programming is an elegant means of modeling state over time in an easily composable way. Approaching the problem conceptually and developing a formal semantics first can lead to better optimization potential and simpler implementation.
Taking a functional reactive programming approach results in systems that are:
- Easier to reason about
- Simpler to implement
- Easier to optimize
Functional Reactive Programming (FRP) is a specific formalism of a model of behaviors that change in response to events, created by Conal Elliot. That's a pretty abstract statement, but FRP is abstract itself. It does not denote a particular implementation, but rather a formal semantics. It is not a style of programming or a paradigm. It's simply a formalism.
Functional Reactive Programming (and Denotational Design, also by Conal Elliott) has a lot to teach us about how to design functional programs.
"},{"location":"introduction/concepts/functional-reactive-programming/#conal-elliott-on-frp-audio-interview","title":"Conal Elliott on FRP Audio Interview","text":"If you're looking for an explanation of the Functional Reactive Programming from the man who invented it, along with an idea of the intriguing process he used to invent it, this HaskellCast episode is for you.
"},{"location":"introduction/concepts/functional-reactive-programming/#functional-reactive-animation","title":"Functional Reactive Animation","text":"A great paper from 1997 that spells out an early form of Functional Reactive Programming. It specifies behaviors (functions of time to a value) that change when events occur.
"},{"location":"introduction/concepts/functional-reactive-programming/#conal-elliots-home-page","title":"Conal Elliot's home page","text":"Conal Elliot created FRP while he was researching graphics and animation. Be sure to check out his FRP papers section.
"},{"location":"introduction/concepts/functional-reactive-programming/#push-pull-functional-reactive-programming","title":"Push-pull functional reactive programming","text":"A more advanced formulation of Functional Reactive Programming that formalizes the types and operations using Haskell's common type classes (Functor, ApplicativeFunctor, Monoid, etc). This one also includes a video of the paper presentation given at ICFP.
The main breakthrough of this paper is to model the notion of a future value for events that have not yet happened. But if they have not happened, how can future values be compared? For instance, how does one ask if event a happens before event b if neither of them has happened yet? The paper develops a cool and inspiring formalism which resolves this problem. And one is left with a value that represents the entire behavior of a system past, present, and future.
"},{"location":"introduction/concepts/functional-reactive-programming/#elm-thesis-pdf","title":"Elm Thesis - PDF","text":"Elm is a different take on FRP (and it is potentially not FRP, according to some). Instead of behaviors (functions of time to a value), Elm uses discreet signals which are transformed to other signals using functional map-like operations. It also solves a real issue with computationally expensive operations blocking the propagation of signals by making some signals asynchronous.
All-in-all, the thesis is a pleasure to read. It is very clear and a decent introduction to the myriad implementations of FRP out there. See the bibliography.
"},{"location":"introduction/concepts/misc/","title":"Misc","text":""},{"location":"introduction/concepts/misc/#characteristics","title":"Characteristics","text":" - Dynamic
- typed - like Python, Ruby or Groovy
- because its a LISP - you can redefine running code by redefining functions and re-evaluating
-
REPL - a fast way to explore your problem domain with code
-
Functional programming
- in contrast to imperative programing
- immutable data structures at its core, everything is immutable by default
- if any piece of data can be changed, that is mutable state
- in imperative programming, we change state where ever we like
- in functional programming we avoid changing state as much as possible
- if a function does not change state it is referentially transparent, always returning the same result when given the same input (arguments) - often returned as a pure function
- impure functions can affect other functions and therefore has to be very mindful of the changes it makes and when it makes them
- pure functions are truly modular as they do not affect any other part of the system ** Changing state
- rather than changing a data structure, fp instead creates a new data structure that contains the changes and copies of the existing data.
- to manage the potential overhead of copying data structures, Clojure uses Persistent collections (Lists, Vectors, Maps) which are immutable but provide an efficient way to mutate by sharing common elements (data) ** Input & output with functional programming
- other fp languages like haskel & Scala use monads to encapsulate data changes whilst appearing stateless to the rest of the program - monads allow us to sneak in impure code into the context of pure code.
- Clojure doesn't try and enforce functional purity, so any function can include impure code
- most functions should be pure though or you loose the benefits of functional programming
- Clojure encourages minimal state changes / mutable state - so its up to the developer to keep the ratio of mutable data small
-
Clojure uses reference types to manage threads and mutable state. References provide synchronisation of threads without using locks (notoriously cumbersome). See STM
-
Hosted on the Java Virtual Machine
- written for the JVM & heavily integrated, giving beautiful integration
- Clojure is compiled to Java byte code
- many parts of the Clojure standard library, Clojure.core defer to the Java Standard library, for example for I/O (reading,writing files)
-
Clojure makes invoking Java very convenient and provides special primitive constructs in the Clojure language to do so (new .javaMethodName javaClassName. etc)
-
Supporting concurrency
- atoms etc
- automatic management of state changes via Software transactional memory - like having an ACID database in memory, managing requests to change values over time.
-
by having immutable data structures - if your values do not change then its trivial to have massive parallelism.
-
A modern LISP
- leaner syntax and not as many brackets as LISP
- clean data structure syntax at the core of the language
- LiSP was the first language to introduce first class functions, garbage collection and dynamic typing, which are common in languages used today
Macros
- a function that takes in source code and returns source code, replacing the macro code
- use macros to take out repetition / boilerplate code
- as LISP syntax is extremely simple it is much easier to write macros that work compared to non-LISP languages
fixme assuming you need more, I'll add to this page, but Clojure is a very powerful language, incredibly flexible and tonnes of fun. What more do you need ?
fixme concepts to explore
Clojure emphasizes safety in its type system and approach to parallelism, making it easier to write correct multi-threaded programs.
Clojure is very concise, requiring very little code to express complex operations.
Data centric design - a well constructed data structure helps define and clarify the purpose of the code
Modularity - Clojure and its community build things in modules / components that work together (in a similar design approach to the Unix file system, for example).
It offers a REPL and dynamic type system: ideal for beginners to experiment with, and well-suited for manipulating complex data structures.
A consistently designed standard library and full-featured set of core data types rounds out the Clojure toolbox.
Clojure is close to the speed of Java
"},{"location":"introduction/concepts/misc/#constraints","title":"Constraints","text":"Clojure relies on the JVM so there can be a longer boot time than a scripting language like Javascript. However, as you can connect to the Clojure runtime (the REPL) of a live system and because Clojure is dynamic, you can make changes to that live system without any downtime.
If you require more performance from Clojure, you can specify ahead of time compilation.
"},{"location":"introduction/concepts/naming-local/","title":"Naming - local scope","text":""},{"location":"introduction/concepts/naming-local/#local-names-in-functions","title":"Local names in functions","text":"You can define names for things within the scope of a function using the let
function.
"},{"location":"introduction/concepts/naming-local/#example","title":"Example","text":"You can use the let function to define a simple expression, for which everything will go out of scope once it has been evaluated
(let [local-name \"some value\"])\n(let [minutes-in-a-day (* 60 60 24)])\n
You can also use let
inside a function to do something with the arguments passed to that function. Here we calculate the hourly-rate from a yearly salary, returning the calculated-rate.
(defn hourly-rate [yearly-salary weeks-in-year days-in-week] (let [calculated-rate (/ yearly-salary weeks-in-year days-in-week)] calculated-rate))
(hourly-rate 60000 48 5)
## Local names in data structures\n\n When defining a map you are creating a series of key value pairs. The key is essentially a name that represents the value it is paired with. Keys are often defined using a `:keyword`.\n\n```clojure\n {:radius 10, :pi 22/7 :colour purple}\n\n (def my-circle {:radius 10, :pi 22/7 :colour purple})\n
Fixme This is incorrect, as a Clojure keyword type (a name starting with :) have global scope within a namespace. If the keys were strings, then they would have the scope of just the collection.
"},{"location":"introduction/concepts/naming-things/","title":"Naming things - data structures and functions","text":"The def
function is used to name data structures in Clojure.
You can also use def
to name functions, however it is more common to use defn
(which is a macro around def) to give a function a name.
"},{"location":"introduction/concepts/naming-things/#keeping-things-private","title":"Keeping things private","text":"There is less emphasis on keeping functions and data structures private (compared to Java, C++, C#). If you want to define a function name so that it is only accessible by other functions of the same namespace, you can use the defn-
function.
There is no private equivalent for def
(as of Clojure 1.6) however you can use metadata to specify this
(def ^:private name data)
TODO: check if there is anything new around this or other common practices
"},{"location":"introduction/concepts/naming-things/#misc-writing-a-private-def-macro","title":"Misc - writing a private def macro","text":"You could write your own macro to create a private def
called def-
(defmacro def- [item value]\n `(def ^{:private true} ~item ~value)\n)\n
There are no naming conventions for a private symbol name. As its defined an used within the scope of that one namespace (file), then there is no real need to make a special convention. Private functions will just be called as normal within the namespace and it will be quite clear from the function definition that it is private.
Clojure community style guide
"},{"location":"introduction/concepts/naming-things/#example","title":"example","text":"Learning Clojure #4: private functions http://tech.puredanger.com/2010/02/09/clojure-4-private-functions/
Sometimes in a Clojure file you just want some helper functions that shouldn\u2019t be exposed outside the namespace. You can create a private function using the special defn- macro instead of defn.
For instance, create a file foo/bar.clj with a public and a private function:
(ns foo.bar) (defn- sq [x] (* x x)) (defn sum-squares [a b] (+ (sq a) (sq b)))
Then use it from the REPL:
user=> (use 'foo.bar) nil user=> (sum-squares 3 4) 25 user=> (sq 5) java.lang.Exception: Unable to resolve symbol: sq in this context (NO_SOURCE_FILE:6)
"},{"location":"introduction/concepts/purpose/","title":"When to use Clojure","text":"Clojure is a general purpose language suitable for any kind of application or service. As Clojure implementations run across multiple technology platforms and operating systems, there are very few barriers to its use.
So Clojure is great for webapps, data science, big data, finance industry (banking, trading, insurance, etc), devops tools (log analysis, etc) and anything else really.
There are areas where Clojure obviously excels.
"},{"location":"introduction/concepts/purpose/#effective-data-manipulation","title":"Effective Data Manipulation","text":"Fundamentally all software systems take in data (in the form of values or events), process or react to that data and return as a result.
The persistent data structures in Clojure (list, vector, hash-map and set) provide an efficient way to use immutable collections of data.
The clojure.core
library contains a vast number of data processing functions in Clojure so data is easily transformed
"},{"location":"introduction/concepts/purpose/#highly-scalable","title":"Highly Scalable","text":"Clojure code is encouraged to be immutable and functions to be pure, you can run millions of parallel instances of your application or service for massive processing power. These features also vastly simplify concurrent programming.
"},{"location":"introduction/concepts/purpose/#reducing-complexity","title":"Reducing Complexity","text":"Clojure encourages a component design through functional composition, breaking down problems into components
Clojure and its libraries are all great examples of well designed components and the community strongly encourages this approach.
"},{"location":"introduction/concepts/purpose/#hintfunctional-reactive-programming","title":"Hint::Functional Reactive Programming","text":"You can also use ClojureScript for Functional Reactive programming of client-side apps for browsers and mobile device.
"},{"location":"introduction/concepts/rationale/","title":"The rationale for Clojure","text":"At the time Clojure was created there were no LISP based languages that ran on a widely adopted platform, that also made concurrency easier for the developer to manage.
Developers and the companies that hire them are comfortable with the performance, security and stability of the Java Virtual Machine.
While Java developers may envy the succinctness, flexibility and productivity of dynamic languages, they have concerns about running on customer-approved infrastructure, access to their existing code base and libraries, and performance. In addition, they face ongoing problems dealing with concurrency using native threads and locking. Clojure is an effort in pragmatic dynamic language design in this context. It endeavors to be a general-purpose language suitable in those areas where Java is suitable. It reflects the reality that, for the concurrent programming future, pervasive, uncontrolled mutation simply has to go.
Clojure meets its goals by: embracing an industry-standard, open platform - the JVM; modernizing a venerable language - Lisp; fostering functional programming with immutable persistent data structures; and providing built-in concurrency support via software transactional memory and asynchronous agents. The result is robust, practical, and fast.
Clojure has a distinctive approach to state and identity.
Why Clojure?
"},{"location":"introduction/concepts/rationale/#motivating-ideas-behind-clojure","title":"Motivating ideas behind Clojure","text":"A LISP base language design is very effecitve
Standard Lisps (Common Lisp, Scheme) have not evolved over time, since standardisation. Their core data structures are mutable and not extensible and therefore no mechanisms for effectively dealing with concurrency.
Clojure is a Lisp not constrained by backwards compatibility, allowing modernisation of the language that otherwise deters developers from adoption.
Clojure extends the code-as-data paradigm to maps and vectors
All data structures default to immutability
Core data structures are extensible abstractions
Embraces a platform (JVM)
Functional programming is a good thing
- Immutable data + first-class functions
- Could always be done in Lisp, by discipline/convention
But if a data structure can be mutated, dangerous to presume it won't be In traditional Lisp, only the list data structure is structurally recursive Pure functional languages tend to strongly static types Not for everyone, or every task Clojure is a functional language with a dynamic emphasis All data structures immutable & persistent, supporting recursion Heterogeneous collections, return types Dynamic polymorphism Languages and Platforms VMs, not OSes, are the platforms of the future, providing: Type system Dynamic enforcement and safety Libraries Abstract away OSes Huge set of facilities Built-in and 3rd-party Memory and other resource management GC is platform, not language, facility Bytecode + JIT compilation Abstracts away hardware Language as platform vs. language + platform Old way - each language defines its own runtime GC, bytecode, type system, libraries etc New way (JVM, .Net) Common runtime independent of language Language built for platform vs language ported-to platform Many new languages still take 'Language as platform' approach When ported, have platform-on-platform issues Memory management, type-system, threading issues Library duplication If original language based on C, some extension libraries written in C don't come over Platforms are dictated by clients 'Must run on JVM' or .Net vs 'must run on Unix' or Windows JVM has established track record and trust level Now also open source Interop with other code required C linkage insufficient these days Java/JVM is a language and platform Not the original story, but other languages for JVM always existed, now embraced by Sun Java can be tedious, insufficiently expressive Lack of first-class functions, no type inference, etc Ability to call/consume Java is critical Clojure is the language, JVM the platform Object Orientation is overrated Born of simulation, now used for everything, even when inappropriate Encouraged by Java/C# in all situations, due to their lack of (idiomatic) support for anything else Mutable stateful objects are the new spaghetti code Hard to understand, test, reason about Concurrency disaster Inheritance is not the only way to do polymorphism \"It is better to have 100 functions operate on one data structure than to have 10 functions operate on 10 data structures.\" - Alan J. Perlis Clojure models its data structures as immutable objects represented by interfaces, and otherwise does not offer its own class system. Many functions defined on few primary data structures (seq, map, vector, set). Write Java in Java, consume and extend Java from Clojure. Polymorphism is a good thing Switch statements, structural matching etc yield brittle systems Polymorphism yields extensible, flexible systems Clojure multi-methods decouple polymorphism from OO and types Supports multiple taxonomies Dispatches via static, dynamic or external properties, metadata, etc Concurrency and the multi-core future Immutability makes much of the problem go away Share freely between threads But changing state a reality for simulations and for in-program proxies to the outside world Locking is too hard to get right over and over again Clojure's software transactional memory and agent systems do the hard part
In short, I think Clojure occupies a unique niche as a functional Lisp for the JVM with strong concurrency support. Check out some of the features.
"},{"location":"introduction/concepts/what-is-functional-programming/","title":"What is Functional Programming","text":"Functional programming can seem quite different from imperative programming used in languages like C, C++ and Java.
Imperative languages may seem easier initially, as defining one step after another is familiar approach to many things in live. As the scale of a system grows, so does complexity. Imperative languages applied object oriented design to manage complexity with varied rates of success.
When shared mutable state is common in an OO design, then a system quickly becomes complex and very difficult to reason about.
Functional programming is actually simpler that the OO approach, although initially it may be unfamiliar and not considered as easy. As systems grow in complexity, the building blocks are still simple and deterministic, creating a system that is far easier to reason about.
"},{"location":"introduction/concepts/what-is-functional-programming/#imperative-programming-languages","title":"Imperative programming languages","text":"In Imperative languages code is written that specifies a sequential of instructions that complete a task. These instructions typically modifies program state until the desired result is achieved.
Variables typically represent memory addresses that are mutable (can be changed) by default.
"},{"location":"introduction/concepts/what-is-functional-programming/#functional-programming-languages","title":"Functional programming languages","text":"Individual tasks are small and achieved by passing data to a function which returns a result.
Functions are composed together to form more complex tasks and satisfy larger business logic. These composed functions pass the result of their evaluation to the next function, until all functions in the composition have been evaluated.
The entire functional program can be thought of as a single function defined in terms of smaller ones.
Program execution is an evaluation of expressions, with the nesting structure of function composition determining program flow.
Data is immutable and cannot be change once created. Changes are expressed as new values, with complex values sharing common values for efficiency.
"},{"location":"iterate-over-data/","title":"Iterate over data","text":"Clojure data is typically within one or more of the built in collection types (vector, map, list, set).
We can use some functions in Clojure core directly on these collection types. Other clojure core functions need a little help.
"},{"location":"iterate-over-data/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"iterate-over-data/#map","title":"map","text":"Used to create a new collection by applying a given function to each element of the collection in turn.
(map inc [1 2 3])\n
If there are multiple collections, map returns a new collection with values created by calling the function with a value from each of the collections. Once map reaches the end of one collection it stops and returns the result.
(map + [1 2 3] [4 5 6] [7 8 9])\n
"},{"location":"iterate-over-data/#apply","title":"apply","text":"Used to remove all the values from a collection so they are treated as individual arguments to the function given to apply.
(= (apply + [1 2 3])\n (+ 1 2 3))\n
"},{"location":"iterate-over-data/#reduce","title":"reduce","text":"reduce can be used in a similar way as apply, to transform a collection into a different value.
reduce can also take an argument referred to as an accumulator, used to keep local state as reduce iterates through the values in the collection.
A function used with reduce is called a reducing function and is a more abstract approach to loop/recur although its possible to give your reducing function a name so is more reusable.
"},{"location":"iterate-over-data/#threading-macros","title":"threading macros","text":"Write code that reads as a sequential series of function calls, rather that the nested function calls typical in lisp.
A threading macro is often used to thread a collection through a number of function calls and expressions.
"},{"location":"iterate-over-data/#comp","title":"comp","text":"Compose functions together that work over a collection. It can be seen as a more abstract approach to a threading macro or nested function calls.
"},{"location":"iterate-over-data/#transduce","title":"transduce","text":"Used like comp to create a pipeline of function calls, however, each function call or expression must return a transducer (transforming reduction). Many clojure.core
functions return a transducer if you do not provide the collection argument.
"},{"location":"iterate-over-data/apply/","title":"apply","text":""},{"location":"iterate-over-data/apply/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"iterate-over-data/comp/","title":"comp - composing functions","text":""},{"location":"iterate-over-data/comp/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"iterate-over-data/filter-remove/","title":"filter and remove","text":"Use filter and remove with predicate functions, those returning true or false, to create a sub-set of the data.
filter creates a new collection that contains all the matching values from the predicate function (true).
remove
creates a new collection with contains all the values that didn't match the predicate function (false).
(filter odd? [1 2 3 4 5 6 7 8 9])\n
"},{"location":"iterate-over-data/filter-remove/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"iterate-over-data/map-fn/","title":"map with fn - anonymous function","text":"(map (fn [arg] (+ arg 5)) [1 2 3 4 5])\n
There is a syntactic short-cut for the anonymous function that does not require a name for the arguments
#(+ %1 5)
Adding this into our previous expression we can see that its still quite readable and helps keep the code clean.
(map #(+ arg 5) [1 2 3 4 5])\n
"},{"location":"iterate-over-data/map-fn/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"iterate-over-data/map-fn/#hintanonymous-function-name","title":"Hint::Anonymous function name","text":"Anonymous functions do not have an externally referable name, so must be used in-line with an expression.
The fn
function can be defined with a name, however, this is only available in the scope of that function definition, the name cannot be used to refer to that function outside of its definition.
Including a name within a fn
definition enables the function to call itself, therefore creating an anonymous recursive function.
"},{"location":"iterate-over-data/map-partial/","title":"map with partial","text":""},{"location":"iterate-over-data/map-partial/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"iterate-over-data/map/","title":"map function","text":""},{"location":"iterate-over-data/map/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"Add examples of using the map function with data
"},{"location":"iterate-over-data/reduce/","title":"reduce","text":""},{"location":"iterate-over-data/reduce/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"iterate-over-data/transduce/","title":"transduce and transforming reducers","text":""},{"location":"iterate-over-data/transduce/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"libraries/","title":"Libraries","text":""},{"location":"libraries/clojars/","title":"Clojars","text":""},{"location":"libraries/clojure-core-lisp-comprehension/","title":"Libraries: clojure.core
lisp comprehension","text":""},{"location":"libraries/clojure-core-lisp-comprehension/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"discuss functions for list comprehension for
"},{"location":"libraries/clojure-core/","title":"Understanding Clojure Libraries: clojure.core
","text":""},{"location":"libraries/clojure-core/#warningvery-rough-draft-of-an-idea","title":"Warning::Very rough draft of an idea","text":"Experimenting with the value of grouping functions in clojure.core to help ensure you are exposed to most of the concepts in Clojure
"},{"location":"libraries/clojure-core/#families-of-functions","title":"Families of functions","text":" - List Comprehension (for, while, )
-
used to process multiple collections
-
Transformations (map, filter, apply, reduce )
- transform the contents of a collection
"},{"location":"libraries/edn/","title":"edn","text":""},{"location":"libraries/om/","title":"om","text":""},{"location":"modifying-data-structures/","title":"Modifying data structures","text":"Wait, I thought you said that data structures were immutable! So how can we change them then?
Yes, lists, vectors, maps and sets are all immutable. However, you can get a new data structure that has the changes you want. To make this approach efficient, the new data structure contains only the new data and links back to the existing data structure for shared data elements.
We will see some of the most common functions that work with data structures in this section. In actuality, everything can be considered a function that works on a data structure though, as that is the language design of clojure.
"},{"location":"modifying-data-structures/lists/","title":"Lists","text":"You can change lists with the cons
function, see (doc cons)
for details
(cons 5 '(1 2 3 4))
You will see that cons
does not change the existing list, it create a new list that contains the number 5 and a link to all the elements of the existing list.
You can also use cons on vectors (cons 5 [1 2 3 4])
(cons \"fish\" '(\"and\" \"chips\"))\n\n(conj '(1 2 3 4) 5)\n\n(conj [1 2 3 4] 5)\n\n\n;; Lets define a simple list and give it a name\n(def list-one '(1 2 3))\n\n;; the name evaluates to what we expect\nlist-one\n\n;; If we add the number 4 using the cons function, then we\n;; get a new list in return, with 4 added to the front (because thats how lists work with cons)\n(cons 4 list-one)\n\n;; If we want to keep the result of adding to the list, we can assign it a different name\n(def list-two (cons 4 list-one))\n;; and we get the result we want\nlist-two\n\n;; we can also pass the original name we used for the list to the new list\n(def list-one (cons 4 list-one))\n\n;; If we re-evaluate the definition above, then each time we will get an extra\n;; number 4 added to the list.\n\nlist-one\n\n;; Again, this is not changing the original list, we have just moved the name\n;; of the list to point to the new list.\n;; Any other function working with this data structure before reassigning the name\n;; will not be affected by the re-assignment and will use the unchanged list.\n
"},{"location":"modifying-data-structures/maps/","title":"Maps","text":"The conj
function works on all of the Clojure collections. The map collection also has functions that affect the evaluation of a map and the value of map returned.
"},{"location":"modifying-data-structures/maps/#adding-new-values-with-conj","title":"Adding new values with conj
","text":"If you have a collection of maps, you can add another map to that collection with the conj
function.
(conj [{:map1 1}] {:map2 2})\n\n;; => [{:map1 1} {:map2 2}]\n
"},{"location":"modifying-data-structures/maps/#changing-values-with-assoc","title":"Changing values with assoc
","text":"The assoc
function is used to update a value in a map without necessary being concerned about the current value. assoc
returns a complete new map with the specified value.
(assoc {:food \"Fish\"} :food \"Fish&Chips\")\n\n;; => {:food \"Fish&Chips\"}\n
It does not matter how many keys are in the map, as keys are unique, then assoc
will look up the specific key and change its value to that specified in the third argument.
If a key is not in the map, assoc
will add both the key and the value.
(def alphabet-soup {:a 1 :b 2 :c 3})\n\n(assoc alphabet-soup :d 4)\n\n;; => {:a 1, :b 2, :c 3, :d 4}\n
If there are multiple levels to the structure of your map, ie. the value of a key in the map is also a map
For example, the value of :luke
in the star-wars-characters
map is represented as a map too {:fullname \"Luke Skywalker\" :skill \"Targeting Swamp Rats\"}
(def star-wars-characters\n {:luke {:fullname \"Luke Skywalker\" :skill \"Targeting Swamp Rats\"}\n :vader {:fullname \"Darth Vader\" :skill \"Crank phone calls\"}\n :jarjar {:fullname \"JarJar Binks\" :skill \"Upsetting a generation of fans\"}})\n
To update the skill of one of the characters we can use assoc-in
to update the correct value by traversing the map via the given keys.
(assoc-in star-wars-characters [:vader :skill] \"The Dark Side of the Force\")\n\n;; => {:luke {:fullname \"Luke Skywalker\", :skill \"Targeting Swamp Rats\"},\n :vader {:fullname \"Darth Vader\", :skill \"The Dark Side of the Force\"},\n :jarjar {:fullname \"JarJar Binks\", :skill \"Upsetting a generation of fans\"}}\n
"},{"location":"modifying-data-structures/maps/#update-values-in-a-map-with-update","title":"Update values in a map with update
","text":"Rather than replace the current value with one specified, update
applies a function to the existing value in order to update that value.
(def alphabet-soup {:a 1 :b 2 :c 3})\n\n(update alphabet-soup :a inc)\n\n;; => {:a 2, :b 2, :c 3}\n
Hint As with assoc
you can also use update
on nested maps using the update-in
function.
"},{"location":"modifying-data-structures/sets/","title":"Sets","text":""},{"location":"modifying-data-structures/vectors/","title":"Vectors","text":""},{"location":"performance/","title":"Clojure Performance and benchmarks","text":"There are several aspects to performance testing
- time taken by individual functions / expressions
- time through a specific path in your application
- response times under different loads
The purpose of performance testing and bench-marking is to understand the expected behaviour of your application under various usage patterns. This kind of testing can also suggest areas of the application that might benefit from optimisation
"},{"location":"performance/#performance-tools-for-clojure","title":"Performance tools for Clojure","text":" - Criterium - benchmarks for Clojure expressions
- Gatling
"},{"location":"performance/#gatling","title":"Gatling","text":"The Gatling Project is a free and open source performance testing tool. Gatling has a basic GUI that's limited to test recorder only. However, the tests can be developed in easily readable/writable domain-specific language (DSL).
Key Features of Gatling:
- HTTP Recorder
- An expressive self-explanatory DSL for test development
- Scala-based
- Production of higher load using an asynchronous non-blocking approach
- Full support of HTTP(S) protocols and can also be used for JDBC and JMS load testing
- Multiple input sources for data-driven tests
- Powerful and flexible validation and assertions system
- Comprehensive informative load reports
"},{"location":"performance/#reference-other-performance-tools","title":"Reference: Other performance tools","text":"Other notable performance tools include:
- The Grinder
- Apache JMeter (Java desktop app)
- Tsung (required Erlang)
- Locust (python)
Key Features of The Grinder:
- TCP proxy to record network activity into the Grinder test script
- Distributed testing that scales with an the increasing number of agent instances
- Power of Python or Closure, combined with any Java API, for test script creation or modification
- Flexible parameterization, which includes creating test data on the fly and the ability to use external data sources like files and databases
- Post-processing and assertion with full access to test results for correlation and content verification
- Support of multiple protocols
Key Features of JMeter:
- Desktop GUI tool
- Cross-platform. JMeter can run on any operating system with Java
- Scalable. When you need a higher load than a single machine can create, JMeter can execute in a distributed mode, meaning one master JMeter machine controls a number of remote hosts.
- Multi-protocol support. The following protocols are all supported out-of-the-box: HTTP, SMTP, POP3, LDAP, JDBC, FTP, JMS, SOAP, TCP
- Multiple implementations of pre- and post-processors around sampler. This provides advanced setup, teardo* wn parametrization, and correlation capabilities
- Various assertions to define criteria
- Multiple built-in and external listeners to visualize and analyze performance test results
- Integration with major build and continuous integration systems, making JMeter performance tests part of the full software development life cycle
- Extensions via plugins
"},{"location":"performance/#resources","title":"Resources","text":" - Open source load testing tools - which one should you use
"},{"location":"performance/load-testing/","title":"Load Testing","text":"The Gatling project can be used to create and run load tests using Clojure (and get fancy reports).
Add the following to your project.clj :dependencies
[clj-gatling \"0.11.0\"]\n
"},{"location":"performance/load-testing/#describe-a-load-test","title":"Describe a Load Test","text":"This is how we would write a simple load test which performs 50 GET requests against a server running at test.com:
class SimpleSimulation extends Simulation {\n //declare a scenario with a simple get request performed 5 times\n val scn = scenario(\"myScenario\")\n .exec(http(\"myRequest\").get(\"http://test.com/page.html\"))\n .repeat(5)\n\n //run the scenario with 10 concurrent users\n setUp(scn.users(10))\n}\n
Gatling refers to load tests as Simulations which have one or more Scenarios. In the one above we are saying we will have 10 users execute 5 requests each in parallel. We could provide a Content-Type header with the request and check for a 200 response code like this:
http(\"myRequest\")\n .get(\"http://test.com/page.html\")\n .header(\"Content-Type\", \"text/html\")\n .check(status.is(200))\nIf we wanted to do a POST request with a JSON body and basic authentication, as well as verify something in the response:\n\nhttp(\"myRequest\")\n .post(\"http://test.com/someresource\"))\n .body(StringBody(\"\"\"{ \"myContent\": \"myValue\" }\"\"\"))\n .asJSON\n .basicAuth(\"username\", \"password\")\n .check(jsonPath(\"$..someField\").is(\"some value\"))\n
The expression used to extract someField from the response is passed to jsonPath() and is based on Goessner\u2019s JsonPath syntax. We use is() to verify the expected value is equal to some value. We can also do other forms of verification on the response json like:
- not(expectedValue): not equal to expectedValue
- in(sequence): to check that a value belongs to the given sequence
- exists(), notExists(): to check for the presence/absence of a field
For a multipart request with 2 parts and gzip compression:
http(\"myRequest\")\n .post(\"http://test.com/someresource\"))\n .bodyPart(StringBodyPart(\"\"\"{ \"myContent\": \"myValue\" }\"\"\"))\n .bodyPart(RawFileBodyPart(\"file\", \"test.txt\")\n .processRequestBody(gzipBody)\nWe can also create scenarios with multiple requests and use the result from previous requests in subsequent requests like this:\n\nscenario(\"myScenario\")\n .exec(http(\"request1\")\n .post(\"http://test.com/resource1\")\n .body(StringBody\"\"\"{ \"myContent\": \"\"}\"\"\")\n .check(jsonPath(\"$..myResponse.guid\").saveAs(\"guid\")))\n .exec(http(\"request2\")\n .put(\"http://test.com/resource2/${guid}\")\n .body(StringBody\"\"\"{ \"someOtherField\": \"\"}\"\"\"))\n
guid is extracted from the response of the first call using saveAs(\"guid\") and used in the path to the PUT call.
Scenarios can also be run with a ramp up. If we wanted to run the scenario above with 1000 users with a ramp up of 20 seconds we would do:
setUp(scn.users(1000).ramp(20))\n
"},{"location":"performance/load-testing/#run-a-simulation","title":"Run a Simulation","text":"There are a number of ways to run Gatling simulations. You can download the bundle, place your simulations under the user-files/simulations directory and then run bin/gatling.sh.
If you prefer integration with your build system there are plugins for Maven, Gradle and SBT. For example, for Maven we just add the dependencies in the pom.xml:
<dependencies>\n <dependency>\n <groupId>io.gatling.highcharts</groupId>\n <artifactId>gatling-charts-highcharts</artifactId>\n <scope>test</scope>\n </dependency>\n</dependencies>\n\n<build>\n <plugins>\n <plugin>\n <groupId>io.gatling</groupId>\n <artifactId>gatling-maven-plugin</artifactId>\n </plugin>\n </plugins>\n</build>\n
Place simulations under src/test/scala/com/company/service and then in the terminal:
mvn gatling:execute -Dgatling.simulationClass=com.company.service.YourSimulation\n
"},{"location":"performance/testing-functions/","title":"Testing Functions","text":""},{"location":"performance/testing-functions/#adding-the-criterium-library","title":"Adding the Criterium library","text":"Add [criterium \"0.4.4\"]
to you project.clj
file.
Add criterium to the namespace were you run your tests
(ns ,,,\n :require [criterium.core] :refer [quick-bench])\n
Or simply require criterium in the REPL
(require '[criterium.core] :refer [quick-bench])\n
"},{"location":"performance/testing-functions/#using-criterium-to-test-code","title":"Using Criterium to test code","text":"Lets try a few similar Clojure functions to see the Criterium benchmark in action
(let [number 5]\n (quick-bench (cond = 5 1 1 2 2 3 3 4 4 5 5)))\n
Benchmark output is sent to the REPL
Evaluation count : 50788488 in 6 samples of 8464748 calls.\n Execution time mean : 2.535916 ns\n Execution time std-deviation : 0.096838 ns\n Execution time lower quantile : 2.435814 ns ( 2.5%)\n Execution time upper quantile : 2.686146 ns (97.5%)\n Overhead used : 9.431514 ns\n\nFound 1 outliers in 6 samples (16.6667 %)\n low-severe 1 (16.6667 %)\n Variance from outliers : 13.8889 % Variance is moderately inflated by outliers\n
Running the benchmark again for the same expression, we get pretty consistent results
Evaluation count : 50408712 in 6 samples of 8401452 calls.\n Execution time mean : 2.571379 ns\n Execution time std-deviation : 0.163071 ns\n Execution time lower quantile : 2.366952 ns ( 2.5%)\n Execution time upper quantile : 2.721099 ns (97.5%)\n Overhead used : 9.431514 ns\n
There is a parallized version of cond
called condp
.
(let [number 5]\n (quick-bench (condp = 5 1 1 2 2 3 3 4 4 5 5)))\n
Evaluation count : 3625284 in 6 samples of 604214 calls.\n Execution time mean : 156.813816 ns\n Execution time std-deviation : 2.560629 ns\n Execution time lower quantile : 154.222522 ns ( 2.5%)\n Execution time upper quantile : 160.425030 ns (97.5%)\n Overhead used : 9.431514 ns\n\nFound 1 outliers in 6 samples (16.6667 %)\n low-severe 1 (16.6667 %)\n Variance from outliers : 13.8889 % Variance is moderately inflated by outliers\n
That figure is quite high, lets run that again.
Evaluation count : 3707922 in 6 samples of 617987 calls.\n Execution time mean : 154.219102 ns\n Execution time std-deviation : 3.427811 ns\n Execution time lower quantile : 149.777377 ns ( 2.5%)\n Execution time upper quantile : 159.225180 ns (97.5%)\n Overhead used : 9.431514 ns\n
So using a parallized version of a function adds a significant exectution time. I believe the extra time is due to setting up a thread. If so, then when using condp
you only get a more effective throughput when running multiple parallel threads, which should be fairly obvious.
Now lets benchmark a similar function called case
. This function is nicely optimised on the JVM especially when the values are sequential, so we should see faster results
(let [number 5]\n (quick-bench (case 5 1 1 2 2 3 3 4 4 5 5)))\n
Benchmark output is sent to the REPL
Evaluation count : 56533626 in 6 samples of 9422271 calls.\n Execution time mean : 1.158650 ns\n Execution time std-deviation : 0.187322 ns\n Execution time lower quantile : 1.021431 ns ( 2.5%)\n Execution time upper quantile : 1.471115 ns (97.5%)\n Overhead used : 9.431514 ns\n\nFound 1 outliers in 6 samples (16.6667 %)\n low-severe 1 (16.6667 %)\n Variance from outliers : 47.5092 % Variance is moderately inflated by outliers\n
"},{"location":"puzzles/","title":"Puzzles","text":"Simple puzzles to help you start thinking functionally
"},{"location":"puzzles/random-seat-assignment/","title":"Random Seat assignment","text":"https://github.com/practicalli/clojure-practicalli-content/issues/4
Take a functional / data oriented approach to solving this problem
"},{"location":"puzzles/random-seat-assignment/#description","title":"Description","text":"You want to randomly assign seating to a number of people for a fixed number of seats. Each seat is represented by an integer number between 1 and 30.
How do you randomly assign seats without choosing the same seat twice.
"},{"location":"puzzles/random-seat-assignment/#loop-recur-approach","title":"Loop / recur approach","text":"Bad...
"},{"location":"puzzles/random-seat-assignment/#recursive-function","title":"recursive function","text":""},{"location":"quickstart/quick-reference/","title":"Clojure Quick Reference","text":"The basic Clojure syntax and a few common functions you should probably learn first.
Also see the Clojure.org cheat-sheet
"},{"location":"quickstart/quick-reference/#calling-functions","title":"Calling functions","text":"The first element in a list, ()
, is treated as a call to a function. This is known as prefix notation which greatly simplifies Clojure syntax and makes mathematical expressions completely deterministic, eliminating the need for operator precedence.
(+ 2 3 5 8 13 (* 3 7))\n(+ 3 (* 2 (- 7 2) 4) (/ 16 4))\n(clojure-version)\n
Functions contain doc-strings and you can ask for a functions documentation, or show the source code.
(doc doc)\n(source doc)\n
Clojure is a dynamically typed language, it is also strongly typed (everything is a type, but you dont have to express the type in your code). The type of anything in Clojure can be returned.
(type 42)\n(type {:hash \"data\" :map \"more data\"})\n
"},{"location":"quickstart/quick-reference/#modeling-data-with-collection-types","title":"Modeling data with Collection types","text":"Clojure has 4 main collection types, all immutable (cannot change once created) and can contain any Clojure types.
(str \"lists used mainly\" (* 2 2) :code)\n\n[0 \"indexed array\"]\n\n{:key \"value\" :pairs \"hash-map\" :aka \"dictionary\"}\n\n#{1 2 3 4 \"unique\" \"set\" \"of\" \"values\" \"unordered\" (* 3 9)}\n
"},{"location":"quickstart/quick-reference/#defining-names-for-values-vars","title":"Defining names for values (vars)","text":"Names can be bound to any values, simple values like numbers, collections or functions. A convenient way to refer to value in your code.
(def public-health-data\n ({:date \"2020-01-01\" :confirmed-cases 23014 :recovery-percent 15}\n {:date \"2020-01-02\" :confirmed-cases 23014 :recovery-percent 15}\n {:date \"2020-01-03\" :confirmed-cases 23014 :recovery-percent 15}))\n\n(def add-hundred (partial + 100))\n
"},{"location":"quickstart/quick-reference/#map-reduce-filter","title":"map reduce filter","text":"Common functions for iterating through a collection / sequence of values
(map * [1 3 5 8 13 21] [3 5 8 13 21 34])\n\n(filter even? [1 3 5 8 13 21 34])\n\n(reduce + [31 28 30 31 30 31])\n
"},{"location":"quickstart/quick-reference/#using-data-structures","title":"Using data structures","text":"Using the map
and inc
function, increment all the numbers in a vector
(map inc [1 2 3 4 5])\n
The above map
function is roughly equivalent to the following expression
(conj [] (inc 1) (inc 2) (inc 3) (inc 4) (inc 5))\n
The conj
function creates a new collection by combining a collection and one or more values.
"},{"location":"quickstart/quick-reference/#defining-custom-functions","title":"Defining custom functions","text":"(defn square-of\n \"Calculates the square of a given number\"\n [number]\n (* number number))\n
Function definitions can also be used within other expressions, useful for mapping custom functions over a collection
(fn [x] (* x x))\n\n(map (fn [x] (* x x)) [1 2 3 4 5])\n
"},{"location":"quickstart/quick-reference/#ratio-type","title":"Ratio Type","text":"; Using the division function (/ ) shows another interesting characteristic of Clojure, the fact that it is lazy. This is not lazy in a bad way, but lazy evaluation of data structures. This actually helps to make clojure more efficient at dealing with data, especially very large data sets.
(/ 22 7)\n22/7\n\n(/ 22 7.0)\n3.142857142857143\n\n(type (/ 22 7))\n
Using a Ratio means that the mathematical division is not evaluated when using whole numbers (Integers) that would produce a decimal number. If you do return a decimal number then what precision of decimal are you expecting. By specifying one or more of the numbers as a decimal value you are giving Clojure a precision to infer and can therefore provide a specific decimal result.
"},{"location":"quickstart/quick-reference/#java-interoperability","title":"Java interoperability","text":".
and new
are Clojure functions that create a Java object. This allows you to use values from Java constants, i.e. PI is a static double from the java.lang.Math object
(. Math PI)\n3.141592653589793\n
Also call static and instance methods from Java objects.
(Math/cos 3)\n\n(javax.swing.JOptionPane/showMessageDialog nil\n \"Hello Java Developers\")\n
"},{"location":"quickstart/quick-reference/#recursion","title":"Recursion","text":"Recursive function
(defn recursive-counter\n [value]\n (if (< value 1000)\n (recur (+ value 25))))\n\n(recursive-counter 100)\n
"},{"location":"reference/","title":"Reference","text":""},{"location":"reference/basic-syntax/","title":"Reference: Basic Syntax","text":""},{"location":"reference/basic-syntax/#notes-from-aphyr","title":"Notes from Aphyr","text":"Let\u2019s write a simple program. The simplest, in fact. Type \u201cnil\u201d, and hit enter.
user=> nil nil nil is the most basic value in Clojure. It represents emptiness, nothing-doing, not-a-thing. The absence of information.
user=> true true user=> false false true and false are a pair of special values called Booleans. They mean exactly what you think: whether a statement is true or false. true, false, and nil form the three poles of the Lisp logical system.
user=> 0 0 This is the number zero. Its numeric friends are 1, -47, 1.2e-4, 1/3, and so on. We might also talk about strings, which are chunks of text surrounded by double quotes:
user=> \"hi there!\" \"hi there!\" nil, true, 0, and \"hi there!\" are all different types of values; the nouns of programming. Just as one could say \u201cHouse.\u201d in English, we can write a program like \"hello, world\" and it evaluates to itself: the string \"hello world\". But most sentences aren\u2019t just about stating the existence of a thing; they involve action. We need verbs.
user=> inc
"},{"location":"reference/basic-syntax/#_1","title":"This is a verb called inc\u2013short for \u201cincrement\u201d. Specifically, inc is a symbol which points to a verb: #\u2013 just like the word \u201crun\u201d is a name for the concept of running.
There\u2019s a key distinction here\u2013that a signifier, a reference, a label, is not the same as the signified, the referent, the concept itself. If you write the word \u201crun\u201d on paper, the ink means nothing by itself. It\u2019s just a symbol. But in the mind of a reader, that symbol takes on meaning; the idea of running.
Unlike the number 0, or the string \u201chi\u201d, symbols are references to other values. when Clojure evaluates a symbol, it looks up that symbol\u2019s meaning. Look up inc, and you get #.
Can we refer to the symbol itself, without looking up its meaning?
user=> 'inc inc Yes. The single quote ' escapes a sentence. In programming languages, we call sentences expressions or statements. A quote says \u201cRather than evaluating this expression\u2019s text, simply return the text itself, unchanged.\u201d Quote a symbol, get a symbol. Quote a number, get a number. Quote anything, and get it back exactly as it came in.
user=> '123 123 user=> '\"foo\" \"foo\" user=> '(1 2 3) (1 2 3) A new kind of value, surrounded by parentheses: the list. LISP originally stood for LISt Processing, and lists are still at the core of the language. In fact, they form the most basic way to compose expressions, or sentences. A list is a single expression which has multiple parts. For instance, this list contains three elements: the numbers 1, 2, and 3. Lists can contain anything: numbers, strings, even other lists:
user=> '(nil \"hi\") (nil \"hi\") A list containing two elements: the number 1, and a second list. That list contains two elements: the number 2, and another list. That list contains two elements: 3, and an empty list.
user=> '(1 (2 (3 ()))) (1 (2 (3 ()))) You could think of this structure as a tree\u2013which is a provocative idea, because languages are like trees too: sentences are comprised of clauses, which can be nested, and each clause may have subjects modified by adjectives, and verbs modified by adverbs, and so on. \u201cLindsay, my best friend, took the dog which we found together at the pound on fourth street, for a walk with her mother Michelle.\u201d
Took Lindsay my best friend the dog which we found together at the pound on fourth street for a walk with her mother Michelle But let\u2019s try something simpler. Something we know how to talk about. \u201cIncrement the number zero.\u201d As a tree:
Increment the number zero We have a symbol for incrementing, and we know how to write the number zero. Let\u2019s combine them in a list:
clj=> '(inc 0) (inc 0) A basic sentence. Remember, since it\u2019s quoted, we\u2019re talking about the tree, the text, the expression, by itself. Absent interpretation. If we remove the single-quote, Clojure will interpret the expression:
user=> (inc 0) 1 Incrementing zero yields one. And if we wanted to increment that value?
Increment increment the number zero user=> (inc (inc 0)) 2 A sentence in Lisp is a list. It starts with a verb, and is followed by zero or more objects for that verb to act on. Each part of the list can itself be another list, in which case that nested list is evaluated first, just like a nested clause in a sentence. When we type
(inc (inc 0)) Clojure first looks up the meanings for the symbols in the code:
(# (# 0)) Then evaluates the innermost list (inc 0), which becomes the number 1:
(# 1) Finally, it evaluates the outer list, incrementing the number 1:
2 Every list starts with a verb. Parts of a list are evaluated from left to right. Innermost lists are evaluated before outer lists.
(+ 1 (- 5 2) (+ 3 4)) (+ 1 3 (+ 3 4)) (+ 1 3 7) 11 That\u2019s it.
The entire grammar of Lisp: the structure for every expression in the language. We transform expressions by substituting meanings for symbols, and obtain some result. This is the core of the Lambda Calculus, and it is the theoretical basis for almost all computer languages. Ruby, Javascript, C, Haskell; all languages express the text of their programs in different ways, but internally all construct a tree of expressions. Lisp simply makes it explicit.
","text":""},{"location":"reference/books/","title":"Books & Tutorials on Clojure","text":"Here is a list of Clojure books, grouped by skill level. There is also a book list on Clojure.org or via a search for Clojure on o'Reilly lists most of these books.
"},{"location":"reference/books/#for-clojure-beginners","title":"For Clojure beginners","text":" - Living Clojure, O'Reilly April 2015
- Clojure for the Brave and the True, NoStartch Press September 2015
- Clojurescript Unraveled - June 2016
- Clojure from the ground up
- Practicalli Clojure
- Practicalli Clojure WebApps
"},{"location":"reference/books/#reference","title":"Reference","text":" - Clojure Programming Cookbook, Packt August 2016
- Clojure Cookbook, O'Reilly March 2014 - hundreds of real-world problems and solutions, ranging from basic utilities to rich web services to heavy data processing
"},{"location":"reference/books/#intermediate-level","title":"Intermediate level","text":" - Web Development with Clojure, Pragmatic July 2016
- Mastering Clojure, Packt March 2016
- Clojure in Action, Manning December 2015
- Programming Clojure, Pragmatic October 2015
- Clojure Applied: From Practice to Practitioner, Pragmatic September 2015
- Joy of Clojure, Manning May 2014
- Clojure Programming, O'Reilly March 2012
"},{"location":"reference/books/#other-books","title":"Other books","text":" - Learning Clojurescript, Packt June 2016
- Professional Clojure, Wiley/Wrox May 2016
- Clojure for Java Developers, Packt February 2016
- Clojure for Finance, January 2016
- Clojure Recipes, Addison-Wesley October 2015
- Clojure for Data Science, Packt September 2015
- Clojure High Performance Programming, Packt September 2015
- Clojure Data Structures & Algorithms, Packt August 2015
- Mastering Clojure Data Analysis, Packt
- Clojure Reactive Programming, Packt March 2015
- Clojure Web Development Essentials, Packt February 2015
- Clojure Data Analysis Cookbook, Packt January 2015
- Mastering Clojure Macros, Pragmatic August 2014
- Clojure for Machine Learning, Packt April 2014
- Clojure for Domain-specific Languages, Packt December 2013
- Clojurescript: Up and Running, O'Reilly October 2012
- Building Web Applications with Clojure, Packt April 2012
- Functional Programming Patterns in Scala and Clojure, Pragmatic August 2014
"},{"location":"reference/changing-state/","title":"Changing State","text":""},{"location":"reference/code-analysis/","title":"Code Analysis","text":"clj-kondo is a lint tool that highlights syntactic errors and suggests idioms for Clojure, ClojureScript and EDN.
Use clj-kondo with your preferred editor to warning about errors as you type so issues can be fixed as soon as they occur, enhancing your joy of Clojure.
clj-kondo can also be used as a command line tool for checking projects in development environments and continuous integration service, such as the setup-clojure GitHub action.
Clojure LSP includes clj-kondo
Clojure LSP install includes clj-kondo, removing the need for a separate install of clj-kondo
"},{"location":"reference/code-analysis/#install","title":"Install","text":"Follow the clj-kondo install guide for your operating system.
Clj-kondo config contains additional configuration for using clj-kondo with libraries that extend the Clojure language via macros.
SpacemacsDoom EmacsNeovim clj-kondo can be used if cider
is configured as the clojure layer backend. If LSP is configured as the backend, should not be used as it may duplicate analysis results (e.g. doubling error and warning messgeas).
Use Clojure LSP with Doom rather than clj-kondo by itself.
Add the +lsp
feature to the Clojure module and enable the lsp
module .config/doom/init.el
(clojure +lsp)\nlsp\n
Add the respective LSP server implementation to the operating system Practicalli Neovim provides a guide to configure Neovim with Treesitter as an LSP client, as well as a fennel based configuration for Neovim.
"},{"location":"reference/code-analysis/#command-line-analysis","title":"Command Line analysis","text":"Run clj-kondo
with the --lint option and specify a file or path
To analyse a specific file
clj-kondon --lint ~/.config/deps.edn\n
Analyse a project, running the clj-kondo command from the root of the project
clj-kondon --lint .\n
"},{"location":"reference/code-analysis/#clj-kondo-with-github-actions","title":"clj-kondo with GitHub actions","text":"Add clj-kondo linting to continuous integration workflow.
"},{"location":"reference/control-flow/","title":"Control Flow","text":""},{"location":"reference/core-async/","title":"Core.async","text":""},{"location":"reference/doc-and-source-functions/","title":"Doc and source functions","text":""},{"location":"reference/doc-and-source-functions/#the-doc-source-functions","title":"The doc & source functions","text":"If you are not using a Clojure aware editor or spend a lot of time in the REPL you can also view the documentation of a function by calling the doc
function and see the source by calling the source
function.
To use the doc
& source
functions in the REPL you should be in the user
namespace.
Note On the command line, start a REPL with the command lein repl
and then view the documentation for three common functions used in clojure
Make sure you are in the user
namespace before calling the doc
function. If you are in another namespace, either change back using (ns 'user)
or see the next section on using these functions in another namespace.
(doc doc)\n(doc map)\n(doc filter)\n(doc cons)\n\n(source doc)\n(source map)\n
Here is the doc string for doc
Here is the source code for the source
function
Hint As the documentation for a function is part of its definition, by looking at the source of a function you also get the documentation.
"},{"location":"reference/doc-and-source-functions/#using-doc-source-function-from-another-namespace","title":"Using doc & source function from another namespace","text":"The doc
and source
functions are only included in the user
namespace. If you switch to another namespace or your editor places you in the current namespace of your project, these functions will not be available unless you including core.repl
in the current namespace.
From the REPL, evaluate the expression:
(use 'clojure.repl)\n
You could also require
the clojure.repl
library in your own code, however if you have a good editor it should provide these features without including this library. Therefore the following code is shown only as an example and not a recommended approach.
(ns foobar\n(:require [clojure.repl :refer :all]))\n
"},{"location":"reference/kebab-case/","title":"Clojure names use kebab-case","text":"kebab-case is a clean, human-readable way to combine the words that would otherwise have spaces.
Cloure uses kebab-case to combines words with a dash, -
, rather than a space. e.g. rock-paper-scissors
, tic-tac-toe
or (def db-spec-development {:db-type \"h2\" :db-name \"banking-on-clojure\"})
kebab-case is used throughout Clojure, including
- Var names with
def
and function names with defn
- Local names with
let
- Clojure spec names
kebab-case is used in lisp languages including Clojure. The style is also used in website URLs, e.g. practicalli.github.io/clojure-webapps
"},{"location":"reference/kebab-case/#using-meaningful-names","title":"Using meaningful names","text":"To provide greater clarity to human developers, words may be combined for the names used when writing the code. Using multiple words can give greater context in to the purpose of that code.
Using a combination of meaningful names makes understanding and debugging code far easier.
"},{"location":"reference/kebab-case/#spaces-characters-have-special-meaning","title":"Spaces characters have special meaning","text":"Programming languages remove spaces between words because the space character is used as a separator when parsing the code.
If spaces were not used as a separator for the some other character would be required, adding complexity to the language syntax.
"},{"location":"reference/kebab-case/#other-styles","title":"Other Styles","text":" - camelCase - used in Java and C-style programming languages
- PascalCase - used in the Pascal programming language
- snake_case - used for
ENVIRONMENT_VARIABLES
and database_table_names_and_columns
"},{"location":"reference/naming-conventions/","title":"Naming Conventions","text":""},{"location":"reference/naming-conventions/#kebab-case","title":"Kebab-case","text":"Kebab-case is the naming convention for all Clojure function names than contain more than one word. Its name comes from the Shish Kebab style of cooking, where the words are the tofu and vegetables and the dashes are the skewers.
clj-time\nstring-parser\ndisplay-name\n
"},{"location":"reference/naming-conventions/#predicates","title":"Predicates","text":"Examples of predicate naming conventions from clojure.core
contains?\nempty?\nevery?\nnot-empty?\nnull?\n
"},{"location":"reference/naming-conventions/#namespace-requires-and-aliases","title":"Namespace requires and aliases","text":"Required libraries should be given a contextually meaningful name as an alias, helping to identify the purpose of functions defined outside of the namespace.
Giving meaningful context helps code to be understood by any person reading the code. It is also easier to search for usage of functions from that context in the current project.
Aliases are rarely typed more than once in full as Clojure editors have auto-complete, so there is no benefit to short of single character aliases.
(ns status-monitor.handler\n (:require [hiccup.page :refer :as web-page]\n [hiccup.form :refer :as web-form]))\n
In very commonly used libraries or very highly used functions through out the code, refer those functions explicitly
(ns naming.is.hard\n (:require [compojure.core :refer [defroutes GET POST]]\n [ring.middleware.defaults :refer [wrap-defaults site-defaults]]))\n
"},{"location":"reference/naming-conventions/#converting-functions","title":"Converting functions","text":"When a function takes values in one format or type and converts them to another
Examples
md->html\n\nmap->Record-name ; map factory function of a record -- creates a new record from a map\n->Record-name ; positional factory function of a record -- creates a new record from a list of values\n
"},{"location":"reference/naming/","title":"Naming","text":""},{"location":"reference/prasmatic-schema/","title":"Prasmatic Schema","text":""},{"location":"reference/reader-macros/","title":"Reader Macros","text":"This is a collection of reader macros (think syntactic sugar) that are valid in Clojure. These macros are useful for commenting out expressions, defining sets, ...
Many reader macros start with the character #, which is in fact the Dispatch macro that tells the Clojure reader (the thing that takes a file of Clojure text and parses it for consumption in the compiler) to go and look at another read table for the definition of the next character - in essence this allows extending default reader behaviour.
-
#_ - Discard macro - ignore the next expression. Often used to comment out code, especially when nested inside other expressions
-
#' - Var macro - returns the reference to the var. Used to pass the definition of something rather than the result of evaluating it.
There is a nice list of reader macros in the article: The weird and wonderful characters of Clojure by @kouphax.
Hint Reader macros are part of the Clojure language specification, so are different to macros, which can be defined by anyone.
"},{"location":"reference/reader-macros/#todore-write","title":"Todo::Re-write","text":""},{"location":"reference/recursion/","title":"Recursion","text":"Recursion is a highly valuable tool in functional programming as it provides an idiomatic way of processing collections of data.
"},{"location":"reference/recursion/#normal-recursion-is-more-idiomatic","title":"normal recursion is more idiomatic","text":"On average it tends to give you clearer, more functional code whereas loop/recur tens to push you more towards an imperative, iterative style.
"},{"location":"reference/recursion/#recursive-functions","title":"Recursive functions","text":""},{"location":"reference/recursion/#warningrecursion-can-hit-the-limit-of-your-heapstack-and-cause-a-exception","title":"Warning::Recursion can hit the limit of your heap/stack and cause a ... exception","text":""},{"location":"reference/recursion/#tail-call-optimisation-with-recur","title":"Tail-call Optimisation with recur
","text":"Tail-call optimisation is where a part of memory is over-written by additional calls during recursive calls. By using the same memory segment each time, then the memory footprint of your code does not increase.
Therefore recur
is good choice for deeply nested recursion or when manipulating larger (non-lazy) data structures.
Without tail-call optimisation the code may otherwise cause a StackOverflow / Heap out of memory Error
"},{"location":"reference/recursion/#info","title":"Info::","text":"Using the recur
function as the last line of a loop
or function will enable tail call optimisation.
"},{"location":"reference/recursion/#fast","title":"Fast","text":"Using loop
and recur
it's one of the most efficient constructs in Clojure, match the speed of an equivalent for loop in Java code.
"},{"location":"reference/recursion/#restrictions","title":"Restrictions","text":"you can only recur in tail position, you can't do mutual recursion between two different function etc.
Sometime it simply isn't possible to use loop/recur or it may require the contort of code to something very unmanageable to do so.
"},{"location":"reference/recursion/#hintuse-recur-once-you-have-created-a-new-recursive-function","title":"Hint::Use recur once you have created a new recursive function","text":"By calling a recursive function by name rather than using recur
can prevent your code from remaining in an infinite loop if you get a terminating condition wrong. Without recur you memory space will be eaten up and your code will stop. Once your function is working correctly, then you can replace the call to itself with recur
.
"},{"location":"reference/recursion/#examples","title":"Examples","text":"Here are two examples using two different recursion approaches. What are the guidelines of usage of one over another?
This example recursively calls itself
(defn take-while\n \"Returns a lazy sequence of successive items from coll while\n (pred item) returns true. pred must be free of side-effects.\"\n {:added \"1.0\"\n :static true}\n [pred coll]\n (lazy-seq\n (when-let [s (seq coll)]\n (when (pred (first s))\n (cons (first s) (take-while pred (rest s)))))))\n
This example uses loop
and recur
for recursively processing the collection.
(defn take-last\n \"Returns a seq of the last n items in coll. Depending on the type\n of coll may be no better than linear time. For vectors, see also subvec.\"\n {:added \"1.1\"\n :static true}\n [n coll]\n (loop [s (seq coll), lead (seq (drop n coll))]\n (if lead\n (recur (next s) (next lead))\n s)))\n
"},{"location":"reference/recursion/#hint","title":"Hint::","text":"The above example could not use recur
instead of the recursive call to take-while
as that call is not in the last position. The cons
function is in the last position of this function.
"},{"location":"reference/recursion/#misc","title":"Misc","text":"The only one reason to use lazy-seq/lazy-cons mechanism is generating lazy sequences. If you don't need them then loop/recur should undoubtedly be used.
"},{"location":"reference/sequences/","title":"Reference: Clojure from the ground up: sequences","text":"In Chapter 3, we discovered functions as a way to abstract expressions; to rephrase a particular computation with some parts missing. We used functions to transform a single value. But what if we want to apply a function to more than one value at once? What about sequences?
For example, we know that (inc 2) increments the number 2. What if we wanted to increment every number in the vector [1 2 3], producing [2 3 4]?
user=> (inc [1 2 3]) ClassCastException clojure.lang.PersistentVector cannot be cast to java.lang.Number clojure.lang.Numbers.inc (Numbers.java:110) Clearly inc can only work on numbers, not on vectors. We need a different kind of tool.
A direct approach Let\u2019s think about the problem in concrete terms. We want to increment each of three elements: the first, second, and third. We know how to get an element from a sequence by using nth, so let\u2019s start with the first number, at index 0:
user=> (def numbers [1 2 3])
"},{"location":"reference/sequences/#usernumbers","title":"'user/numbers","text":"user=> (nth numbers 0) 1 user=> (inc (nth numbers 0)) 2 So there\u2019s the first element incremented. Now we can do the second:
user=> (inc (nth numbers 1)) 3 user=> (inc (nth numbers 2)) 4 And it should be straightforward to combine these into a vector\u2026
user=> [(inc (nth numbers 0)) (inc (nth numbers 1)) (inc (nth numbers 2))] [2 3 4] Success! We\u2019ve incremented each of the numbers in the list! How about a list with only two elements?
user=> (def numbers [1 2])
"},{"location":"reference/sequences/#usernumbers_1","title":"'user/numbers","text":"user=> [(inc (nth numbers 0)) (inc (nth numbers 1)) (inc (nth numbers 2))]
IndexOutOfBoundsException clojure.lang.PersistentVector.arrayFor (PersistentVector.java:107) Shoot. We tried to get the element at index 2, but couldn\u2019t, because numbers only has indices 0 and 1. Clojure calls that \u201cindex out of bounds\u201d.
We could just leave off the third expression in the vector; taking only elements 0 and 1. But the problem actually gets much worse, because we\u2019d need to make this change every time we wanted to use a different sized vector. And what of a vector with 1000 elements? We\u2019d need 1000 (inc (nth numbers ...)) expressions! Down this path lies madness.
Let\u2019s back up a bit, and try a slightly smaller problem.
Recursion What if we just incremented the first number in the vector? How would that work? We know that first finds the first element in a sequence, and rest finds all the remaining ones.
user=> (first [1 2 3]) 1 user=> (rest [1 2 3]) (2 3) So there\u2019s the pieces we\u2019d need. To glue them back together, we can use a function called cons, which says \u201cmake a list beginning with the first argument, followed by all the elements in the second argument\u201d.
user=> (cons 1 [2]) (1 2) user=> (cons 1 [2 3]) (1 2 3) user=> (cons 1 [2 3 4]) (1 2 3 4) OK so we can split up a sequence, increment the first part, and join them back together. Not so hard, right?
(defn inc-first [numbers] (cons (inc (first numbers)) (rest numbers))) user=> (inc-first [1 2 3 4]) (2 2 3 4) Hey, there we go! First element changed. Will it work with any length list?
user=> (inc-first [5]) (6) user=> (inc-first [])
NullPointerException clojure.lang.Numbers.ops (Numbers.java:942) Shoot. We can\u2019t increment the first element of this empty vector, because it doesn\u2019t have a first element.
user=> (first []) nil user=> (inc nil)
NullPointerException clojure.lang.Numbers.ops (Numbers.java:942) So there are really two cases for this function. If there is a first element in numbers, we\u2019ll increment it as normal. If there\u2019s no such element, we\u2019ll return an empty list. To express this kind of conditional behavior, we\u2019ll use a Clojure special form called if:
"},{"location":"reference/sequences/#user-doc-if","title":"user=> (doc if)","text":"if (if test then else?) Special Form Evaluates test. If not the singular values nil or false, evaluates and yields then, otherwise, evaluates and yields else. If else is not supplied it defaults to nil.
Please see http://clojure.org/special_forms#if To confirm our intuition:
user=> (if true :a :b) :a user=> (if false :a :b) :b Seems straightforward enough.
(defn inc-first [numbers] (if (first numbers) ; If there's a first number, build a new list with cons (cons (inc (first numbers)) (rest numbers)) ; If there's no first number, just return an empty list (list)))
user=> (inc-first []) () user=> (inc-first [1 2 3]) (2 2 3) Success! Now we can handle both cases: empty sequences, and sequences with things in them. Now how about incrementing that second number? Let\u2019s stare at that code for a bit.
(rest numbers) Hang on. That list\u2013(rest numbers)\u2013that\u2019s a list of numbers too. What if we\u2026 used our inc-first function on that list, to increment its first number? Then we\u2019d have incremented both the first and the second element.
(defn inc-more [numbers] (if (first numbers) (cons (inc (first numbers)) (inc-more (rest numbers))) (list))) user=> (inc-more [1 2 3 4]) (2 3 4 5) Odd. That didn\u2019t just increment the first two numbers. It incremented all the numbers. We fell into the complete solution entirely by accident. What happened here?
Well first we\u2026 yes, we got the number one, and incremented it. Then we stuck that onto (inc-first [2 3 4]), which got two, and incremented it. Then we stuck that two onto (inc-first [3 4]), which got three, and then we did the same for four. Only that time around, at the very end of the list, (rest [4]) would have been empty. So when we went to get the first number of the empty list, we took the second branch of the if, and returned the empty list.
Having reached the bottom of the function calls, so to speak, we zip back up the chain. We can imagine this function turning into a long string of cons calls, like so:
(cons 2 (cons 3 (cons 4 (cons 5 '())))) (cons 2 (cons 3 (cons 4 '(5)))) (cons 2 (cons 3 '(4 5))) (cons 2 '(3 4 5)) '(2 3 4 5) This technique is called recursion, and it is a fundamental principle in working with collections, sequences, trees, or graphs\u2026 any problem which has small parts linked together. There are two key elements in a recursive program:
Some part of the problem which has a known solution A relationship which connects one part of the problem to the next Incrementing the elements of an empty list returns the empty list. This is our base case: the ground to build on. Our inductive case, also called the recurrence relation, is how we broke the problem up into incrementing the first number in the sequence, and incrementing all the numbers in the rest of the sequence. The if expression bound these two cases together into a single function; a function defined in terms of itself.
Once the initial step has been taken, every step can be taken.
user=> (inc-more [1 2 3 4 5 6 7 8 9 10 11 12]) (2 3 4 5 6 7 8 9 10 11 12 13) This is the beauty of a recursive function; folding an unbounded stream of computation over and over, onto itself, until only a single step remains.
Generalizing from inc We set out to increment every number in a vector, but nothing in our solution actually depended on inc. It just as well could have been dec, or str, or keyword. Let\u2019s parameterize our inc-more function to use any transformation of its elements:
(defn transform-all [f xs] (if (first xs) (cons (f (first xs)) (transform-all f (rest xs))) (list))) Because we could be talking about any kind of sequence, not just numbers, we\u2019ve named the sequence xs, and its first element x. We also take a function f as an argument, and that function will be applied to each x in turn. So not only can we increment numbers\u2026
user=> (transform-all inc [1 2 3 4]) (2 3 4 5) \u2026but we can turn strings to keywords\u2026
user=> (transform-all keyword [\"bell\" \"hooks\"]) (:bell :hooks) \u2026or wrap every element in a list:
user=> (transform-all list [:codex :book :manuscript]) ((:codex) (:book) (:manuscript)) In short, this function expresses a sequence in which each element is some function applied to the corresponding element in the underlying sequence. This idea is so important that it has its own name, in mathematics, Clojure, and other languages. We call it map.
user=> (map inc [1 2 3 4]) (2 3 4 5) You might remember maps as a datatype in Clojure, too\u2013they\u2019re dictionaries that relate keys to values.
{:year 1969 :event \"moon landing\"} The function map relates one sequence to another. The type map relates keys to values. There is a deep symmetry between the two: maps are usually sparse, and the relationships between keys and values may be arbitrarily complex. The map function, on the other hand, usually expresses the same type of relationship, applied to a series of elements in fixed order.
Building sequences Recursion can do more than just map. We can use it to expand a single value into a sequence of values, each related by some function. For instance:
(defn expand [f x count] (if (pos? count) (cons x (expand f (f x) (dec count))))) Our base case is x itself, followed by the sequence beginning with (f x). That sequence in turn expands to (f (f x)), and then (f (f (f x))), and so on. Each time we call expand, we count down by one using dec. Once the count is zero, the if returns nil, and evaluation stops. If we start with the number 0 and use inc as our function:
user=> user=> (expand inc 0 10) (0 1 2 3 4 5 6 7 8 9) Clojure has a more general form of this function, called iterate.
user=> (take 10 (iterate inc 0)) (0 1 2 3 4 5 6 7 8 9) Since this sequence is infinitely long, we\u2019re using take to select only the first 10 elements. We can construct more complex sequences by using more complex functions:
user=> (take 10 (iterate (fn [x] (if (odd? x) (+ 1 x) (/ x 2))) 10)) (10 5 6 3 4 2 1 2 1 2) Or build up strings:
user=> (take 5 (iterate (fn [x] (str x \"o\")) \"y\")) (\"y\" \"yo\" \"yoo\" \"yooo\" \"yoooo\") iterate is extremely handy for working with infinite sequences, and has some partners in crime. repeat, for instance, constructs a sequence where every element is the same.
user=> (take 10 (repeat :hi)) (:hi :hi :hi :hi :hi :hi :hi :hi :hi :hi) user=> (repeat 3 :echo) (:echo :echo :echo) And its close relative repeatedly simply calls a function (f) to generate an infinite sequence of values, over and over again, without any relationship between elements. For an infinite sequence of random numbers:
user=> (rand) 0.9002678382322784 user=> (rand) 0.12375594203332863 user=> (take 3 (repeatedly rand)) (0.44442397843046755 0.33668691162169784 0.18244875487846746) Notice that calling (rand) returns a different number each time. We say that rand is an impure function, because it cannot simply be replaced by the same value every time. It does something different each time it\u2019s called.
There\u2019s another very handy sequence function specifically for numbers: range, which generates a sequence of numbers between two points. (range n) gives n successive integers starting at 0. (range n m) returns integers from n to m-1. (range n m step) returns integers from n to m, but separated by step.
user=> (range 5) (0 1 2 3 4) user=> (range 2 10) (2 3 4 5 6 7 8 9) user=> (range 0 100 5) (0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95) To extend a sequence by repeating it forever, use cycle:
user=> (take 10 (cycle [1 2 3])) (1 2 3 1 2 3 1 2 3 1) Transforming sequences Given a sequence, we often want to find a related sequence. map, for instance, applies a function to each element\u2013but has a few more tricks up its sleeve.
user=> (map (fn [n vehicle] (str \"I've got \" n \" \" vehicle \"s\")) [0 200 9] [\"car\" \"train\" \"kiteboard\"]) (\"I've got 0 cars\" \"I've got 200 trains\" \"I've got 9 kiteboards\") If given multiple sequences, map calls its function with one element from each sequence in turn. So the first value will be (f 0 \"car\"), the second (f 200 \"train\"), and so on. Like a zipper, map folds together corresponding elements from multiple collections. To sum three vectors, column-wise:
user=> (map + [1 2 3] [4 5 6] [7 8 9]) (12 15 18) If one sequence is bigger than another, map stops at the end of the smaller one. We can exploit this to combine finite and infinite sequences. For example, to number the elements in a vector:
user=> (map (fn [index element] (str index \". \" element)) (iterate inc 0) [\"erlang\" \"ruby\" \"haskell\"]) (\"0. erlang\" \"1. ruby\" \"2. haskell\") Transforming elements together with their indices is so common that Clojure has a special function for it: map-indexed:
user=> (map-indexed (fn [index element] (str index \". \" element)) [\"erlang\" \"ruby\" \"haskell\"]) (\"0. erlang\" \"1. ruby\" \"2. haskell\") You can also tack one sequence onto the end of another, like so:
user=> (concat [1 2 3] [:a :b :c] [4 5 6]) (1 2 3 :a :b :c 4 5 6) Another way to combine two sequences is to riffle them together, using interleave.
user=> (interleave [:a :b :c] [1 2 3]) (:a 1 :b 2 :c 3) And if you want to insert a specific element between each successive pair in a sequence, try interpose:
user=> (interpose :and [1 2 3 4]) (1 :and 2 :and 3 :and 4) To reverse a sequence, use reverse.
user=> (reverse [1 2 3]) (3 2 1) user=> (reverse \"woolf\") (\\f \\l \\o \\o \\w) Strings are sequences too! Each element of a string is a character, written \\f. You can rejoin those characters into a string with apply str:
user=> (apply str (reverse \"woolf\")) \"floow\" \u2026and break strings up into sequences of chars with seq.
user=> (seq \"sato\") (\\s \\a \\t \\o) To randomize the order of a sequence, use shuffle.
user=> (shuffle [1 2 3 4]) [3 1 2 4] user=> (apply str (shuffle (seq \"abracadabra\"))) \"acaadabrrab\" Subsequences We\u2019ve already seen take, which selects the first n elements. There\u2019s also drop, which removes the first n elements.
user=> (range 10) (0 1 2 3 4 5 6 7 8 9) user=> (take 3 (range 10)) (0 1 2) user=> (drop 3 (range 10)) (3 4 5 6 7 8 9) And for slicing apart the other end of the sequence, we have take-last and drop-last:
user=> (take-last 3 (range 10)) (7 8 9) user=> (drop-last 3 (range 10)) (0 1 2 3 4 5 6) take-while and drop-while work just like take and drop, but use a function to decide when to cut.
user=> (take-while pos? [3 2 1 0 -1 -2 10]) (3 2 1) In general, one can cut a sequence in twain by using split-at, and giving it a particular index. There\u2019s also split-with, which uses a function to decide when to cut.
(split-at 4 (range 10)) [(0 1 2 3) (4 5 6 7 8 9)] user=> (split-with number? [1 2 3 :mark 4 5 6 :mark 7]) [(1 2 3) (:mark 4 5 6 :mark 7)] Notice that because indexes start at zero, sequence functions tend to have predictable numbers of elements. (split-at 4) yields four elements in the first collection, and ensures the second collection begins at index four. (range 10) has ten elements, corresponding to the first ten indices in a sequence. (range 3 5) has two (since 5 - 3 is two) elements. These choices simplify the definition of recursive functions as well.
We can select particular elements from a sequence by applying a function. To find all positive numbers in a list, use filter:
user=> (filter pos? [1 5 -4 -7 3 0]) (1 5 3) filter looks at each element in turn, and includes it in the resulting sequence only if (f element) returns a truthy value. Its complement is remove, which only includes those elements where (f element) is false or nil.
user=> (remove string? [1 \"turing\" :apple]) (1 :apple) Finally, one can group a sequence into chunks using partition, partition-all, or partition-by. For instance, one might group alternating values into pairs:
user=> (partition 2 [:cats 5 :bats 27 :crocodiles 0]) ((:cats 5) (:bats 27) (:crocodiles 0)) Or separate a series of numbers into negative and positive runs:
(user=> (partition-by neg? [1 2 3 2 1 -1 -2 -3 -2 -1 1 2]) ((1 2 3 2 1) (-1 -2 -3 -2 -1) (1 2)) Collapsing sequences After transforming a sequence, we often want to collapse it in some way; to derive some smaller value. For instance, we might want the number of times each element appears in a sequence:
user=> (frequencies [:meow :mrrrow :meow :meow]) {:meow 3, :mrrrow 1} Or to group elements by some function:
user=> (pprint (group-by :first [{:first \"Li\" :last \"Zhou\"} {:first \"Sarah\" :last \"Lee\"} {:first \"Sarah\" :last \"Dunn\"} {:first \"Li\" :last \"O'Toole\"}])) {\"Li\" [{:last \"Zhou\", :first \"Li\"} {:last \"O'Toole\", :first \"Li\"}], \"Sarah\" [{:last \"Lee\", :first \"Sarah\"} {:last \"Dunn\", :first \"Sarah\"}]} Here we\u2019ve taken a sequence of people with first and last names, and used the :first keyword (which can act as a function!) to look up those first names. group-by used that function to produce a map of first names to lists of people\u2013kind of like an index.
In general, we want to combine elements together in some way, using a function. Where map treated each element independently, reducing a sequence requires that we bring some information along. The most general way to collapse a sequence is reduce.
"},{"location":"reference/sequences/#user-doc-reduce","title":"user=> (doc reduce)","text":"clojure.core/reduce ([f coll] [f val coll]) f should be a function of 2 arguments. If val is not supplied, returns the result of applying f to the first 2 items in coll, then applying f to that result and the 3rd item, etc. If coll contains no items, f must accept no arguments as well, and reduce returns the result of calling f with no arguments. If coll has only 1 item, it is returned and f is not called. If val is supplied, returns the result of applying f to val and the first item in coll, then applying f to that result and the 2nd item, etc. If coll contains no items, returns val and f is not called. That\u2019s a little complicated, so we\u2019ll start small. We need a function, f, which combines successive elements of the sequence. (f state element) will return the state for the next invocation of f. As f moves along the sequence, it carries some changing state with it. The final state is the return value of reduce.
user=> (reduce + [1 2 3 4]) 10 reduce begins by calling (+ 1 2), which yields the state 3. Then it calls (+ 3 3), which yields 6. Then (+ 6 4), which returns 10. We\u2019ve taken a function over two elements, and used it to combine all the elements. Mathematically, we could write:
1 + 2 + 3 + 4 3 + 3 + 4 6 + 4 10 So another way to look at reduce is like sticking a function between each pair of elements. To see the reducing process in action, we can use reductions, which returns a sequence of all the intermediate states.
user=> (reductions + [1 2 3 4]) (1 3 6 10) Oftentimes we include a default state to start with. For instance, we could start with an empty set, and add each element to it as we go along:
user=> (reduce conj #{} [:a :b :b :b :a :a])
"},{"location":"reference/sequences/#a-b","title":"{:a :b}","text":"Reducing elements into a collection has its own name: into. We can conj [key value] vectors into a map, for instance, or build up a list:
user=> (into {} [[:a 2] [:b 3]]) {:a 2, :b 3} user=> (into (list) [1 2 3 4]) (4 3 2 1) Because elements added to a list appear at the beginning, not the end, this expression reverses the sequence. Vectors conj onto the end, so to emit the elements in order, using reduce, we might try:
user=> (reduce conj [] [1 2 3 4 5]) (reduce conj [] [1 2 3 4 5]) [1 2 3 4 5] Which brings up an interesting thought: this looks an awful lot like map. All that\u2019s missing is some kind of transformation applied to each element.
(defn my-map [f coll] (reduce (fn [output element] (conj output (f element))) [] coll)) user=> (my-map inc [1 2 3 4]) [2 3 4 5] Huh. map is just a special kind of reduce. What about, say, take-while?
(defn my-take-while [f coll] (reduce (fn [out elem] (if (f elem) (conj out elem) (reduced out))) [] coll)) We\u2019re using a special function here, reduced, to indicate that we\u2019ve completed our reduction early and can skip the rest of the sequence.
user=> (my-take-while pos? [2 1 0 -1 0 1 2]) [2 1] reduce really is the uber function over sequences. Almost any operation on a sequence can be expressed in terms of a reduce\u2013though for various reasons, many of the Clojure sequence functions are not written this way. For instance, take-while is actually defined like so:
user=> (source take-while) (defn take-while \"Returns a lazy sequence of successive items from coll while (pred item) returns true. pred must be free of side-effects.\" {:added \"1.0\" :static true} [pred coll] (lazy-seq (when-let [s (seq coll)] (when (pred (first s)) (cons (first s) (take-while pred (rest s))))))) There\u2019s a few new pieces here, but the structure is essentially the same as our initial attempt at writing map. When the predicate matches the first element, cons the first element onto take-while, applied to the rest of the sequence. That lazy-seq construct allows Clojure to compute this sequence as required, instead of right away. It defers execution to a later time.
Most of Clojure\u2019s sequence functions are lazy. They don\u2019t do anything until needed. For instance, we can increment every number from zero to infinity:
user=> (def infinite-sequence (map inc (iterate inc 0)))
"},{"location":"reference/sequences/#userinfinite-sequence","title":"'user/infinite-sequence","text":"user=> (realized? infinite-sequence) false That function returned immediately. Because it hasn\u2019t done any work yet, we say the sequence is unrealized. It doesn\u2019t increment any numbers at all until we ask for them:
user=> (take 10 infinite-sequence) (1 2 3 4 5 6 7 8 9 10) user=> (realized? infinite-sequence) true Lazy sequences also remember their contents, once evaluated, for faster access.
Putting it all together We\u2019ve seen how recursion generalizes a function over one thing into a function over many things, and discovered a rich landscape of recursive functions over sequences. Now let\u2019s use our knowledge of sequences to solve a more complex problem: find the sum of the products of consecutive pairs of the first 1000 odd integers.
First, we\u2019ll need the integers. We can start with 0, and work our way up to infinity. To save time printing an infinite number of integers, we\u2019ll start with just the first 10.
user=> (take 10 (iterate inc 0)) (0 1 2 3 4 5 6 7 8 9) Now we need to find only the ones which are odd. Remember, filter pares down a sequence to only those elements which pass a test.
user=> (take 10 (filter odd? (iterate inc 0))) (1 3 5 7 9 11 13 15 17 19) For consecutive pairs, we want to take [1 3 5 7 ...] and find a sequence like ([1 3] [3 5] [5 7] ...). That sounds like a job for partition:
user=> (take 3 (partition 2 (filter odd? (iterate inc 0)))) ((1 3) (5 7) (9 11)) Not quite right\u2013this gave us non-overlapping pairs, but we wanted overlapping ones too. A quick check of (doc partition) reveals the step parameter:
user=> (take 3 (partition 2 1 (filter odd? (iterate inc 0)))) ((1 3) (3 5) (5 7)) Now we need to find the product for each pair. Given a pair, multiply the two pieces together\u2026 yes, that sounds like map:
user=> (take 3 (map (fn [pair] (* (first pair) (second pair))) (partition 2 1 (filter odd? (iterate inc 0))))) (3 15 35) Getting a bit unwieldy, isn\u2019t it? Only one final step: sum all those products. We\u2019ll adjust the take to include the first 1000, not the first 3, elements.
user=> (reduce + (take 1000 (map (fn [pair] (* (first pair) (second pair))) (partition 2 1 (filter odd? (iterate inc 0))))) 1335333000 The sum of the first thousand products of consecutive pairs of the odd integers starting at 0. See how each part leads to the next? This expression looks a lot like the way we phrased the problem in English\u2013but both English and Lisp expressions are sort of backwards, in a way. The part that happens first appears deepest, last, in the expression. In a chain of reasoning like this, it\u2019d be nicer to write it in order.
user=> (->> 0 (iterate inc) (filter odd?) (partition 2 1) (map (fn [pair] (* (first pair) (second pair)))) (take 1000) (reduce +)) 1335333000 Much easier to read: now everything flows in order, from top to bottom, and we\u2019ve flattened out the deeply nested expressions into a single level. This is how object-oriented languages structure their expressions: as a chain of function invocations, each acting on the previous value.
But how is this possible? Which expression gets evaluated first? (take 1000) isn\u2019t even a valid call\u2013where\u2019s its second argument? How are any of these forms evaluated?
What kind of arcane function is ->>?
All these mysteries, and more, in Chapter 5: Macros.
Problems Write a function to find out if a string is a palindrome\u2013that is, if it looks the same forwards and backwards. Find the number of \u2018c\u2019s in \u201cabracadabra\u201d. Write your own version of filter. Find the first 100 prime numbers: 2, 3, 5, 7, 11, 13, 17, \u2026.
"},{"location":"reference/threading-macros/","title":"Reference: Threading macros","text":"Using the threading macro, the result of every function is passed onto the next function in the list. This can be seen very clearly using ,,, to denote where the value is passed to the next function
(->\n \"project.clj\"\n slurp ,,,\n read-string ,,,\n (nth ,,, 2))\n
To make this really simple lets create a contrived example of the threading macro. Here we use the str
function to join strings together. Each individual str
function joins its own strings together, passing the resulting string as the first argument to the next function.
(->\n (str \"This\" \" \" \"is\" \" \")\n (str \"the\" \" \" \"threading\" \" \" \"macro\")\n (str \"in\" \" \" \"action.\"))\n
Output
;; => \"This is the threading macro in action\"\n
"},{"location":"reference/threading-macros/#hintcommas-in-clojure-are-whitespace","title":"Hint::Commas in clojure are whitespace","text":"Commas are simply ignored when the Clojure Reader parses code. Commas are rarely used and only to help human readability of the code
"},{"location":"reference/threading-macros/#thread-last-macro","title":"Thread-last macro","text":"Using the thread-last macro, ->>, the result of a function is passed as the last argument of the next function call. So in another simple series of str function calls, our text comes out backwards.
(->> \" this\"\n (str \" is\")\n (str \" backwards\"))\n
"},{"location":"reference/clojure-cli/","title":"Reference: Clojure CLI","text":"A reference on using Clojure CLI and using community tools effectively.
- structure of the deps.edn configuration
- execution options
- Java Virtual Machine options
- defining custom aliases
- common aliases from practicalli/clojure-deps-edn project
"},{"location":"reference/clojure-cli/example-alias-definitions/","title":"Common alias definitions","text":""},{"location":"reference/clojure-cli/example-alias-definitions/#task-run-a-simple-terminal-repl","title":"Task: Run a simple terminal REPL","text":"clojure
and clj
(requires rlwrap) will run a REPL if given no other arguments.
Running either command from the root directory of a project will merge the deps.edn
configuration with ~/.clojure/deps.edn
.
"},{"location":"reference/clojure-cli/example-alias-definitions/#task-run-a-repl-with-additional-dependencies-and-paths","title":"Task: Run a REPL with additional dependencies and paths","text":"clojure -M:alias
will run a repl if the alias does not contain a main namespace defined in :main-opts
, e.g. :main-opts [\"-m\" \"namespace.main\"]
. The deps and path values are included from the alias.
If the following alias is defined in the project deps.edn
file
:env/dev\n{:extra-paths [\"resources\"]\n :extra-deps {com.h2database/h2 {:mvn/version \"1.4.200\"}}}\n
clojure -M:env/dev
will add resources
directory to the path and the h2 database library to the dependencies, then runs a REPL.
Including the -r
option in the command line forces a REPL to run, even if a main namespace is provided via :main-opts
or the command line.
clojure -r -M:alias1:alias2\n
The dependencies and paths will be merged from the alias from left to right, with each successive alias over-riding the value of any matching keys in the dependencies.
"},{"location":"reference/clojure-cli/example-alias-definitions/#task-create-a-new-project-from-template","title":"Task: Create a new project from template","text":"The clj-new
community tool can be used to create a Clojure / ClojureScript project, using a template to provide a project structure and example source code and tests.
Using the :main-opts
approach, an alias for clj-new
would be defined as follows
:project/new\n {:extra-deps {seancorfield/clj-new {:mvn/version \"1.0.215\"}}\n :main-opts [\"-m\" \"clj-new.create\"]}\n
The clj-new
tool can be run using the -M
flag, passing the template and project names as arguments.
clojure -M:project/new template-name project-domain/application-name
To create a project as an application (to be run via the command line) for the practicalli domain with the application called banking-on-clojure
clojure -M:new app practicalli/banking-on-clojure\n
The latest version of the clj-new
project also supports using the -X
flag and default arguments.
Adding the :exec-fn
to the clj-new
alias, the -X
flag can be used instead of the -M
. Arguments are supplied as key/value pairs
:project/new\n {:extra-deps {seancorfield/clj-new {:mvn/version \"1.1.215\"}}\n :exec-fn clj-new/create}\n
Use this alias with the -X
flag
clojure -X:project/new :template template-name :name practicalli/banking-on-clojure\n
Default values can be added using the :exec-args
key to the alias
:project/new\n{:extra-deps {seancorfield/clj-new {:mvn/version \"1.1.215\"}}\n :exec-fn clj-new.create\n :exec-args {:template lib :name practicalli/playground}}\n
clojure -M:project/new :name practicalli/awesome-webapp
will create a new project using the {:template lib :name practicalli/awesome-webapp}
argument.
"},{"location":"reference/clojure-cli/example-alias-definitions/#task-executing-a-specific-function","title":"Task: Executing a specific function","text":"Clojure can run a specific function, useful for one off tasks or timed batch processing (via cron or similar tool) as well as complete applications.
Arguments to the function are passed as a hash-map, defined in either an aliases :exec-args
key or as key value pairs on the command line. Command line key value pairs are merged with the :exec-arg
hash-map, replacing the values from the command line if there are matching keys.
Scenarios
clojure -X namespace/fn
runs the function specified on the command line, passing an empty hash-map as an argument
clojure -X:alias fn
runs the function if the :ns-default
is set to the namespace that contains the function, otherwise \"Unqualified function can't be resolved: fn-name\" error is returned.
clojure -X:alias
runs the function specified by :exec-fn
in the alias. The function must include its namespace or have that namespace defined in :ns-default
. If :exec-args
is defined in the alias, its value is passed to the function, otherwise an empty hash-map is passed to the function as an argument.
clojure -X:alias namespace/fn
will run the function specified on the command line, over-riding :exec-fn
if it is defined in the alias. :exec-args
will be passed to the command line function if defined in the alias. Dependencies and paths will be used from the alias. Assumption: the command line namespace also overrides the :ns-default
value if set.
clojure -X:alias :key1 val1 :key2 val2
will execute the function defined in :exec-fn
and pass it the key value pairs from the command line as a hash map. If the alias has :exec-args
defined, command line args are merged into the :exec-fn
hash-map, replacing the default values in :exec-args
where keys match.
Assuming there is an alias called database/migrate
defined in the project deps.edn
:database/migrate\n{:exec-fn practicalli.banking-on-clojure.database/migrate\n :exec-args {:db-type \"h2\" :database \"banking-on-clojure\"}}\n
clojure -X:database/migrate :database \"specs-repository\"
would merge the command line args with :exec-args
to create the hash-map {:db-type \"h2\" :database \"specs-repository\"}
which is passed to the practicalli.banking-on-clojure.database/migrate
function as an argument.
"},{"location":"reference/clojure-cli/example-alias-definitions/#task-executing-a-range-of-functions","title":"Task: Executing a range of functions","text":":ns-default
in an alias defines the namespace that contains the functions that could be executed.
{:aliases\n {:project/run\n {:ns-default practicalli/banking-on-clojure}}}\n
Specific functions from the namespace can be called via the command line
clojure -X:project/run migrate-db :db-type h2 :database banking-on-clojure\nclojure -X:project/run server-start :port 8080\n
"},{"location":"reference/clojure-cli/example-alias-definitions/#task-dry-run-or-prepare-for-ci-containers","title":"Task: Dry Run or Prepare for CI / Containers","text":"clojure -P
will download the libraries defined in :deps
in the project deps.edn
and do nothing else. Standard out shows downloading of dependencies not already cached locally, including name and versions and repository downloaded from.
Qualified namespaces required
If an unqualified library name is used, e.g. compojure
, then a warning is sent to the standard out. Change the name of the library to be fully qualified e.g. weavejester/compojure
. Use the same name if there is no official qualified domain, e.g. http-kit/http-kit
The -P
flag can be used to modify an existing command to ensure no execution takes place, ensuring a prepare only (dry run) action.
clojure -P -M:alias-name
downloads the dependencies for the specific aliases and multiple aliases can be chained together, e.g. clojure -P -M:dev/env:test-runner/kaocha
The -P
flag uses everything from an alias not related to execution.
The classic way to download deps was to run clojure -A:aliases -Spath
, where -Spath
prevented execution of repl or main.
"},{"location":"reference/clojure-cli/example-alias-definitions/#run-a-clojure-application","title":"Run a Clojure application","text":"clojure -m full.namespace.to.dash-main
calls the -main
function from the given namespace. Arguments to the function are simply added to the end of the command line and passed to the -main
function in the given namespace.
The -m
flag in the CLI tools pre-release returns a warning that -M
should be used.
Using -M
and -m
works, but seems redundant. Using -M
by itself runs the REPL.
clojure -M -m full.namespace.to.dash-main\n
-M
seems useful when including an alias with extra configuration (eg. :extra-deps
, :extra-paths
, :main-opts
). As :main-opts
is no different to the -m
option, creating an alias just to avoid the warning seems excessive.
"},{"location":"reference/clojure-cli/example-alias-definitions/#task-executing-a-project-using-edn-style-args","title":"Task: Executing a project - using Edn style args","text":"Clojure CLI tools is encouraging a move to functions that take a hash-map for their arguments. Passing arguments in as an edn data structure has more rigor than options and strings on the command line.
The simplest form is to define an alias to run the project, specifying just the function to execute using :exec-fn
:aliases\n {:project/run\n {:exec-fn practicalli.banking-on-clojure/server-start}\n } ;; End of Aliases\n
Then the project can be run using this alias.
clojure -X:project/run\n
Arguments can be passed to the function as key/value pairs on the command line.
clojure -X:project/run :port 8080 :host \"localhost\"\n
:exec-args
provides a way to define default arguments for the function, regardless of if it is defined in ;:exec-fn
or passed via the command line.
:exec-args
defines a hash-map of arguments so the function must support taking a hash-map as an argument.
A function may take variable args, especially if it is supporting both hash-maps and strings as options.
:aliases\n {:project/run\n {:exec-fn fully.qualified/namespace\n :exec-args {:default \"arguments\" :can-be-over-ridden-by \"command-line-args\"} }\n } ;; End of Aliases\n
Adding :exec-args
to the :run-project
:aliases\n {:project/run\n {:exec-fn practicalli.banking-on-clojure/server-start\n :exec-args {:port 8888 :host \"localhost\"}}\n } ;; End of Aliases\n
"},{"location":"reference/clojure-cli/example-alias-definitions/#example-of-running-a-clojure-project-hello-world","title":"Example of running a Clojure project - hello-world","text":"In this example I use the hello-world example from https://clojure.org/guides/deps_and_cli#_writing_a_program A project deps.edn
file was created containing the dependency for clojure.java-time and the source code from that page copied into src/hello.clj
clojure -m
hello runs the project and returns the time from running the -main function. However this gives a warning:
WARNING: When invoking clojure.main, use -M\n
clojure -M
runs a REPL
clojure -M -m hello
runs the project and returns the time. But then I ask myself what is the purpose of -M
Creating an alias to run the project seems an interesting idea, as I could also set default arguments.
Adding an :project-run
alias to the project deps.edn
works when calling with clojure -M:project-run
:aliases\n {:project-run {:main-opts [\"-m\" \"hello\"]}}\n
Changing the :project-run
alias to use :exec-fn
and a fully qualified function (-main by default) should work when calling with clojure -X:project-run
. :aliases {:run-project {:exec-fn hello]}}
However, the hello-world
project has an unqualified function and cannot be resolved.
Moving the source code to src/practicalli/hello.clj
and calling clojure -X:run-project
gives an execution error, (ArityException)
as the -main
function does not take any arguments, (defn -main [] ,,,)
.
Changing the -main
function to (defn -main [& args] ,,,)
fixes the arity exception and calling clojure -X:run-project
works.
"},{"location":"reference/clojure-cli/example-alias-definitions/#local-maven-install","title":"Local Maven install","text":"Install a jar into the local Maven cache, typically ~/.m2/repository/
directory, organised by groupId
clojure -X:deps mvn-install :jar '\"/path/to.jar\"'\n
edn strings must be in double quotes, and then single-quoted for the shell
mvn-install
uses the .pom
file contained in the jar (if it exists) to determine the groupId, artifactId, and version coordinates to use when the jar is installed.
The .pom
file can also be specified using the :pom
argument.
The install argmap takes the following options:
key Required Description :jar
required path to the jar file to install :pom
optional path to .pom file (if .jar file does not contain .pom) :lib
optional qualified symbol e.g my.org/lib
:version
optional Version number of library (string type) :classifier
optional (string type) :local-repo
optional path to local repo (default = ~/.m2/repository)"},{"location":"reference/clojure-cli/jvm-options/","title":"Reference: Clojure CLI JVM Options","text":"JDK_JAVA_OPTIONS
Environment Variable
JDK_JAVA_OPTIONS
is the official Environment Variable for setting options when calling java
, javac
and other Java commands to start running a Java Virtual Machine (Java version 9 onward).
Java Virtual Machine options can be passed using the Clojure CLI, either via the -J
command line flag or :jvm-opts
in a deps.edn
alias.
Java Virtual Machine configuration and reporting
Java Virtual Machine section covers commonly used options, reporting JVM metrics and optimisation of the JVM process.
"},{"location":"reference/clojure-cli/jvm-options/#clojure-cli-command-line-options","title":"Clojure CLI command line options","text":"Clojure CLI -J
flag passes configuration options to the JVM. When there are multiple, each must be prefixed with -J
.
clojure -J-XX:+UnlockDiagnosticVMOptions -J\u2011XX:NativeMemoryTracking=summary -J\u2011XX:+PrintNMTStatistics\n
"},{"location":"reference/clojure-cli/jvm-options/#clojure-cli-depsedn-configuration","title":"Clojure CLI deps.edn configuration","text":":jvm-opts
key in an alias adds JVM options to Clojure CLI deps.edn configuration. The :jvm-opts
key has a value that is a collection of string JVM options [\"-Xms2048m\" \"-Xmx4096\"]
Alias to set a large heap size
:jvm/heap-max-2g {:jvm-opts [\"-Xmx2G\"]}\n
Report a full breakdown of the HotSpot JVM\u2019s memory usage upon exit using the following option combination:
:jvm/report {:jvm-opts [\"-XX:+UnlockDiagnosticVMOptions\"\n \"\u2011XX:NativeMemoryTracking=summary\"\n \"\u2011XX:+PrintNMTStatistics\"]}\n
Add a Java module
:jvm/xml-bind {:jvm-opts [\"\u2013add-modules java.xml.bind\"]}\n
Ignoring unrecognised options
:jvm-opts [\"-XX:+IgnoreUnrecognizedVMOptions\"]\n
The aliases can be used with the Clojure CLI execution options: -A
(for built-in REPL invocation), -X
and -T
(for clojure.exec function execution), or -M
(for clojure.main execution).
-J
JVM options specified on the command line are concatenated after the alias options
"},{"location":"reference/clojure-cli/jvm-options/#calling-a-clojure-uberjar","title":"Calling A Clojure Uberjar","text":"JVM options must be specified when calling an uberjar with the java
command, :jvm-opts
in the project deps.edn
are not used with the java
command
java -jar project-uberjar.jar -J...\n
Use JDK_JAVA_OPTIONS
to define JVM options
JDK_JAVA_OPTIONS
environment variable is used to define options that are used whenever the java
command is called, greatly simplifying java
commands.
The JDK_JAVA_OPTIONS
environment variable can be used with deployment systems and passed to container environments to simplify adjustment of resources used by the JVM process.
"},{"location":"reference/clojure-cli/jvm-options/#clojure-related-jvm-options","title":"Clojure related JVM options","text":"Specify options or system properties to set up the Clojure service
-Dclojure.compiler.disable-locals-clearing=true
- make more info available to debuggers
-Dclojure.main.report=stderr
- print stack traces to standard error instead of saving to file, useful if process failing on startup
-Dclojure.spec.skip-macros=false
- skip spec checks against macro forms
"},{"location":"reference/clojure-cli/jvm-options/#memory-management","title":"Memory Management","text":"-XX:CompressedClassSpaceSize=3G
- prevent a specific type of OOMs
-XX:MaxJavaStackTraceDepth=1000000
- prevents trivial Stack Overflow errors
-Xmx24G
- set high maximum heap, preventing certain types of Out Of Memory errors (ideally high memory usage should be profiled if cause not known)
-Xss6144k
- increase stack size x6 to prevent Stack Overflow errors
The current default can be found with java -XX:+PrintFlagsFinal -version 2>/dev/null | grep \"intx ThreadStackSize\"
-Xms6G
- Set minimum memory that is equal or greater than memory used by a running REPL, to improve performance
-Xmx1G
- limit maximum heap allocation so a process can never use more memory, useful for environments with limited memory resources
:jvm/mem-max1g {:jvm-opts [\"-Xmx1G\"]}\n
"},{"location":"reference/clojure-cli/jvm-options/#container-memory-management","title":"Container Memory Management","text":"JDK_JAVA_OPTIONS
environment variable should be used for setting JVM options within a container or in the provisioning service (e.g. Kubernettes / Argo CD) that deploys containers.
Use JVM options that optimise running in a container
-
-XshowSettings:system
to output the resources the JVM believes it has access too, a very simple diagnostic tool to include
-
-XX:+UseContainerSupport
instruct the JVM that it is running in a container environment, disabling the checks the JVM would otherwise carry out to determine if it was running in a container. Can save a very small amount of start up time, though mainly used to ensure the JVM knows its in a container.
-
-XX:MaxRAMPercentage=90
to set a relative maximum percentage of heap to use, based on the memory available from the host, e.g. -XX:MaxRAMPercentage=80
will use a heap size of 80% of the available host memory
"},{"location":"reference/clojure-cli/jvm-options/#dockerfile-example-with-jdk_java_options-environment-variable","title":"Dockerfile example with JDK_JAVA_OPTIONS environment variable","text":"In this Dockerfile
excerpt the JDK_JAVA_OPTIONS
environment variable is used to print out the resources the JVM believes it has access to at startup. The JVM is instructed that it is running in a container environment and should use a maximum 90% heap size of the hosts memory resource.
ENV JDK_JAVA_OPTIONS \"-XshowSettings:system -XX:+UseContainerSupport -XX:MaxRAMPercentage=90\"\nCMD [\"java\", \"-jar\", \"/opt/practicalli-service.jar\"]\n
"},{"location":"reference/clojure-cli/jvm-options/#low-latency-systems","title":"Low latency systems","text":"For systems that require very low latency, use the Z Garbage collector
\"-XX:+UnlockExperimentalVMOptions -XX:+UseZGC\"\n
"},{"location":"reference/clojure-cli/jvm-options/#stack-traces","title":"Stack traces","text":"-XX:+TieredCompilation
- enable tiered compilation to support accurate bench-marking (increases startup time)
-XX:-OmitStackTraceInFastThrow
- don't elide stack traces
"},{"location":"reference/clojure-cli/jvm-options/#startup-options","title":"Startup options","text":"-Xverify:none
option reduces startup time of the JVM by skipping verification process
\"-Xverify:none\"\n
The verification process is a valuable check, especially for code that has not been run before. So the code should be run through the verification process before deploying to production.
"},{"location":"reference/clojure-cli/jvm-options/#benchmark-options","title":"Benchmark options","text":"Enable various optimizations, for guaranteeing accurate benchmarking (at the cost of slower startup):
\"-server\"
"},{"location":"reference/clojure-cli/jvm-options/#graphical-ui-related-options","title":"Graphical UI related options","text":"-Djava.awt.headless=true
- disable all UI features for disabling the clipboard for personal security:
-Dapple.awt.UIElement=true
- remove icon from the MacOSX Dock
-Dclash.dev.expound=true
- ?
"},{"location":"reference/clojure-cli/jvm-options/#garbage-collection","title":"Garbage Collection","text":"Setup GC with short STW pauses which can be relevant for very high web server workloads
:jvm/g1gc\n{:jvm-opts [\"-XX:+UseG1GC\"\n \"-XX:MaxGCPauseMillis=200\"\n \"-XX:ParallelGCThreads=20\"\n \"-XX:ConcGCThreads=5\"\n \"-XX:InitiatingHeapOccupancyPercent=70\"]}\n
- Source: Tuning Garbage Collection with Oracle JDK
"},{"location":"reference/clojure-cli/jvm-options/#view-jvm-options-of-a-running-jvm-process","title":"View JVM options of a running JVM process","text":"Use a JMX client, e.g. VisualVM
jcmd pid VM.system_properties
or jcmd pid VM.flags
using jcmd -l
to get the pid of the JVM process
On Linux ps -ef | grep java
which includes the options to run the JVM process, ps -auxww
to show long arguments
Getting the parameters of a running JVM
"},{"location":"reference/clojure-cli/jvm-options/#references","title":"References","text":"JVM Options cheatsheet - JRebel
"},{"location":"reference/clojure-svg/","title":"Clojure Scalable Vector Graphics - SVG","text":"Scalable Vector Graphics, SVG, is an image format for two-dimensional (2D) graphics.
An SVG image uses data to describe how to draw an image, ensuring that images can shrink and scale easily and retain a high quality image. As images are formed from data, shapes can easily be combined or intersected to form new shapes. Using a data format also means SVG images can be created from code and therefore animated.
Raster image formats like gif, jpeg and png use a grid of squares called pixels to define an image (also known as a bitmap). Each pixel has a colour and position in an image. When zooming into an image the pixels grow larger distorting the sharpness of an image, referred to as pixelation, .Multiple versions of raster images are often created at different resolutions to reduce the loss of quality when viewed at different sizes.
"},{"location":"reference/clojure-svg/#hintwork-in-progress","title":"Hint::Work in progress","text":""},{"location":"reference/clojure-svg/#concepts","title":"Concepts","text":" - viewbox
- style - border, background, width, height, stoke, fill, draw (path)
- shapes - circle, path
"},{"location":"reference/clojure-svg/#viewbox","title":"Viewbox","text":"A viewbox defines a co-ordinate system for the image. Defining a size for the viewbox defining a frame for the image where positions are relative to that frame, irrespective of the size of the image or how that image is scaled.
A viewbox size should be selected to make the image as simple as possible to define within itself.
Example: tictactoe O's and X's and the grid that represents the board.
tictactoe O's and X's and the grid that represents the board
"},{"location":"reference/clojure-svg/#related-projects","title":"Related projects","text":" - TicTacToe with ClojureScript, Reagent and SVG
- System monitoring
- Practicalli SVG examples library
- Programming SVG with Clojure (TODO)
"},{"location":"reference/clojure-svg/#references","title":"References","text":" - SVG: Scalable Vector Graphics - Mozilla Developer network
"},{"location":"reference/clojure-syntax/assigning-names/","title":"Assigning Names","text":"If we have to type the same values over and over, it would be very hard to write a program. What we need are names for values, so we can refer to them in a way we can remember. We do that using def
.
(def mangoes 3)\n(def oranges 5)\n(+ mangoes oranges)\n
When you assign a name to a value, that name is called a symbol. You can assign more than simple values to symbols. Try the following:
(def fruit (+ mangoes oranges))\n(def average-fruit-amount (/ fruit 2))\naverage-fruit-amount\n
Look at the last line, and see how we can use symbols by themselves to refer to a value.
Note Take the Clojure syntax you have learnt to far and write a metric/imperial converter
Take your height in feet and inches and convert it to inches using arithmetic in Clojure.
Then convert that to centimeters. There are 2.54 centimeters in an inch.
Lastly, ask two people near you for their height in centimeters. Find the average of your heights.
Note Bonus: Convert that average back to feet and inches. The feet and the inches will be separate numbers. (quot x y)
will give you the whole number part when dividing two numbers. (mod x y)
will give you the remainder when dividing two numbers.
"},{"location":"reference/clojure-syntax/code-documentation/","title":"Code documentation","text":"Clojure functions are documented by adding a string to the function definition, after the function name. This is referred to as the doc string.
(defn example-function\n \"This is the documentation for this function, referred to as a doc string\"\n [arguments]\n (str \"some behaviour\"))\n
def
bindings can also be documented to provide context to the data the name is bound to.
(def weather-data\n \"Data set for weather across the major capitals of Europe\"\n [{:date \"2020-05-015\" :city \"Berlin\" :temperature-max 24 :temperature-min 13 :rainfall 1}\n {:date \"2020-05-015\" :city \"Amsterdam\" :temperature-max 25 :temperature-min 14 :rainfall 0}])\n
"},{"location":"reference/clojure-syntax/code-documentation/#write-easily-understandable-docstrings","title":"Write easily understandable docstrings","text":"Practically recommends including specific details of the arguments passed to a function and the expected return type. Including this at the end of the docstring makes that information very quick to find.
\"Geographic visualization data set generator\n\n Arguments:\n - combined data set of GeoJSON and Cases\n - maximum integer value for scale\n Returns:\n - Oz view hash-map\"\n
"},{"location":"reference/clojure-syntax/code-documentation/#reading-source-code","title":"Reading source code","text":"clojure.repl/source
will show the source code of a given function, which can be a valuable way to understand the function. Reading function source code also provides ideas when writing custom Clojure code.
Reading the source code for clojure.core
functions is a good way to learn those functions, although some functions have been optimised for performance and are harder to follow.
Source code for clojure.core is available online and is also linked to from the function descriptions on clojuredocs.org.
"},{"location":"reference/clojure-syntax/code-documentation/#writing-your-own-documentation","title":"Writing your own documentation","text":"Writing good documentation for your own functions take practice which pays off in the long run.
()\n
(defn my-function\n \"I should practice writing clear and meaningful documentation for my functions.\n Arguments: brief description of arguments\"\n [arguments]\n (str \"I should write pure functions where ever possible. \"\n \"Each function should have a specific purpose. \"\n \"A function should be clean and easy to read.\"))\n
"},{"location":"reference/clojure-syntax/code-documentation/#notedefine-your-own-function","title":"Note::Define your own function","text":"Practice writing a meaningful documentation in the doc string
"},{"location":"reference/clojure-syntax/comments/","title":"Comments","text":"As well as the classic line comments, Clojure also can comment specific parts of the code structure, even when it run over multiple lines.
;;
to comment a whole line and ;
to add a comment after the start of a line
(comment )
wraps forms and returns nil
when evaluated, referred to as rich comments
#_
to ignore the next form as if it has not been written, commonly used for debugging
"},{"location":"reference/clojure-syntax/comments/#line-comments","title":"Line comments","text":"Add general documentation for a namespace, such as a comment header that describes the overall purpose of a namespace.
Separate a namespace into logical sections to aid navigation and help identify opportunities to refactor a namespace as it grows.
"},{"location":"reference/clojure-syntax/comments/#comment-function","title":"comment function","text":"The (comment ,,,)
function is used to included code that is only run by the developer directly.
(comment (map + [1 2 3] [1 2 3]))\n
The comment
function returns nil
so its advised not to use it inside another form. For example:
(map + [1 2 3] (comment [1 2 3])) ; nil will be passed to map as the third argument\n
This will fail as it tries to use the +
function to add 1
to nil
The #_
is the appropriate comment style for this example
"},{"location":"reference/clojure-syntax/comments/#rich-comment","title":"Rich comment","text":"The comment
expression is referred to a a rich comment, as it is often used to evaluate expressions it contains as part of a REPL driven development workflow.
Unlike line comments, forms inside a comment block can be evaluated in a Clojure aware editor to help the developer work with a project.
Rich comment are useful for rapidly iterating over different design decisions by including the same function but with different implementations. Hide clj-kondo linter](/clojure-cli/install/install-clojure.html#clj-kondo-static-analyser--linter) warnings for redefined vars (def
, defn
) when using this approach.
;; Rich comment block with redefined vars ignored\n#_{:clj-kondo/ignore [:redefined-var]}\n(comment\n\n ) ;; End of rich comment block\n
The expressions can represent example function for using the project, such as starting/restarting the system, updating the database, etc.
Expressions in rich comment blocks can also represent how to use a namespace API, providing examples of arguments to supply to further convey meaning to the code.
These rich comments make a project more accessible and easier to use.
The \"Rich\" in the name also refers to Rich Hickey, the author and benevolent leader of the Clojure language.
"},{"location":"reference/clojure-syntax/comments/#comment-forms-with-the-comment-reader-macro","title":"Comment forms with the comment reader macro","text":"#_
is the comment reader macro that instructs the Clojure reader to completely ignore the next form, as if it had never been written.
No value is returned, so this comment is safe to use within an expression.
You can place #_
before the start of a form and everything inside that form will be commented
#_(def my-data [1 2 3 4 5])\n
#_
will comment forms that span multiple lines, for example function definitions
#_(defn my-function\n [args]\n (str \"I am an experiment, so not always needed\"))\n
#_
can also be put on the line(s) before the Clojure form, which can make your code more readable and keep alignment of your code consistent.
"},{"location":"reference/clojure-syntax/comments/#debugging-with-comment-macro","title":"debugging with comment macro","text":"As the comment macro can be used without returning a value, it can safely be added to code to help with debugging.
This code example finds the most common word in the text of a book. Most of the lines of code in the threading macro have been commented to discover what the non-commented code does.
As each expression in the threading macros is understood, by looking at its results, comments can be removed to understand more of the code.
(defn most-common-words [book]\n (->> book\n (re-seq #\"[a-zA-Z0-9|']+\" ,,,)\n #_(map #(clojure.string/lower-case %))\n #_(remove common-english-words)\n #_frequencies\n #_(sort-by val)\n #_reverse\n ))\n
This is an effective way to deconstruct parts of a larger Clojure expression.
Watch episode #13 of Practicalli Clojure study group to see this in practice.
"},{"location":"reference/clojure-syntax/comments/#comment-nested-forms","title":"comment nested forms","text":"#_
tells the reader to ignore the next form, it is therefore never evaluated and neither is the #_
. This means that #_
can be used inside a nested form to comment just a part of the expression
In this example the third vector of values is not read by the Clojure reader and therefore is not passed as an argument to +
function by map
(map + [1 2 3] [4 5 6] #_[7 8 9])
"},{"location":"reference/clojure-syntax/comments/#stacking-comments","title":"Stacking comments","text":"The comment reader macro has the ability to stack these comments on forms, so using #_#_
will comment out two successive forms.
In a let
form we can comment out a name binding that is causing problems. As the name and value are both forms, then we use a stacked #_
to comment both out. We also do the same in the body of the let, so as to not include the evaluation of the string or name2
local name in the str
form.
(let [name1 \"value\"\n #_#_name2 \"another-value]\n (str \"First name is: \" name1 #_#_\" second name is: \" name2\n
"},{"location":"reference/clojure-syntax/control-flow/","title":"Control Flow","text":"The following section of functions gives examples of simple control flow. As you gain more experience with Clojure, you will discover more functional ways to achieve the same (or better) results.
Hint Although these functions may seem similar to other non-functional languages, there are subtle differences
"},{"location":"reference/clojure-syntax/control-flow/#if","title":"If","text":"Using the if
function you can test if an expression evaluates to true. If it is true, the first value is returned, if its false the second value is returned.
Here is a simple example to see if one number is bigger that another
(if (> 3 2)\n \"Higher\"\n \"Lower\")\n\n=> \"Higher\"\n
Here is an example of an condition inside an anonymous function.
(defn even-number [number]\n (if (odd? number)\n (inc number)\n number))\n\n(even-number 41)\n;; => 42\n
"},{"location":"reference/clojure-syntax/control-flow/#when","title":"When","text":"When a condition is true, then return the value of evaluating the next expression. If the condition is false, then return nil
(when (> 3 2)\n \"Higher\")\n\n=> \"Higher\"\n
"},{"location":"reference/clojure-syntax/control-flow/#case","title":"Case","text":"When one of these things is true, do this, else default
(case (inc 3)\n 1 \"Not even close\"\n 2 \"I wish I was that young\"\n 3 \"That was my previous value\"\n 4 \"You sunk my battleship\"\n \"I dont think you understood the question\")\n\n=> \"You sunk my battleship\"\n
"},{"location":"reference/clojure-syntax/control-flow/#cond","title":"Cond","text":"Return the associated value of the first condition that is true, or return the default value specified by :otherwise
(cond\n (= 7 (inc 2)) \"(inc 2) is not 7, so this condition is false\"\n (= 16 (* 8 2)) \"This is the first correct condition so its associated expression is returned\"\n (zero? (- (* 8 8) 64)) \"This is true but not returned as a previous condition is true\"\n :otherwise \"None of the above are true\")\n\n;; => \"This is the first correct condition so its associated expression is returned\"\n
"},{"location":"reference/clojure-syntax/control-flow/#for","title":"For","text":"Using the for
function you can Iterate through the values in a collection and evaluate each value in tern with with a condition, using either :when
or :while
.
(for [x (range 10) :when (odd? x)] x)\n\n(for [x (range 10) :while (even? x)] x)\n\n(for [x (range 10)\n y (range 10)]\n [x y])\n
"},{"location":"reference/clojure-syntax/control-flow/#while","title":"While","text":"Do something while the condition is true
(while (condition)\n (do something))\n
Here is a simple while example that uses a (mutable) counter and prints out the results to the repl window.
;; create a counter using a mutable counter\n(def counter (atom 10))\n\n;; While the counter is positive (is a number greater than zero), print out the current value of the counter.\n(while (pos? @counter)\n (do\n (println @counter)\n (swap! counter dec)))\n
This example uses mutable state and causes a side effect by printing to the repl. Both these kinds of things are typically kept to a minimum in Clojure.
TODO An alternative would be to use use the iteration over a collection to control the while condition
"},{"location":"reference/clojure-syntax/defining-functions/","title":"Defining Functions","text":"clojure.core/defn
defines a custom function that can be called from anywhere in the current namespace by just using the name. A defined function can be called from where ever its namespace is required in other namespaces.
Here is a simple function definition that takes a number and divides it by two
(defn half-a-number\n \"Divide a given number by 2\"\n [number]\n (/ number 2))\n
Once you have defined a function, you can call it by using the function name as the first element of a list, ()
. Any other elements in the list are arguments passed to the function.
(half-a-number 4)\n
"},{"location":"reference/clojure-syntax/defining-functions/#understanding-the-defn-syntax","title":"Understanding the defn
syntax","text":"The standard form of defn
:
(defn name doc-string? attr-map? [params*] prepost-map? body)\n
name is a symbol used to call the function.
doc-string? is an optional string used to provide a meaningful description of the function definition. This description is the living documentation of the function and can be accessed via `clojure.repl/doc** functions and Clojure aware editors.
attr-map? is an optional map for pre-conditions for a function.
[params*] is a zero or more vector of symbols that represent the arguments passed to a function. The number of symbols defined must be matched when calling the function, or an exception will occur.
prepost-map? an optional map for post-conditions for a function.
body is the algorithm that will evaluate when the function is called.
There is a second form of the defn
function, one which responds differently based on the number of arguments used to call the function (polymorphic).
(defn name doc-string? attr-map?\n ([params*] prepost-map? body) + attr-map?)\n
Thinking Functionally - Polymorphism has examples of using defn to define polymorphic functions
"},{"location":"reference/clojure-syntax/defining-functions/#breaking-down-the-defn-syntax","title":"Breaking down the defn syntax","text":"The syntax defn
is what we call a macro, it is a simpler way to write clojure code that does the same thing.
You can think of defining a function with defn
as two steps
- Give the function a name - using the
def
syntax - Define the functions behaviour and arguments it takes - using the
fn
syntax
Here is the same function if you typed it out in full
(def half-a-number\n (fn [number]\n (/ number 2)))\n
"},{"location":"reference/clojure-syntax/defining-functions/#hintmacroexpand-functions","title":"Hint::Macroexpand functions","text":"The macroexpand-1
function takes an expression that includes one or more macros and returns the expanded version of Clojure code. The macroexpand-all
will also expand macros into Clojure code, doing so recursively for all macros it finds.
Clojure editors also provide evaluation functions that will macroexpand.
"},{"location":"reference/clojure-syntax/global-definitions/","title":"Global definitions","text":"Fixme work in progress
"},{"location":"reference/clojure-syntax/java-interop/","title":"Java Interoperability","text":"Clojure provides very clear and simple syntax for Java interoperability, using the following functions
import
- add functions from the Java library into the current namespace new
- create a new Java object .
- is the short form of the new
function
As Clojure is hosted on the Java Virtual Machine (JVM), its very easy to include libraries from any other languages that runs on the JVM, for example Java, Groovy, Scala, Jython, JRuby, Jaskell, etc.
The Leiningen build tool provides a simple way to include libraries as dependencies, using the :dependencies
section of the project.clj
file. Any library published to Maven Central is available for download by Leiningen, as both Maven Central and Clojars.org repositories are used by default.
java.lang included
Clojure projects and REPL environments include the java.lang
library automatically. Any methods from that library can be used without having to import
them or include any dependencies
"},{"location":"reference/clojure-syntax/java-interop/#the-syntax","title":"The syntax","text":"Its very easy to call Java methods and objects from clojure using the following syntax
(.instanceMember instance args*)\n(.instanceMember Classname args*)\n(.-instanceField instance)\n(Classname/staticMethod args*)\nClassname/staticField\n
Note Use the instanceMember .toUpperCase to convert a string from lower case to upper case
Call the .toUpperCase
function on any string you like, for example
(.toUpperCase \"I was low, but now I'm up\")\n
The string passed as an argument should now be all uppercase: \"I WAS LOW, BUT NOW I'M UP\"
Note Use the staticField Math/PI
to return the approximate value of Pi
You can treat any static field like any name defined in your Clojure code, so when evaluated the static field simply returns the value it represents
In this case the Math/PI
static field simply returns the approximate value of Pi that fits into a java.lang.Double type.
Math/PI\n-> 3.141592653589793\n
"},{"location":"reference/clojure-syntax/java-interop/#getting-the-java-environment","title":"Getting the Java environment","text":"Earlier we used Clojure functions to find information about our environment. We can also used the getProperty()
method from the java.lang.System
object to ask for the java version and jvm name.
Note Get version of Java & the JVM, returning those values as a meaningful string. Then get the version of the Clojure project
(str \"You are running Java version \" (System/getProperty \"java.version\") \"with the JVM\" (System/getProperty \"java.vm.name\"))\n\n(str \"Latest project version: \" (System/getProperty \"playground.version\"))\n
Note Use System/getenv
to return your system's environment variables as a map
(System/getenv)\n
You may notice that this is a map data structure that we return, so we can use use destructuring or the maps behaviour itself to pull out information.
Hint A full list of properties can be seen in the getProperty() documentation
There are more examples of Java Interoperability in the next few sections.
"},{"location":"reference/clojure-syntax/local-bindings/","title":"Local Bindings","text":"Fixme work in progress
"},{"location":"reference/clojure-syntax/more-java-fun/","title":"More Java fun","text":"Lets try some more examples to show how easy it is to use Java methods and objects. Remember that everything in java.lang is available in your Clojure project by default
"},{"location":"reference/clojure-syntax/more-java-fun/#returning-specific-types","title":"Returning specific types","text":"Clojure has types, after all it runs on the JVM and included java.lang
library in ever project. Types are inferred at runtime, saving you the need to design types yourself.
Sometimes you want to ensure a value is of a particular type and you can use Java to do this.
Note Return a string value as an integer
When you create a new java.lang.Integer
object you can provide a default value by passing either a number or string type.
(new Integer \"123\")\n\n;; Or the more common short-hand forms\n\n(Integer. \"123\")\n
This is the equivalent to the Java code:
Integer myInt = new Integer(\"123\");\n
The .
function essentially instantiates a new object from the class, in this case Integer
, passing any arguments to its constructor.
Hint Example: converting the port number read from an environment variable as a string which needs to be passed to the Jetty server as a number. See the Clojure Webapp workshop an example.
More on types in the section a quick look at types
fixme The following is work in progress
"},{"location":"reference/clojure-syntax/more-java-fun/#using-java-date","title":"Using Java date","text":"Note Use java.util.Date
to explore date and time
(import java.util.Date)\n\n(Date.)\n\n(def now (Date.))\n\n(str now)\n
Its easy to create a local reference to a Java Date object instance and then call methods on that date object
(let [date (java.util.Date.)] (.getHours date))\n
Or using the threading macro, we can make the code a little clearer
(->\n (java.util.Date.)\n (.getHours))\n
"},{"location":"reference/clojure-syntax/more-java-fun/#its-joda-time","title":"Its Joda Time","text":"clj-time
is a Clojure wrapper for Joda time. As this is an external library, you need to add it to your project.clj file as a dependency. To find the latest version, check the clj-time library on Clojars.org
Note Add the clj-time dependency to your project (restart needed), require the clj-time library in your code and use the functions now
, formatters
& unparse
(require '[clj-time.core :as time])\n(require '[clj-time.format :as time-format])\n\n(time/now)\n\n;; ISO 8601 UTC format\n(def time-formatter (time-format/formatters :basic-date-time))\n(time-format/unparse custom-formatter (date-time 2010 10 3))\n
"},{"location":"reference/clojure-syntax/more-java-fun/#swing-coding","title":"Swing coding","text":"Swing GUI coding in Java feels quite messy to me, however using Swing in Clojure feels much cleaner. Using the doto
function allow you to chain function (Java method) calls together.
Note Start with the import
function to add the necessary swing libraries. Then create a button and add it to a panel, adding that panel to a frame.
(import '(javax.swing JFrame JPanel JButton))\n(def button (JButton. \"Click Me!\"))\n(def panel (doto (JPanel.)\n (.add button)))\n(def frame (doto (JFrame. \"Hello Frame\")\n (.setSize 200 200)\n (.setContentPane panel)\n (.setVisible true)))\n
Let\u2019s make our button show a message using an JOptionPane/showMessageDialog widget
(import 'javax.swing.JOptionPane)\n(defn say-hello []\n (JOptionPane/showMessageDialog\n nil \"Hello, World!\" \"Greeting\"\n JOptionPane/INFORMATION_MESSAGE))\n
To connect this function to our button, write a class implementing the ActionListener interface. Clojure\u2019s proxy feature is the easiest way to do this:
(import 'java.awt.event.ActionListener)\n(def act (proxy [ActionListener] []\n (actionPerformed [event] (say-hello))))\n
act
is an instance of an anonymous class implementing the actionPerformed method, so attach this class as a listener the button
(.addActionListener button act)\n
Now evaluate the say-hello
function to see the new button in action.
(say-hello)\n
Hint Seesaw is a really nice library for swing development. Also talk a look at the Seesaw minesweeper series.
"},{"location":"reference/clojure-syntax/more-java-fun/#understanding-the-dot-special-form","title":"Understanding the dot special form","text":"Fixme This section onwards needs reworking
All of these examples (except java.lang.Math/PI) use macros which expand to use the dot special form. In general, you won't need to use the dot special form unless you want to write your own macros to interact with Java objects and classes. Nevertheless, here is each example followed by its macroexpansion:
(macroexpand-1 '(.toUpperCase \"By Bluebeard's bananas!\"))\n; => (. \"By Bluebeard's bananas!\" toUpperCase)\n\n(macroexpand-1 '(.indexOf \"Synergism of our bleeding edges\" \"y\"))\n; => (. \"Synergism of our bleeding edges\" indexOf \"y\")\n\n(macroexpand-1 '(Math/abs -3))\n; => (. Math abs -3)\n
You can think of the general form of the dot operator as:
(. object-expr-or-classname-symbol method-or-member-symbol optional-args*)\n
There are a few more details to the dot operator than that, and if you're interested in exploring it further you can look at clojure.org's documentation on Java interop.
Input/output involves resources, be they files, sockets, buffers, or whatever. Java has separate classes for reading a resource's contents, writings its contents, and for interacting with the resource's properties.
For example, the java.io.File class is used to interact with a file's properties. Among other things, you can use it to check whether a file exists, to get the file's read/write/execute permissions, and to get its filesystem path:
(let [file (java.io.File. \"/\")]\n (println (.exists file))\n (println (.canWrite file))\n (println (.getPath file)))\n; => true\n; => false\n; => /\n
Noticeably missing from this list of capabilities are reading and writing. To read a file, you could use the java.io.BufferedReader class or perhaps java.io.FileReader. Likewise, you can use the java.io.BufferedWriter or java.io.FileWriter class for writing. There are other classes available for reading and writing as well, and which one you choose depends on your specific needs. Reader and Writer classes all have the same base set of methods for their interfaces; readers implement read, close, and more, while writers implement append, write, close, and flush. So, Java gives you a variety of tools for performing IO. A cynical person might say that Java gives you enough rope to hang yourself, and if you find such a person I hope you give them just enough arms to hug them.
Either way, Clojure makes things easier for you. First, there's spit and slurp. Spit writes to a resource, and slurp reads from one. Here's an example of using them to write and read a file:
(spit \"/tmp/hercules-todo-list\"\n\"- wash hair\n - charm the multi-headed snake\")\n\n(slurp \"/tmp/hercules-todo-list\")\n\n; => \"- wash hair\n; => - charm the multi-headed snake\"\n
You can also use these functions with objects representing resources other than files. The next example uses a StringWriter, which allows you to perform IO operations on a string:
(let [s (java.io.StringWriter.)]\n (spit s \"- charm the multi-headed snake\")\n (.toString s))\n; => \"- charm the multi-headed snake\"\n
Naturally, you can also read from a StringReader with slurp:
(let [s (java.io.StringReader. \"- charm the multi-headed snake\")]\n (slurp s))\n; => \"- charm the multi-headed snake\"\n
Of course, you can also use the read and write methods for resources. It doesn't really make much of a difference which you use; spit and slurp are often convenient because they work with just a string representing a filesystem path or a URL.
The with-open macro is another convenience: it implicitly closes a resource at the end of its body. There's also the reader function, a nice utility which, according to the clojure.java.io api docs, \"attempts to coerce its argument to an open java.io.Reader.\" This is convenient when you don't want to use slurp because you don't want to try to read a resource in its entirety, and you don't want to figure out which Java class you need to use. You could use it along with with-open and the line-seq function if you're trying to read a file one line at a time:
(with-open [todo-list-rdr (clojure.java.io/reader \"/tmp/hercules-todo-list\")]\n (doseq [todo (line-seq todo-list-rdr)]\n (println todo)))\n; => \"- wash hair\n; => - charm the multi-headed snake\"\n
That should be enough for you to get started with IO in Clojure. If you're trying to do something more sophisticated, definitely take a look at the clojure.java.io docs, the java.nio.file package docs, or the java.io package docs. 5. Summary
In this chapter, you learned what it means for Clojure to be hosted on the JVM. Clojure programs get compiled to Java bytecode and executed within a JVM process. Clojure programs also have access to Java libraries, and you can easily interact with them using Clojure's interop facilities. 6. Resources
"},{"location":"reference/clojure-syntax/more-java-fun/#from-httpclojureorgjava_interop","title":"From http://clojure.org/java_interop","text":"(.instanceMember instance args*)\n(.instanceMember Classname args*)\n(.-instanceField instance)\n\n(.toUpperCase \"fred\")\n-> \"FRED\"\n(.getName String)\n-> \"java.lang.String\"\n(.-x (java.awt.Point. 1 2))\n-> 1\n(System/getProperty \"java.vm.version\")\n-> \"1.6.0_07-b06-57\"\nMath/PI\n-> 3.141592653589793\n
The preferred idiomatic forms for accessing field or method members are given above. The instance member form works for both fields and methods. The instanceField form is preferred for fields and required if both a field and a 0-argument method of the same name exist. They all expand into calls to the dot operator (described below) at macroexpansion time. The expansions are as follows:
(.instanceMember instance args*) ==> (. instance instanceMember args*)\n(.instanceMember Classname args*) ==>\n (. (identity Classname) instanceMember args*)\n(.-instanceField instance) ==> (. instance -instanceField)\n(Classname/staticMethod args*) ==> (. Classname staticMethod args*)\nClassname/staticField ==> (. Classname staticField)\n
The Dot special form
(. instance-expr member-symbol)\n(. Classname-symbol member-symbol)\n(. instance-expr -field-symbol)\n(. instance-expr (method-symbol args*)) or\n(. instance-expr method-symbol args*)\n(. Classname-symbol (method-symbol args*)) or\n(. Classname-symbol method-symbol args*)\n
Special form.
The '.' special form is the basis for access to Java. It can be considered a member-access operator, and/or read as 'in the scope of'.
If the first operand is a symbol that resolves to a class name, the access is considered to be to a static member of the named class. Note that nested classes are named EnclosingClass$NestedClass, per the JVM spec. Otherwise it is presumed to be an instance member and the first argument is evaluated to produce the target object.
If the second operand is a symbol and no args are supplied it is taken to be a field access - the name of the field is the name of the symbol, and the value of the expression is the value of the field, unless there is a no argument public method of the same name, in which case it resolves to a call to the method. If the second operand is a symbol starting with -, the member-symbol will resolve only as field access (never as a 0-arity method) and should be preferred when that is the intent.
If the second operand is a list, or args are supplied, it is taken to be a method call. The first element of the list must be a simple symbol, and the name of the method is the name of the symbol. The args, if any, are evaluated from left to right, and passed to the matching method, which is called, and its value returned. If the method has a void return type, the value of the expression will be nil. Note that placing the method name in a list with any args is optional in the canonic form, but can be useful to gather args in macros built upon the form.
Note that boolean return values will be turned into Booleans, chars will become Characters, and numeric primitives will become Numbers unless they are immediately consumed by a method taking a primitive.
The member access forms given at the top of this section are preferred for use in all cases other than in macros.
(.. instance-expr member+)\n(.. Classname-symbol member+)\nmember => fieldName-symbol or (instanceMethodName-symbol args*)\n
Macro. Expands into a member access (.) of the first member on the first argument, followed by the next member on the result, etc. For instance:
(.. System (getProperties) (get \"os.name\"))\n
expands to:
(. (. System (getProperties)) (get \"os.name\"))\n
but is easier to write, read, and understand. See also the -> macro which can be used similarly:
(-> (System/getProperties) (.get \"os.name\"))\n
(doto instance-expr (instanceMethodName-symbol args)) Macro. Evaluates instance-expr then calls all of the methods/functions with the supplied arguments in succession on the resulting object, returning it.
(doto (new java.util.HashMap) (.put \"a\" 1) (.put \"b\" 2))\n-> {a=1, b=2}\n
Note the above applies to the latest Clojure SVN revision. If you are using the 20080916 release only method calls are allowed, and the syntax is:
(doto (new java.util.HashMap) (put \"a\" 1) (put \"b\" 2))\n-> {a=1, b=2}\n
(Classname. args) (new Classname args)
Special form. The args, if any, are evaluated from left to right, and passed to the constructor of the class named by Classname. The constructed object is returned.
Alternative Macro Syntax
As shown, in addition to the canonic special form new, Clojure supports special macroexpansion of symbols containing '.':
(new Classname args*)\n
can be written
(Classname. args*)\n;; note trailing dot\n
the latter expanding into the former at macro expansion time.
(instance? Class expr) Evaluates expr and tests if it is an instance of the class. Returns true or false
(set! (. instance-expr instanceFieldName-symbol) expr) (set! (. Classname-symbol staticFieldName-symbol) expr) Assignment special form. When the first operand is a field member access form, the assignment is to the corresponding field. If it is an instance field, the instance expr will be evaluated, then the expr.
In all cases the value of expr is returned.
Note - you cannot assign to function params or local bindings. Only Java fields, Vars, Refs and Agents are mutable in Clojure.
(memfn method-name arg-names*) Macro. Expands into code that creates a fn that expects to be passed an object and any args and calls the named instance method on the object passing the args. Use when you want to treat a Java method as a first-class fn.
(map (memfn charAt i) [\"fred\" \"ethel\" \"lucy\"] [1 2 3]) -> (\\r \\h \\y)
Note it almost always preferable to do this directly now, with syntax like:
(map #(.charAt %1 %2) [\"fred\" \"ethel\" \"lucy\"] [1 2 3]) -> (\\r \\h \\y)
(bean obj) Takes a Java object and returns a read-only implementation of the map abstraction based upon its JavaBean properties.
(bean [[http://java.awt.Color/black|java.awt.Color/black]]) -> {:RGB -16777216, :alpha 255, :transparency 1, :class class java.awt.Color, :green 0, :blue 0, :colorSpace java.awt.color.ICC_ColorSpace@c94b51, :red 0}
Support for Java in Clojure Library Functions
Many of the Clojure library functions have defined semantics for objects of Java types. contains? and get work on Java Maps, arrays, Strings, the latter two with integer keys. count works on Java Strings, Collections and arrays. nth works on Java Strings, Lists and arrays. seq works on Java reference arrays, Iterables and Strings. Since much of the rest of the library is built upon these functions, there is great support for using Java objects in Clojure algorithms.
Implementing Interfaces and Extending Classes
Clojure supports the dynamic creation of objects that implement one or more interfaces and/or extend a class with the proxy macro. The resulting objects are of an anonymous class. You can also generate statically-named classes and .class files with gen-class. As of Clojure 1.2, reify is also available for implementing interfaces.
( proxy [class-and-interfaces] [args] fs+) class-and-interfaces - a vector of class names args - a (possibly empty) vector of arguments to the superclass constructor. f => (name [params] body) or (name ([params] body) ([params+] body) ...)
Macro
Expands to code which creates a instance of a proxy class that implements the named class/interface(s) by calling the supplied fns.
A single class, if provided, must be first. If not provided it defaults to Object.
The interfaces names must be valid interface types. If a method fn is not provided for a class method, the superclass method will be called.
If a method fn is not provided for an interface method, an UnsupportedOperationException will be thrown should it be called.
Method fns are closures and can capture the environment in which proxy is called. Each method fn takes an additional implicit first argument, which is bound to this.
Note that while method fns can be provided to override protected methods, they have no other access to protected members, nor to super, as these capabilities cannot be a proxy.
Arrays
Clojure supports the creation, reading and modification of Java arrays. It is recommended that you limit use of arrays to interop with Java libraries that require them as arguments or use them as return values.
Note that many other Clojure functions work with arrays such as via the seq library. The functions listed here exist for initial creation of arrays, or to support mutation or higher performance operations on arrays.
Create array from existing collection: aclone amap to-array to-array-2d into-array Multi-dimensional array support: aget aset to-array-2d make-array Type-specific array constructors: boolean-array byte-array char-array double-array float-array int-array long-array object-array short-array Primitive array casts: booleans bytes chars doubles floats ints longs shorts Mutate an array: aset Process an existing array: aget alength amap areduce
Type Hints
Clojure supports the use of type hints to assist the compiler in avoiding reflection in performance-critical areas of code. Normally, one should avoid the use of type hints until there is a known performance bottleneck. Type hints are metadata tags placed on symbols or expressions that are consumed by the compiler. They can be placed on function parameters, let-bound names, var names (when defined), and expressions:
(defn len [x] (.length x))
(defn len2 [^String x] (.length x))
user=> (time (reduce + (map len (repeat 1000000 \"asdf\")))) \"Elapsed time: 3007.198 msecs\" 4000000 user=> (time (reduce + (map len2 (repeat 1000000 \"asdf\")))) \"Elapsed time: 308.045 msecs\" 4000000
Once a type hint has been placed on an identifier or expression, the compiler will try to resolve any calls to methods thereupon at compile time. In addition, the compiler will track the use of any return values and infer types for their use and so on, so very few hints are needed to get a fully compile-time resolved series of calls. Note that type hints are not needed for static members (or their return values!) as the compiler always has the type for statics.
There is a warn-on-reflection flag (defaults to false) which will cause the compiler to warn you when it can't resolve to a direct call:
(set! warn-on-reflection true) -> true
(defn foo [s] (.charAt s 1)) -> Reflection warning, line: 2 - call to charAt can't be resolved. -> #user/foo
(defn foo [^String s] (.charAt s 1)) -> #user/foo
For function return values, the type hint can be placed before the arguments vector:
(defn hinted (^String []) (^Integer [a]) (^java.util.List [a & args]))
-> #user/hinted
Aliases
Clojure provides aliases for primitive Java types and arrays which do not have typical representations as Java class names. For example, long arrays (long-array []) have a type of \"[J\".
int - A primitive int\nints - An int array\nlong - A primitive long\nlongs - A long array\nfloat - A primitive float\nfloats - A float array\ndouble - A primitive double\ndoubles - A double array\nvoid - A void return\nshort - A primitive short\nshorts - A short array\nboolean - A primitive boolean\nbooleans - A boolean array\nbyte - A primitive byte\nbytes - A byte array\nchar - A primitive character\nchars - A character array\n
Support for Java Primitives
Clojure has support for high-performance manipulation of, and arithmetic involving, Java primitive types in local contexts. All Java primitive types are supported: int, float, long, double, boolean, char, short, and byte.
let/loop-bound locals can be of primitive types, having the inferred, possibly primitive type of their init-form.\nrecur forms that rebind primitive locals do so without boxing, and do type-checking for same primitive type.\nArithmetic (+,-,*,/,inc,dec,<,<=,>,>= etc) is overloaded for primitive types where semantics are same.\naget/aset are overloaded for arrays of primitives\naclone, alength functions for arrays of primitives\nconstructor functions for primitive arrays: float-array, int-array, etc.\nType hints for primitive arrays - ^ints, ^floats, etc.\nCoercion ops int, float, etc. produce primitives when consumer can take primitive\nThe num coercion function boxes primitives to force generic arithmetic\nArray cast functions ints longs, etc. which produce int[], long[], etc.\nA set of \"unchecked\" operations for utmost performing, but potentially unsafe, integer (int/long) ops: unchecked-multiply unchecked-dec unchecked-inc unchecked-negate unchecked-add unchecked-subtract unchecked-remainder unchecked-divide\nA dynamic var to automatically swap safe operations with unchecked operations: *unchecked-math*\namap and areduce macros for functionally (i.e. non-destructively) processing one or more arrays in order to produce a new array or aggregate value respectively.\n
Rather than write this Java:
static public float asum(float[] xs){ float ret = 0; for(int i = 0; i < xs.length; i++) ret += xs[i]; return ret; }
you can write this Clojure:
(defn asum [^floats xs] (areduce xs i ret (float 0) (+ ret (aget xs i))))
and the resulting code is exactly the same speed (when run with java -server).
The best aspect of this is that you need not do anything special in your initial coding. Quite often these optimizations are unneeded. Should a bit of code be a bottleneck, you can speed it up with minor adornment:
(defn foo [n] (loop [i 0] (if (< i n) (recur (inc i)) i)))
(time (foo 100000)) \"Elapsed time: 0.391 msecs\" 100000
(defn foo2 [n] (let [n (int n)] (loop [i (int 0)] (if (< i n) (recur (inc i)) i))))
(time (foo2 100000)) \"Elapsed time: 0.084 msecs\" 100000
Coercions At times it is necessary to have a value of a particular primitive type. These coercion functions yield a value of the indicated type as long as such a coercion is possible: bigdec bigint boolean byte char double float int long num short
Some optimization tips
All arguments are passed to Clojure fns as objects, so there's no point to putting non-array primitive type hints on fn args. Instead, use the let technique shown to place args in primitive locals if they need to participate in primitive arithmetic in the body.\n(let [foo (int bar)] ...) is the correct way to get a primitive local. Do not use ^Integer etc.\nDon't rush to unchecked math unless you want truncating operations. HotSpot does a good job at optimizing the overflow check, which will yield an exception instead of silent truncation. On a typical example, that has about a 5% difference in speed - well worth it. Also, people reading your code don't know if you are using unchecked for truncation or performance - best to reserve it for the former and comment if the latter.\nThere's usually no point in trying to optimize an outer loop, in fact it can hurt you as you'll be representing things as primitives which just have to be re-boxed in order to become args to the inner call. The only exception is reflection warnings - you must get rid of them in any code that gets called frequently.\nAlmost every time someone presents something they are trying to optimize with hints, the faster version has far fewer hints than the original. If a hint doesn't improve things in the end - take it out.\nMany people seem to presume only the unchecked- ops do primitive arithmetic - not so. When the args are primitive locals, regular + and * etc do primitive math with an overflow check - fast and safe.\nSo, the simplest route to fast math is to leave the operators alone and just make sure the source literals and locals are primitive. Arithmetic on primitives yields primitives. If you've got a loop (which you probably do if you need to optimize) make sure the loop locals are primitives first - then if you accidentally are producing a boxed intermediate result you'll get an error on recur. Don't solve that error by coercing your intermediate result, instead, figure out what argument or local is not primitive.\n
Simple XML Support Included with the distribution is simple XML support, found in the src/xml.clj file. All names from this file are in the xml namespace.
(parse source) Parses and loads the source, which can be a File, InputStream or String naming a URI. Returns a tree of the xml/element struct-map, which has the keys :tag, :attrs, and :content. and accessor fns tag, attrs, and content.
(xml/parse \"/Users/rich/dev/clojure/build.xml\") -> {:tag :project, :attrs {:name \"clojure\", :default \"jar\"}, :content [{:tag :description, ...
Calling Clojure From Java The clojure.java.api package provides a minimal interface to bootstrap Clojure access from other JVM languages. It does this by providing:
- The ability to use Clojure's namespaces to locate an arbitrary var, returning the var's clojure.lang.IFn interface.
- A convenience method read for reading data using Clojure's edn reader
IFns provide complete access to Clojure's APIs. You can also access any other library written in Clojure, after adding either its source or compiled form to the classpath.
The public Java API for Clojure consists of the following classes and interfaces:
clojure.java.api.Clojure\nclojure.lang.IFn\n
All other Java classes should be treated as implementation details, and applications should avoid relying on them.
To lookup and call a Clojure function:
IFn plus = Clojure.var(\"clojure.core\", \"+\"); plus.invoke(1, 2);
Functions in clojure.core are automatically loaded. Other namespaces can be loaded via require:
IFn require = Clojure.var(\"clojure.core\", \"require\"); require.invoke(Clojure.read(\"clojure.set\"));
IFns can be passed to higher order functions, e.g. the example below passes plus to read:
IFn map = Clojure.var(\"clojure.core\", \"map\"); IFn inc = Clojure.var(\"clojure.core\", \"inc\"); map.invoke(inc, Clojure.read(\"[1 2 3]\"));
Most IFns in Clojure refer to functions. A few, however, refer to non-function data values. To access these, use deref instead of fn:
IFn printLength = Clojure.var(\"clojure.core\", \"print-length\"); IFn deref = Clojure.var(\"clojure.core\", \"deref\"); deref.invoke(printLength);
"},{"location":"reference/clojure-syntax/naming/","title":"Naming","text":""},{"location":"reference/clojure-syntax/naming/#naming-when-requiring-other-namespaces","title":"Naming when requiring other namespaces","text":"(require [cheshire.core :refer :all])
is an example of self-inflicted errors, as this library included a contains?
function that will over-write the clojure.core/contains?
function when using :refer :all
or the (use )
expression.
This situation is one example of why :refer :all
and use
are not recommended and can cause lots of debugging headaches.
If a namespace is predominantly about using a specific library, then refer specific functions as they are used within the current namespace
(ns current.namespace\n(:require\n [cheshire.core :refer [function-name another-function etc]))\n
A classic example is a test namespace that uses clojure core (ns practicalli.random-function-test (:require [clojure.test :refer [deftest is testing]] [practicalli.random-function :as sut])) Otherwise use a meaningful alias, ideally referring to what that library is doing (which makes it easer to swap out with a different library later on if required). As Cheshire is a JSON related library, then (ns my.ns (:require [cheshire.core :as json])) This gives a context to all the functions called from that library and makes code easier for humans to understand.
"},{"location":"reference/clojure-syntax/naming/#hintclj-kondo-lint-tool-shows-unused-functions","title":"Hint::clj-kondo lint tool shows unused functions","text":"Using clj-kondo
"},{"location":"reference/clojure-syntax/numbers-maths/","title":"Maths","text":"Fixme Split this into sections ?
Writing some simple mathematics helps you get used to the form of Clojure. Unlike other languages, Clojure does not have operators for mathematics. Instead + - * /
are all functions in their own right.
As Clojure uses pre-fix notation then mathematical expressions are always unambiguous. There is no need for an operator precedence table in Clojure.
Note Write some simple math to help you get used to the form of Clojure
(+ 1 2 3 4 5 6 7)\n(- 2 1)\n(* 3 7)\n(/ 12 4)\n(/ 500 20)\n(+ 1 1 2489 459 2.)\n(+ 1 2 (* 3 4) (- 5 6 -7))\n
"},{"location":"reference/clojure-syntax/numbers-maths/#variable-numbers-of-arguments","title":"Variable numbers of arguments","text":"Mathematic functions show the flexibility of Clojure, as they take a variable number of arguments (variadic functions). Its common for Clojure functions to have zero, one or many arguments (many arguments typically represented as a built-in data structure (map, vector, set or list)
Note Write some more maths to show the variadic nature of mathematic (and manu other) functions
(+)\n(*)\n(* 2)\n(+ 4)\n\n(+ 1 2 3)\n(< 1 2 3)\n(< 1 3 8 4)\n
Note Explore some number related functions
(rem 22 7)\n(mod 20 12)\n(quot 13 4)\n\n(inc 3)\n(dec 4)\n\n(min 1 2 3 5 8 13)\n(max 1 2 3 5 8 13)\n\n(repeat 4 9)\n\n(range 10)\n(range 18 66)\n(range 2 99 2)\n
"},{"location":"reference/clojure-syntax/numbers-maths/#equality","title":"Equality","text":"Equality is represented by the =
function. Yes, =
is a proper function too, not just an operator as with other languages.
Note Explore what equality means in Clojure. Equality is very useful when your data structures are immutable
(= 1 1)\n(= 2 1)\n\n(identical? \"foo\" \"bar\")\n(identical? \"foo\" \"foo\")\n(= \"foo\" \"bar\")\n(= \"foo\" \"foo\")\n\n(identical? :foo :bar)\n(identical? :foo :foo)\n\n(true)\n(false)\n(not true)\n(true? (= 1 1))\n(false (= 1 -1))\n
Equality is very efficient when your data structures are immutable. For example if you have very large data sets, you can simply compare a hash value to see if those data structures are the same.
Of course you also have the not
function for reversing logic too
(not true)\n\n=> false\n
"},{"location":"reference/clojure-syntax/numbers-maths/#boolean-true-and-false","title":"Boolean - True and False","text":";; some truthiness with math functions for you to try
(+)\n(class (+))\n(*)\n(true? +)\n(false? +)\n(true? *)\n(false? *)\n(true? 1)\n(true? -1)\n(true? true)\n(- 2)\n
"},{"location":"reference/clojure-syntax/numbers-maths/#boolean-predicates","title":"Boolean & Predicates","text":"Predicates are functions that take a value and return a boolean result (true | false)
(true? true)\n(true? (not true))\n(true? false)\n(true? (not false))\n(true? nil)\n
"},{"location":"reference/clojure-syntax/numbers-maths/#types","title":"Types","text":"Clojure uses Java's object types for booleans, strings and numbers. Use the class
function to inspect them.
(class 1)\n; Integer literals are java.lang.Long by default\n(class 1.1) ; Float literals are java.lang.Double\n\n(class \"\")\n; Strings always double-quoted, and are java.lang.String\n\n(class false) ; Booleans are java.lang.Boolean\n(class nil) ; The \"null\" value is called nil\n\n(class (list 1 2 3 4))\n\n\n(class true)\n(class ())\n(class (list 1 2 34 5))\n(class (str 2 3 4 5))\n(class (+ 22/7))\n(class 5)\n(class \"fish\")\n(type [1 2 3])\n(type {:a 1 :b 2})\n\n(type (take 3 (range 10)))\n
"},{"location":"reference/clojure-syntax/numbers-maths/#ratios","title":"Ratios","text":"To help maintain the precision of numbers, Clojure has a type called Ratio. So when you are dividing numbers you can keep the as a fraction using whole numbers, rather than constrain the result to a approximate
(/ 2)\n
A classic example is dividing 22 by 7 which is approximately the value of Pi
(/ 22 7)\n\n(class (/ 22 7))\n
If you want to force Clojure to evaluate this then you can specify one of the numbers with a decimal point
(class (/ 22 7.0))\n
"},{"location":"reference/clojure-syntax/parenthesis/","title":"Parenthesis - defining the structure of Clojure code","text":"Clojure uses parenthesis, round brackets ()
, as a simple way to define the structure of the code and provide clear and unambiguous scope. This structure is the syntax of symbolic expressions.
Parenthesis, or parens for short, are used to define and call functions in our code, include libraries and in fact any behavior we wish to express.
Clojure includes 3 other bracket types to specifically identify data: '()
quoted lists and sequences, []
for vectors (arrays) and argument lists, {}
for hash-maps and #{}
for sets of data.
No other terminators or precedence rules are required to understand how to read and write Clojure.
"},{"location":"reference/clojure-syntax/parenthesis/#the-parenthesis-hangup","title":"The Parenthesis hangup","text":"Some raise the concern that there are \"too many brackets\" in Clojure.
Clojure doesn't require any additional parens compared to other languages, it simply moves the open parens to the start of the expression giving a clearly defined structure to the code
With support for higher order functions, functional composition and threading macros, Clojure code typically uses fewer parens than other languages especially as the scope of the problem space grows.
All languages use parens to wrap a part of an expression, requiring additional syntax to identify the boundaries of each expression so it can be parsed by humans and computers alike. Clojure uses a single way to express everything, homoiconicity, where as most other languages require additional syntax for different parts of the code.
Using parens, Clojure has a well defined structure that provides a clearly defined scope to every part of the code. There is no requirement to remember a long list of ad-hoc precedence rules, e.g. JavaScript operator precedence.
This structure of Clojure code is simple to parse for both humans and computers. it is simple to navigate, simple to avoid breaking the syntax of the language and simple to provide tooling that keeps the syntax of the code correct.
After realising the simplicity that parens bring, you have to wonder why other (non-lisp) languages made their syntax more complex.
"},{"location":"reference/clojure-syntax/parenthesis/#working-with-parens","title":"Working with Parens","text":"Clojure aware editors all support structured editing to manage parens and ensure they remain balanced (same number of open and close parens).
A developer only needs to type the open paren and the editor will automatically add the closing paren.
Parens cannot be deleted unless their content is empty.
Code can be pulled into parens (slurp) or pushed out of parens (barf). Code can be split, joined, wrapped, unwrapped, transposed, convoluted and raised, all without breaking the structure.
- Smartparens for Structural editing - a modern update of ParEdit
- The animated guide to ParEdit
"},{"location":"reference/clojure-syntax/parenthesis/#homoiconicity-and-macros","title":"Homoiconicity and Macros","text":"Clojure is a dialect of LISP and naturally was designed to be a homoiconic language. This means the syntax for behavior and data is the same. This greatly simplifies the syntax of Clojure and all LISP style languages.
The Clojure Reader is a parser that reads in data structures as expression, rather than parsing of text required by other languages. The result of parsing is a collection of data structures that can be traversed (asymmetric syntax tree - AST). Compared to most languages the compiler does very little and you can consider Clojure really does not have a syntax.
Code is written as data structures that are accessible to the other parts of the code, providing a way to write code that manipulate those data structures and generate new code. In Clojure this type of code is called a macro, a piece of code that writes new code.
None of this would work as simply as it does without using parens and the symbolic expression syntax.
- Inspired by Beating the Averages by Paul Graham
"},{"location":"reference/clojure-syntax/parenthesis/#example-function-invocation","title":"Example: Function invocation","text":"The choice was made early in the design of Lisp that lists would be used for function invocation in the form:
(function arg1 arg2 arg3)\n;; => value returned\n
The advantages of this design are:
- a function call is just one expression, called a \"form\"
- function calls can be constructed (cons function-symbol list-of-args)
- functions can be arguments to other functions (higher order functions)
- simple syntax to parse - everything between two parentheses is a self-contained expression.
- fewer parens due to high order functions, composition and threading macros
The function name could have been put outside the parentheses:
function (arg1 arg2 arg3) => some result\n
This design has many disadvantages:
- a function call is no longer a single form and have to pass the function name and the argument list.
- syntax is complex and requires additional syntax rules to define a function call
- code generation is very complex
- same number of parens as Clojure or possibly more (no direct support for higher order functions)
"},{"location":"reference/clojure-syntax/private-functions/","title":"Private functions","text":"Fixme work in progress
"},{"location":"reference/clojure-syntax/quick-look-at-types/","title":"A quick look at types","text":"As we mentioned before, underneath Clojure lurks Java byte code so there are going to be types in Clojure. However, Clojure being a dynamic language, most of the time you can just let Clojure manage the types for you.
Hint When you run Clojure on a different host platform, eg. .Net or Javascript (via Clojurescript), Clojure will use the types of that host platform.
Should you want to know the type of something you are working on, you can use two functions, type
and class
.
Note Discover the class or type of some common Clojure code
(class 1)\n(class 1.1)\n(class \"\")\n(class true)\n(class false)\n(class nil)\n\n(class ())\n(class (list 1 2 3 4))\n(class (str 2 3 4 5))\n(class (+ 22/7))\n\n(type [1 2 3])\n(type {:a 1 :b 2})\n(type (take 3 (range 10)))\n
Hint If you cant live without static type checking, look at core.typed, a type system for Clojure all in one library
"},{"location":"reference/clojure-syntax/ratios/","title":"Ratios","text":"In mathematics you need to ensure that you manage precision of your calculations when you are dividing numbers. Once you create a decimal number then everything it touches had a greater potential to becoming a decimal.
Note Calculate a rough approximation to Pi by dividing 22 by 7
(/ 22 7)\n(class (/ 22 7))\n(/ (* 22/7 3) 3)\n
If the result of an integer calculation would be a decimal number, then Clojure holds the value as a Ratio. This is one example of lazy evaluation. Rather than calculate the decimal value at some particular precision (number of decimal points). Clojure is saving the calculation until its needed, at which time the specific precision required should be known.
Note Explore the ratio type further and see how to get a decimal value as the result
(/ 14 4)\n(/ 16 12)\n(/ 2)\n(/ 22 7.0)\n(type (/ 22 7.0))\n(float (/ 22 7))\n(double (/ 22 7))\n
When one or more of the numbers in the division is a decimal, then Clojure will return a decimal value. Or you can coerce a value to a specific decimal type, eg. float or double.
"},{"location":"reference/clojure-syntax/strings/","title":"Strings","text":"Strings in Clojure are actually Java Strings.
Hint Why do you think this design decision was taken for Clojure?
If you think about the state property of String objects, then you realise that String Objects are immutable and cannot be changed. As this is the default approach for other data structures and values in Clojure it makes sense to use Java Strings instead of writing a Clojure implementation.
As Clojure strings are Java strings, then you can use all the same functions you can in Java.
Note Use the Java function println
to output a string
(println \"Hello, whats different with me? What value do I return\")\n
Something different happens when you evaluate this expression. The actual value returned is nil
, not the string. You see the string because println is writing to the console (i.e the REPL).
Hint Avoid code that creates side-effects where possible to keep your software less complex to understand.
You may be used to using println statements to help you debug your code, however, with the fast feedback you get from developing in the REPL then there is usually no need for them.
"},{"location":"reference/clojure-syntax/strings/#strings-the-clojure-way","title":"Strings the Clojure way","text":"Its more common to use the str
function when working with strings, as this function returns the string as its. value when evaluated.
(str \"Hello, I am returned as a value of this expression\")
Note Join strings together with the function str
(str \"I\" \"like\" \"to\" \"be\" \"close\" \"together\"\n(str \"Hello\" \", \" \"Devoxx UK\")\n(str \"Hello \" \"developers\" \", \" \"welcome\" \" \" \"to\" \" \" \"HackTheTower UK\")\n
You can see that there are no side-effects when using str
and the string is returned as the value of the function call.
"},{"location":"reference/clojure-syntax/strings/#using-interpose-with-strings","title":"Using Interpose with Strings","text":"Its easy to join strings together with the str
function, however str
leaves no spaces between words.
"},{"location":"reference/clojure-syntax/strings/#using-regex","title":"Using Regex","text":""},{"location":"reference/clojure-syntax/strings/#java-interop-for-strings","title":"Java Interop for Strings","text":"Note Change the case of strings and other common actions using the String object methods, in the form (.methodName object)
(.toUpperCase \"show me the money\")\n\n(.getName String)\n\n(.indexOf \"Where is the $ in this string\" \"$\")\n
Hint Look at the API docs for java.lang.String for other methods you can call.
"},{"location":"reference/clojure-syntax/syntax/","title":"Clojure syntax","text":"Clojure is perceived as having an abundance of ()
, the symbols that represent a list.
As Clojure is a LISP (List Processing) language then everything is written in the form of a list. This makes Clojure very powerful and also easier to read.
Using a list structure also demonstrates the data-centric nature of Clojure. Every item in the list has a value, with the first item evaluated by a function call.
"},{"location":"reference/clojure-syntax/syntax/#hintparens-everywhere","title":"Hint::Parens everywhere","text":"The seemingly abundance of ()
can be confusing until its realized there are fewer \"special characters\" in Clojure than other languages. Clojure aware editors support matching parens, adding a closed paren when typing an open paren, ensuring it is easy to write correctly formed Clojure.
Syntax differences are a trivial reason to avoid trying Clojure. Syntax aware editors significantly reduce typing by automatically closing parenthesis and eliminating errors due to missing delimiters (ie. no more errors due to missing ; in C-based languages)
"},{"location":"reference/clojure-syntax/syntax/#prefix-notation","title":"Prefix notation","text":"Instead of having a mix of notations like in many other languages, Clojure uses pre-fix notation entirely.
In Clojure operators are applied uniformly and there is no room for ambiguity:
(+ 1 2 3 5 8 13 21)\n (+ 1 2 (- 4 1) 5 (* 2 4) 13 (/ 42 2))\n (str \"Clojure\" \" uses \" \"prefix notation\")\n
In Java and other C-based languages you have to explicitly add operators everywhere and there can be a mixture of notations
(1 + 2 + 3 + 5 + 8 + 13 + 21);\n (1 + 2 + (- 4 1) + 5 + (* 2 4) + 13 + (/ 42 2));\n StringBuffer description = new StringBuffer(\"C-based languages\" + \" mix \" + \"notation\");\n x+=1;\n x++;\n x--;\n x+=y;\n x-=y;\n x*=y;\n x/=y;\n
"},{"location":"reference/clojure-syntax/syntax/#references","title":"References","text":""},{"location":"reference/clojure-syntax/threading-syntactic-sugar/","title":"Threading Syntax Sugar","text":"The previous code is written in classic Lisp style. When you come to read Lisp, you start from the inside out. In this case you start with (slurp ...)
and what it returns is used as the argument to (read-string ...)
and so on...
In our minds we probably constructed the following basic algorithm:
- Get the contents of the project.clj file using
slurp
- Read the text of that file using read-string
- Select just the third string using nth 2 (using an index starting at 0)
Can we rewrite our Clojure code to fit the way we think?
"},{"location":"reference/clojure-syntax/threading-syntactic-sugar/#thread-first-macro","title":"Thread first macro","text":"Using the thread-first macro -> we can chain Clojure functions together with a terser syntax, passing the result of the first evaluation as the first argument to the next function and so on. Using this style, we can write code that matches the algorithm in our head.
To make this really simple lets create a contrived example of the threading macro. Here we use the str
function to join strings together. Each individual str
function joins its own strings together, passing the resulting string as the first argument to the next function.
(->\n (str \"This\" \" \" \"is\" \" \")\n (str \"the\" \" \" \"threading\" \" \" \"macro\")\n (str \"in\" \" \" \"action.\"))\n\n;; => \"This is the threading macro in action\"\n
"},{"location":"reference/clojure-syntax/threading-syntactic-sugar/#thread-last-macro","title":"Thread-last macro","text":"Using the thread-last macro, ->>, the result of a function is passed as the last argument of the next function call. So in another simple series of str function calls, our text comes out backwards.
(->>\n (str \" This\")\n (str \" is\")\n (str \" backwards\"))\n\n;; => backwards is This\"\n
"},{"location":"reference/clojure-syntax/threading-syntactic-sugar/#_1","title":"Threading Syntax Sugar","text":"Note Refactor the Clojure code using the thread-first macro
(->\n \"./project.clj\"\n slurp\n read-string\n (nth 2))\n
Hint The \"project.clj\" is a string, so when you evaluate it as an expression, it simply returns the same string. That string is then passed as an argument to any following functions.
Using the threading macro, the result of every function is passed onto the next function in the list. This can be seen very clearly using ,,, to denote where the value is passed to the next function
(->\n \"project.clj\"\n slurp ,,,\n read-string ,,,\n (nth ,,, 2))\n
Hint Commas in clojure are treated as whitespace, they are simply ignored when it comes to evaluating code. Typically commas are rarely used and only to help human readability of the code
Note Create a new map that contains the project configuration for the current project
(->> \"project.clj\"\n slurp\n read-string\n (drop 2)\n (cons :version)\n (apply hash-map)\n (def project-configs))\n\n;; Evaluate the new map defined as project\nproject\n
We pull out the map of project information using slurp
, tidy the text up using read-string
and drop the first two elements (defproject playground). This returns a list that we want to turn into a map, but first we need to add a key to the version number. Using the cons
function we can add an element to the start of the list, in this case the :version
keyword
Now we can successfully convert the list that is returned into a map, with balanced key-value pairs. Then we simply create a name for this new map, project-configs
, so we can refer to it elsewhere in the code.
Hint The slurp
function holds the contents of the whole file in memory, so it may not be appropriate for very large files. If you are dealing with a large file, consider wrapping slurp in a lazy evaluation or use Java IO (eg. java.io.BufferedReader
, java.io.FileReader.
). See the Clojure I/O cookbook and The Ins & Outs of Clojure for examples.
"},{"location":"reference/clojure-syntax/whats-my-environment/","title":"Whats my environment","text":"Clojure has symbols (names that point to values). Some of these symbols are built into the language and their names start (and usually end with) the *
character.
When symbols are evaluated they return the value that they point to.
Note Check the version of Clojure running in your REPL.
Enter the following code into the Clojure REPL:
*clojure-version*\n
The full clojure version can be used to check you are running a particular version, major or minor of Clojure core. This information is represented as a map containing :major
, :minor
, :incremental
and :qualifier
keys. Feature releases may increment :minor and/or :major, bugfix releases will increment :incremental. Possible values of :qualifier include \"GA\", \"SNAPSHOT\", \"RC-x\" \"BETA-x\"
Hint A map in Clojure is a built in data structure represented by { }
. A map is a key-value pair and there must be a value for every key for the map to be valid. Keys are often defined using :keyword
, a self-referential pointer that can be used to look up values in a map or other data structures.
"},{"location":"reference/clojure-syntax/whats-my-environment/#viewing-the-class-path","title":"Viewing the Class path","text":"Clojure compiles to Java bytecode that runs on the JVM, so that code needs to be available in the Java class path.
Note Look at the class path for your project
The directory where the Clojure compiler will create the .class files for the current project. All .class files must be on the class path otherwise the Clojure run time environment will not know they exist.
*compile-path*\n
"},{"location":"reference/clojure-syntax/whats-my-environment/#namespace","title":"Namespace","text":"A namespace in clojure is a way to separate functions and data structures into logical components (similar to Java packages). A clojure.lang.Namespace
object representing the current namespace.
Note Find out the current namespace
*ns*\n
"},{"location":"reference/clojure-syntax/whats-my-environment/#last-3-values-in-the-repl","title":"Last 3 values in the REPL","text":"You can also get the 3 most most recent values returned in the REPL.
Note Evaluate the following three expressions in the REPL, then pull out the last three results
(+ 1 2 3)\n\n(+ 1 2 (+ 1 2) (+ 2 3))\n\n(str \"Java will be fully functional in version \" (+ 10 (rand-int 20))\n
Now get the last three values returned in the REPL
(str *1 *2 *3)\n
Hint You can cycle through previous expressions entered into the REPL using the Shift-UpArrow
keyboard shortcut
"},{"location":"reference/clojure-syntax/syntax/","title":"Clojure Syntax","text":"The Clojure syntax is very small and is actually a data structure, defined as a list, ()
, with the first element of a list being a function call and all other elements arguments to that function.
Examples are editable (using an embedded REPL) so feel free to experiment and watch as the return value changes as you change the code. Reload the page if you want to reset all the code back to the starting point.
"},{"location":"reference/clojure-syntax/syntax/#edn-based-notation","title":"edn based notation","text":"The core Clojure syntax is defined in the extensible data notation (edn). edn demonstrates that Clojure code is defined as a series of data structures
Clojure adds an execution model on top of edn to make a programming language and is a super-set of edn.
edn is used as a data transfer format, especially for Datomic the Clojure transactional database
- A case for Clojure by James Reeves provides a great introduction to edn
"},{"location":"reference/clojure-syntax/syntax/#calling-functions","title":"Calling functions","text":"The first element in a list, ()
, is treated as a call to a function. The examples show how to call functions with multiple arguments.
(+ 1 2)\n
(+ 3 (* 2 (- 7 2) 4) (/ 16 4))\n
(str \"Clojure is \" (- 2020 2007) \" years old\")\n
(inc 1)\n
(map inc [1 2 3 4 5])\n
(filter odd? (range 11))\n
"},{"location":"reference/clojure-syntax/syntax/#hintprefix-notation-and-parens","title":"Hint::Prefix notation and parens","text":"Hugging code with ()
is a simple syntax to define the scope of code expressions. No additional ;
, ,
or spaces are required.
Treating the first element of a list as a function call is referred to as prefix notation, which greatly simplifies Clojure syntax. Prefix notation makes mathematical expressions completely deterministic, eliminating the need for operator precedence.
"},{"location":"reference/clojure-syntax/syntax/#understanding-functions","title":"Understanding functions","text":"Functions contain doc-strings describing what that function does. The doc
function returns the doc-string of a particular function. Most editors also support viewing of doc-strings as well as jumping to function definitions to view the source code
(doc doc)\n
"},{"location":"reference/clojure-syntax/syntax/#strongly-typed-under-the-covers","title":"Strongly typed under the covers","text":"Clojure is a dynamically typed language so types do not need to be explicitly defined, although type hints can be added for performance where required.
Clojure is strongly typed and everything is a type underneath, relative to the host platform (Clojure uses Java types, ClojureScript uses JavaScript types). The type of anything in Clojure can be returned using the type
function.
(type 42)\n;; (type {:hash \"data\" :map \"more data\"})\n
(type {:hash \"data\" :map \"more data\"})\n
"},{"location":"reference/clojure-syntax/syntax/#modeling-data-with-collection-types","title":"Modeling data with Collection types","text":"Clojure has 4 main collection types, all immutable (cannot change once created) and can contain any Clojure types.
(str \"lists used mainly \" (* 2 2) \" \" :code)\n
[0 \"indexed\" :array (* 2 2) \"random-access\"]\n
{:key \"value\" \"hash-map\" \"also referred to as dictionary\"}\n
#{1 2 3 4 \"unique\" \"set\" \"of\" \"values\" \"unordered\" (* 3 9)}\n
"},{"location":"reference/clojure-syntax/syntax/#hintpersistent-data-types","title":"Hint::Persistent data types","text":"To change data in Clojure new copies are created rather than changing existing values. The copies of data will share values from the original data that are common in both. This sharing is called persistent data types and enables immutable data to be used efficiently.
"},{"location":"reference/clojure-syntax/syntax/#defining-names-for-values-vars","title":"Defining names for values (vars)","text":"Names can be bound to any values, from simple values like numbers, collections or even function calls. Using def
is convenient way to create names for values that are shared in your code.
evaluating a name will return the value it is bound to.
(def public-health-data\n ({:date \"2020-01-01\" :confirmed-cases 23014 :recovery-percent 15}\n {:date \"2020-01-02\" :confirmed-cases 23014 :recovery-percent 15}\n {:date \"2020-01-03\" :confirmed-cases 23014 :recovery-percent 15}))\n\npublic-health-data\n
"},{"location":"reference/clojure-syntax/syntax/#hintdef-for-shared-values-let-for-locally-scoped-values","title":"Hint::def for shared values, let for locally scoped values","text":"let
function is used to bind names to values locally, such as within a function definition. Names bound with def
have namespace scope so can be used with any code in that namespace.
"},{"location":"reference/clojure-syntax/syntax/#using-data-structures","title":"Using data structures","text":"Using the map
and inc
function, increment all the numbers in a vector
(map inc [1 2 3 4 5])\n
The above map
function is roughly equivalent to the following expression
(conj [] (inc 1) (inc 2) (inc 3) (inc 4) (inc 5))\n
The conj
function creates a new collection by combining a collection and one or more values.
map
reduce
filter
are common functions for iterating through a collection / sequence of values
(map * [1 3 5 8 13 21] [3 5 8 13 21 34])\n
(filter even? [1 3 5 8 13 21 34])\n
(reduce + [31 28 30 31 30 31])\n
(empty? [])\n
"},{"location":"reference/clojure-syntax/syntax/#defining-custom-functions","title":"Defining custom functions","text":"(defn square-of\n \"Calculates the square of a given number\"\n [number]\n (* number number))\n\n(square-of 9)\n
Function definitions can also be used within other expressions, useful for mapping custom functions over a collection
(map (fn [x] (* x x)) [1 2 3 4 5])\n
"},{"location":"reference/clojure-syntax/syntax/#host-interoperability","title":"Host Interoperability","text":"The REPL in this web page is running inside a JavaScript engine, so JavaScript functions can be used from within ClojureScript code (ClojureScript is Clojure that runs in JavaScript environments).
In the box below, replace ()
with (js/alert \"I am a pop-up alert\")
()\n
JavaScript libraries can be used with ClojureScript, such as React.js
(defn concentric-circles []\n [:svg {:style {:border \"1px solid\"\n :background \"white\"\n :width \"150px\"\n :height \"150px\"}}\n [:circle {:r 50, :cx 75, :cy 75, :fill \"green\"}]\n [:circle {:r 25, :cx 75, :cy 75, :fill \"blue\"}]\n [:path {:stroke-width 12\n :stroke \"white\"\n :fill \"none\"\n :d \"M 30,40 C 100,40 50,110 120,110\"}]\n [:path {:stroke-width 12\n :stroke \"white\"\n :fill \"none\"\n :d \"M 75,75 C 50,90 50,110 35,110\"}]])\n
"},{"location":"reference/jvm/","title":"Reference: Java Virtual Machine","text":"Understand the configuration options for the Java Virtual machine (JVM) which Clojure is hosted upon.
Overview of tools for monitoring and profiling Clojure applications running on the JVM, to ensure effective running of Clojure applications in production.
- Common JVM Options - for development and deployment
- JVM Profiling tools - understand resources and help diagnose run-time problems
JDK_JAVA_OPTIONS
Environment Variable
JDK_JAVA_OPTIONS
is the official Environment Variable for setting options when calling java
, javac
and other Java commands to start running a Java Virtual Machine (Java version 9 onward).
"},{"location":"reference/jvm/#display-resources-available-to-the-jvm","title":"Display resources available to the JVM","text":"-XshowSettings:system
displays the resources the JVM believes it has access too when running any Java command and is a very simple diagnostic tool to start with.
See the environment resources available to the JVM without running a Clojure or Java application:
java -XshowSettings:system -version\n
Include -XshowSettings:system
when running any Java command to provide simple diagnostics, e.g. when running a Clojure Uberjar
java -XshowSettings:system -jar practicalli-service.jar\n
Print resources in Container systems
-XshowSettings:system
is especially useful for environments which may vary in resources available, such as containers (Docker, Kubernettes, etc.)
"},{"location":"reference/jvm/#jvm-option-types","title":"JVM option types","text":"-X
- nonstandard VM options
-XX
standard VM options
-XX
options are not checked for validity, so are ignored if the VM does not recognize the option. Options can therefore be used across different VM versions without ensuring a particular level of the VM.
-D
a system property for the application running on the JVM using a name=value
"},{"location":"reference/jvm/#java-modules","title":"Java Modules","text":"Java 9 introduced modules to move features out of JVM itself and include them as optional modules.
Before CLJS-2377 issue was resolved, ClojureScript (2017) depended on java.xml.bind.DataTypeConverter
. java.xml.bind package
was deprecated in Java 9 and moved to a non-default module.
At that time, compiling a ClojureScript project without adding the java.xml.bind module would return the error:
<Exception details>\n...\nCaused by: java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverter\n
clojure J--add-modules \"java.xml.bind\"
command line option will include the module
:jvm-opts [\"--add-modules\" \"java.xml.bind\"]
added to Clojure CLI deps.edn or Leiningen project.clj file will include the module.
-Djdk.launcher.addmods=java.xml.bind
added to the JAVA_TOOL_OPTIONS
environment variable (jdk.launcher.addmods
--add-modules
doesn\u2019t work in JAVA_TOOL_OPTIONS
)
"},{"location":"reference/jvm/#unified-logging-sub-system","title":"Unified Logging sub-system","text":"-Xlog
- JEP 158
"},{"location":"reference/jvm/#references","title":"References","text":" - Best practice for JVM Tuning with G1 GC
- Command Line Options - IBM SDK documentation
- Best HotSpot JVM Options and switches for Java 11 through Java 17
"},{"location":"reference/jvm/common-options/","title":"Common JVM Options","text":"Examples of commonly used options for any language on the Java Virtual Machine (JVM).
The JVM is excellent at self-optimising its performance. Introducing specific options should only be done if specific resource or performance issues have been identified.
Understanding memory usage has more options to diagnose out of memory errors, garbage collection pauses and JIT compilation
JDK_JAVA_OPTIONS
Environment Variable
JDK_JAVA_OPTIONS
is the official Environment Variable for setting options when calling java
, javac
and other Java commands to start running a Java Virtual Machine (Java version 9 onward).
"},{"location":"reference/jvm/common-options/#java-heap-size","title":"Java heap size","text":"Java Ergonomics should provide sensible default options. Performance analysis of the running code may show advantages of manually setting memory sizes.
Set the initial heap size if memory usage will quickly grow
-Xms
\u2013 start heap size for JVM, e.g. -Xms2048m
sets an initial heap size of 2 GB
-XX:InitialRAMPercentage=n
sets the initial heap as n
percentage of total RAM
Set the maximum heap size if usage is relatively high under normal conditions
-Xmx
\u2013 maximum heap size of JVM, e.g. -Xmx2048m
-XX:MaxRAMPercentage=n
sets the maximum heap as n
percentage of total RAM
-Xss
- set java thread stack size
-Xms
and -Xmx
are commonly used together (where there is a know fixed value for memory resources).
"},{"location":"reference/jvm/common-options/#heap-and-garbage-collection","title":"Heap and garbage collection","text":"-XX:MaxHeapFreeRatio
\u2013 maximum percentage of heap free after garbage collection to avoid shrinking.
-XX:MinHeapFreeRatio
\u2013 minimum percentage of heap free after GC to avoid expansion
VisualVM or JConsole can monitor the heap usage
"},{"location":"reference/jvm/common-options/#container-environments","title":"Container Environments","text":"-XX:InitialRAMPercentage
and -XX:MaxRAMPercentage
options should be used to set relative limits to the resources available from the host.
Setting specific heap sizes with -Xms
and -Xmx
is strongly discouraged in Container environments, as resources available to the container from the host could change between deployments (e.g. a change in operational configuration in Kubernettes, etc.)
"},{"location":"reference/jvm/common-options/#stack-traces","title":"Stack traces","text":"-XX:-OmitStackTraceInFastThrow
no StackTrace for implicit exceptions thrown by JVM, e.g. NullPointerException, ArithmeticException, ArrayIndexOutOfBoundsException, ArrayStoreException or ClassCastException.
"},{"location":"reference/jvm/common-options/#reflection","title":"Reflection","text":"--illegal-access
option controls how deep reflection warnings are handled.
- permit (default) - generates warning only when the first illegal access was detected
- warn - emit warning after each illegal access detection
- debug - add stack trace to warning
- deny - like debug for the first detection, then killing the program.
Java 16 deprecates --illegal-access
flag, via work done for JEP403 - may still be useful for 3rd party Java libraries.
"},{"location":"reference/jvm/common-options/#enable-class-data-sharing","title":"Enable class data sharing","text":"-Xshareclasses
enables class data sharing in a shared class cache.
The JVM connects to an existing cache (creating a cache if none exist). Multiple caches specified by adding a sub-option to the -Xshareclasses
option.
"},{"location":"reference/jvm/common-options/#handling-outofmemory-error","title":"Handling \u2018OutOfMemory\u2019 Error","text":"Generating a Heap Dump for out of memory (OOM) issues is recommended for production systems, to provide data for a deep analysis of the problem. Generating a heap dump does not add overhead to the running JVM.
-XX:+HeapDumpOnOutOfMemoryError
- trigger heap dump on out of memory failure
-XX:HeapDumpPath=path-to-heap-dump-directory
- sets path to write the heap dump file (defaults to directory in which java command was ran from)
A heap dump file can gigabytes in size, so assure that the target file system has sufficient capacity.
-XX:OnOutOfMemoryError=\"shutdown -r\"
- restart the process immediately after out of memory failure
The option can take multiple commands, separated by a ;
, e.g. -XX:OnOutOfMemoryError=\"< cmd args >;< cmd args >\"
"},{"location":"reference/jvm/common-options/#trace-classloading-and-unloading","title":"Trace classloading and unloading","text":"Identify memory leaks suspected from the JVM Class Loader, e.g. classes are not unloading or garbage collected
-XX:+TraceClassLoading
- log classes loaded into the JVM
-XX:+TraceClassUnloading
- log classes unloaded from the JVM
"},{"location":"reference/jvm/common-options/#profiling","title":"Profiling","text":"Profiling JVM processes provides a fine-grained view of application execution and resource utilization. Monitor parameters including Method Executions, Thread Executions, Garbage Collections and Object Creations.
Consider using a profile tool, such as VisualVM
"},{"location":"reference/jvm/common-options/#skip-byte-code-verification","title":"Skip byte code verification","text":"The byte code for each class loaded by the JVM Class Loader is verified, which is a relatively expensive task at startup. Adding classes on the boot classpath skips the cost of the verification, although also introduces a security risk so should only be used when classes have been previously verified.
-Xbootclasspath
specifies classpath entries to load without verification
Profiling an application is a more suitable long term solution than skipping byte code verification
Checks carried out by the verifier include
- Uninitialized Variables
- Access rules for private data and methods are not violated
- Method calls match the object Reference
- There are no operand stack overflows or under-flows
- Arguments to all JVM instructions are valid types
- Final classes are not subclassed and final methods are not overridden
- field and method references have valid names, valid classes and valid type descriptor
"},{"location":"reference/jvm/common-options/#print-gc","title":"Print GC","text":"Enable the garbage collection logging to capture detailed statistics, e.g. type of garbage collector, how often memory is restored and how much time memory was held for. Garbage collection can last several milliseconds, so logging is useful for latency-sensitive processes.
-verbose:gc
- logs garbage collector runs and how long they're taking. -XX:+PrintGCDetails
- includes the data from -verbose:gc but also adds information about the size of the new generation and more accurate timings. -XX:-PrintGCTimeStamps
- Print timestamps at garbage collection.
Consider using LMAX disruptor for a Garbage Collection free architecture for ultra latency-sensitive applications
"},{"location":"reference/jvm/common-options/#deprecated-permgen-size","title":"Deprecated: PermGen Size","text":"-XX:PermSize
- size of PermGen space where string pool and class metadata is saved.
Option is useful for web servers which load classes of a web application during deployment (e.g. deploying a jar or war to Tomcat).
Metaspace has taken over PermGen space in Java 8 onward
"},{"location":"reference/jvm/experimental-options/","title":"Reference: JVM Experimental Options","text":"The HotSpot JVM provides the opportunity to try features that may appear in future release, although are currently not production-ready.
HotSpot JVM experimental features need to be unlocked by specifying the -XX:+UnlockExperimentalVMOptions
option.
For example, the ZGC garbage collector in JDK 11 can be accessed using
java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC\n
The ZGC collector became a product option in JDK 15, so is no longer experimental.
"},{"location":"reference/jvm/experimental-options/#manageable","title":"Manageable","text":"Show locks held by java.util.concurrent
classes in a HotSpot JVM thread dump:
java -XX:+UnlockExperimentalVMOptions -XX:+PrintConcurrentLocks\n
These options can be set at runtime via the MXBean API or related JDK tools
"},{"location":"reference/jvm/experimental-options/#diagnostic","title":"Diagnostic","text":"Accessing advanced diagnostic information about the HotSpot JVM.
These options require you to use the -XX:+UnlockDiagnosticVMOptions
option before they can be used.
View advance compilation optimisations using the -XX:+LogCompilation
option:
java -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation\n
The HotSpot JVM outputs a log file containing details of all the optimisations made by the JIT compilers. Inspect the output to understand which parts of your program were optimized and to identify parts of the program that might not have been optimized as expected.
The LogCompilation output is verbose but can be visualized in a tool such as JITWatch, which can tell you about method inlining, escape analysis, lock elision, and other optimizations that the HotSpot JVM made to your running code.
"},{"location":"reference/jvm/java-17-flags/","title":"Reference: Java 17 JVM flags","text":"A complete list of all flags available for the JVM, created using the -XX:+PrintFlagsFinal
option and the results written to a file
java -XX:+PrintFlagsFinal > java-flags.md\n
Find specific flags by using grep on the output with a name
java -XX:+PrintFlagsFinal -version | grep MaxHeap\n
Type Name Units ? ? uintx MaxHeapFreeRatio 70 {manageable} {default} size_t MaxHeapSize 8342470656 {product} {ergonomic} size_t SoftMaxHeapSize 8342470656 {manageable} {ergonomic}"},{"location":"reference/jvm/java-17-flags/#full-list-of-jvm-flags","title":"Full list of JVM flags","text":"Type Option Name Default value Product Category int ActiveProcessorCount -1 {product} {default} uintx AdaptiveSizeDecrementScaleFactor 4 {product} {default} uintx AdaptiveSizeMajorGCDecayTimeScale 10 {product} {default} uintx AdaptiveSizePolicyCollectionCostMargin 50 {product} {default} uintx AdaptiveSizePolicyInitializingSteps 20 {product} {default} uintx AdaptiveSizePolicyOutputInterval 0 {product} {default} uintx AdaptiveSizePolicyWeight 10 {product} {default} uintx AdaptiveSizeThroughPutPolicy 0 {product} {default} uintx AdaptiveTimeWeight 25 {product} {default} bool AdjustStackSizeForTLS false {product} {default} bool AggressiveHeap false {product} {default} intx AliasLevel 3 {C2 product} {default} bool AlignVector false {C2 product} {default} ccstr AllocateHeapAt {product} {default} intx AllocateInstancePrefetchLines 1 {product} {default} intx AllocatePrefetchDistance 256 {product} {default} intx AllocatePrefetchInstr 0 {product} {default} intx AllocatePrefetchLines 3 {product} {default} intx AllocatePrefetchStepSize 64 {product} {default} intx AllocatePrefetchStyle 1 {product} {default} bool AllowParallelDefineClass false {product} {default} bool AllowRedefinitionToAddDeleteMethods false {product} {default} bool AllowUserSignalHandlers false {product} {default} bool AllowVectorizeOnDemand true {C2 product} {default} bool AlwaysActAsServerClassMachine false {product} {default} bool AlwaysCompileLoopMethods false {product} {default} bool AlwaysLockClassLoader false {product} {default} bool AlwaysPreTouch false {product} {default} bool AlwaysRestoreFPU false {product} {default} bool AlwaysTenure false {product} {default} ccstr ArchiveClassesAtExit {product} {default} intx ArrayCopyLoadStoreMaxElem 8 {C2 product} {default} size_t AsyncLogBufferSize 2097152 {product} {default} intx AutoBoxCacheMax 128 {C2 product} {default} intx BCEATraceLevel 0 {product} {default} bool BackgroundCompilation true {pd product} {default} size_t BaseFootPrintEstimate 268435456 {product} {default} intx BiasedLockingBulkRebiasThreshold 20 {product} {default} intx BiasedLockingBulkRevokeThreshold 40 {product} {default} intx BiasedLockingDecayTime 25000 {product} {default} intx BiasedLockingStartupDelay 0 {product} {default} bool BlockLayoutByFrequency true {C2 product} {default} intx BlockLayoutMinDiamondPercentage 20 {C2 product} {default} bool BlockLayoutRotateLoops true {C2 product} {default} intx C1InlineStackLimit 5 {C1 product} {default} intx C1MaxInlineLevel 9 {C1 product} {default} intx C1MaxInlineSize 35 {C1 product} {default} intx C1MaxRecursiveInlineLevel 1 {C1 product} {default} intx C1MaxTrivialSize 6 {C1 product} {default} bool C1OptimizeVirtualCallProfiling true {C1 product} {default} bool C1ProfileBranches true {C1 product} {default} bool C1ProfileCalls true {C1 product} {default} bool C1ProfileCheckcasts true {C1 product} {default} bool C1ProfileInlinedCalls true {C1 product} {default} bool C1ProfileVirtualCalls true {C1 product} {default} bool C1UpdateMethodData true {C1 product} {default} intx CICompilerCount 12 {product} {ergonomic} bool CICompilerCountPerCPU true {product} {default} bool CITime false {product} {default} bool CheckJNICalls false {product} {default} bool ClassUnloading true {product} {default} bool ClassUnloadingWithConcurrentMark true {product} {default} bool ClipInlining true {product} {default} uintx CodeCacheExpansionSize 65536 {pd product} {default} bool CompactStrings true {pd product} {default} ccstr CompilationMode default {product} {default} ccstrlist CompileCommand {product} {default} ccstr CompileCommandFile {product} {default} ccstrlist CompileOnly {product} {default} intx CompileThreshold 10000 {pd product} {default} double CompileThresholdScaling 1.000000 {product} {default} intx CompilerThreadPriority -1 {product} {default} intx CompilerThreadStackSize 1024 {pd product} {default} size_t CompressedClassSpaceSize 1073741824 {product} {default} uint ConcGCThreads 3 {product} {ergonomic} intx ConditionalMoveLimit 3 {C2 pd product} {default} intx ContendedPaddingWidth 128 {product} {default} bool CrashOnOutOfMemoryError false {product} {default} bool CreateCoredumpOnCrash true {product} {default} bool CriticalJNINatives false {product} {default} bool DTraceAllocProbes false {product} {default} bool DTraceMethodProbes false {product} {default} bool DTraceMonitorProbes false {product} {default} bool DisableAttachMechanism false {product} {default} bool DisableExplicitGC false {product} {default} bool DisplayVMOutputToStderr false {product} {default} bool DisplayVMOutputToStdout false {product} {default} bool DoEscapeAnalysis true {C2 product} {default} bool DoReserveCopyInSuperWord true {C2 product} {default} bool DontCompileHugeMethods true {product} {default} bool DontYieldALot false {pd product} {default} ccstr DumpLoadedClassList {product} {default} bool DumpReplayDataOnError true {product} {default} bool DumpSharedSpaces false {product} {default} bool DynamicDumpSharedSpaces false {product} {default} bool EagerXrunInit false {product} {default} intx EliminateAllocationArraySizeLimit 64 {C2 product} {default} bool EliminateAllocations true {C2 product} {default} bool EliminateAutoBox true {C2 product} {default} bool EliminateLocks true {C2 product} {default} bool EliminateNestedLocks true {C2 product} {default} bool EnableContended true {product} {default} bool EnableDynamicAgentLoading true {product} {default} size_t ErgoHeapSizeLimit 0 {product} {default} ccstr ErrorFile {product} {default} bool ErrorFileToStderr false {product} {default} bool ErrorFileToStdout false {product} {default} uint64_t ErrorLogTimeout 120 {product} {default} double EscapeAnalysisTimeout 20.000000 {C2 product} {default} bool EstimateArgEscape true {product} {default} bool ExecutingUnitTests false {product} {default} bool ExitOnOutOfMemoryError false {product} {default} bool ExplicitGCInvokesConcurrent false {product} {default} bool ExtendedDTraceProbes false {product} {default} bool ExtensiveErrorReports false {product} {default} ccstr ExtraSharedClassListFile {product} {default} bool FilterSpuriousWakeups true {product} {default} bool FlightRecorder false {product} {default} ccstr FlightRecorderOptions {product} {default} bool ForceTimeHighResolution false {product} {default} intx FreqInlineSize 325 {C2 pd product} {default} double G1ConcMarkStepDurationMillis 10.000000 {product} {default} uintx G1ConcRSHotCardLimit 4 {product} {default} size_t G1ConcRSLogCacheSize 10 {product} {default} size_t G1ConcRefinementGreenZone 0 {product} {default} size_t G1ConcRefinementRedZone 0 {product} {default} uintx G1ConcRefinementServiceIntervalMillis 300 {product} {default} uint G1ConcRefinementThreads 13 {product} {ergonomic} size_t G1ConcRefinementThresholdStep 2 {product} {default} size_t G1ConcRefinementYellowZone 0 {product} {default} uintx G1ConfidencePercent 50 {product} {default} size_t G1HeapRegionSize 4194304 {product} {ergonomic} uintx G1HeapWastePercent 5 {product} {default} uintx G1MixedGCCountTarget 8 {product} {default} uintx G1PeriodicGCInterval 0 {manageable} {default} bool G1PeriodicGCInvokesConcurrent true {product} {default} double G1PeriodicGCSystemLoadThreshold 0.000000 {manageable} {default} intx G1RSetRegionEntries 768 {product} {default} intx G1RSetSparseRegionEntries 32 {product} {default} intx G1RSetUpdatingPauseTimePercent 10 {product} {default} uint G1RefProcDrainInterval 1000 {product} {default} uintx G1ReservePercent 10 {product} {default} uintx G1SATBBufferEnqueueingThresholdPercent 60 {product} {default} size_t G1SATBBufferSize 1024 {product} {default} size_t G1UpdateBufferSize 256 {product} {default} bool G1UseAdaptiveConcRefinement true {product} {default} bool G1UseAdaptiveIHOP true {product} {default} uintx GCDrainStackTargetSize 64 {product} {ergonomic} uintx GCHeapFreeLimit 2 {product} {default} uintx GCLockerEdenExpansionPercent 5 {product} {default} uintx GCPauseIntervalMillis 201 {product} {default} uintx GCTimeLimit 98 {product} {default} uintx GCTimeRatio 12 {product} {default} size_t HeapBaseMinAddress 2147483648 {pd product} {default} bool HeapDumpAfterFullGC false {manageable} {default} bool HeapDumpBeforeFullGC false {manageable} {default} intx HeapDumpGzipLevel 0 {manageable} {default} bool HeapDumpOnOutOfMemoryError false {manageable} {default} ccstr HeapDumpPath {manageable} {default} uintx HeapFirstMaximumCompactionCount 3 {product} {default} uintx HeapMaximumCompactionInterval 20 {product} {default} uintx HeapSearchSteps 3 {product} {default} size_t HeapSizePerGCThread 43620760 {product} {default} bool IgnoreEmptyClassPaths false {product} {default} bool IgnoreUnrecognizedVMOptions false {product} {default} uintx IncreaseFirstTierCompileThresholdAt 50 {product} {default} bool IncrementalInline true {C2 product} {default} uintx InitialCodeCacheSize 2555904 {pd product} {default} size_t InitialHeapSize 490733568 {product} {ergonomic} uintx InitialRAMFraction 64 {product} {default} double InitialRAMPercentage 1.562500 {product} {default} uintx InitialSurvivorRatio 8 {product} {default} uintx InitialTenuringThreshold 7 {product} {default} uintx InitiatingHeapOccupancyPercent 45 {product} {default} bool Inline true {product} {default} ccstr InlineDataFile {product} {default} intx InlineSmallCode 2500 {C2 pd product} {default} bool InlineSynchronizedMethods true {C1 product} {default} intx InteriorEntryAlignment 16 {C2 pd product} {default} intx InterpreterProfilePercentage 33 {product} {default} bool JavaMonitorsInStackTrace true {product} {default} intx JavaPriority10_To_OSPriority -1 {product} {default} intx JavaPriority1_To_OSPriority -1 {product} {default} intx JavaPriority2_To_OSPriority -1 {product} {default} intx JavaPriority3_To_OSPriority -1 {product} {default} intx JavaPriority4_To_OSPriority -1 {product} {default} intx JavaPriority5_To_OSPriority -1 {product} {default} intx JavaPriority6_To_OSPriority -1 {product} {default} intx JavaPriority7_To_OSPriority -1 {product} {default} intx JavaPriority8_To_OSPriority -1 {product} {default} intx JavaPriority9_To_OSPriority -1 {product} {default} size_t LargePageHeapSizeThreshold 134217728 {product} {default} size_t LargePageSizeInBytes 0 {product} {default} intx LiveNodeCountInliningCutoff 40000 {C2 product} {default} bool LoadExecStackDllInVMThread true {product} {default} intx LoopMaxUnroll 16 {C2 product} {default} intx LoopOptsCount 43 {C2 product} {default} intx LoopPercentProfileLimit 30 {C2 pd product} {default} uintx LoopStripMiningIter 1000 {C2 product} {default} uintx LoopStripMiningIterShortLoop 100 {C2 product} {default} intx LoopUnrollLimit 60 {C2 pd product} {default} intx LoopUnrollMin 4 {C2 product} {default} bool LoopUnswitching true {C2 product} {default} bool ManagementServer false {product} {default} size_t MarkStackSize 4194304 {product} {ergonomic} size_t MarkStackSizeMax 536870912 {product} {default} uint MarkSweepAlwaysCompactCount 4 {product} {default} uintx MarkSweepDeadRatio 5 {product} {default} intx MaxBCEAEstimateLevel 5 {product} {default} intx MaxBCEAEstimateSize 150 {product} {default} uint64_t MaxDirectMemorySize 0 {product} {default} bool MaxFDLimit true {product} {default} uintx MaxGCMinorPauseMillis 18446744073709551615 {product} {default} uintx MaxGCPauseMillis 200 {product} {default} uintx MaxHeapFreeRatio 70 {manageable} {default} size_t MaxHeapSize 7818182656 {product} {ergonomic} intx MaxInlineLevel 15 {C2 product} {default} intx MaxInlineSize 35 {C2 product} {default} intx MaxJNILocalCapacity 65536 {product} {default} intx MaxJavaStackTraceDepth 1024 {product} {default} intx MaxJumpTableSize 65000 {C2 product} {default} intx MaxJumpTableSparseness 5 {C2 product} {default} intx MaxLabelRootDepth 1100 {C2 product} {default} intx MaxLoopPad 15 {C2 product} {default} size_t MaxMetaspaceExpansion 5439488 {product} {default} uintx MaxMetaspaceFreeRatio 70 {product} {default} size_t MaxMetaspaceSize 18446744073709551615 {product} {default} size_t MaxNewSize 4689231872 {product} {ergonomic} intx MaxNodeLimit 80000 {C2 product} {default} uint64_t MaxRAM 137438953472 {pd product} {default} uintx MaxRAMFraction 4 {product} {default} double MaxRAMPercentage 25.000000 {product} {default} intx MaxRecursiveInlineLevel 1 {C2 product} {default} uintx MaxTenuringThreshold 15 {product} {default} intx MaxTrivialSize 6 {C2 product} {default} intx MaxVectorSize 32 {C2 product} {default} ccstr MetaspaceReclaimPolicy balanced {product} {default} size_t MetaspaceSize 22020096 {product} {default} bool MethodFlushing true {product} {default} size_t MinHeapDeltaBytes 4194304 {product} {ergonomic} uintx MinHeapFreeRatio 40 {manageable} {default} size_t MinHeapSize 8388608 {product} {ergonomic} intx MinInliningThreshold 250 {product} {default} intx MinJumpTableSize 10 {C2 pd product} {default} size_t MinMetaspaceExpansion 327680 {product} {default} uintx MinMetaspaceFreeRatio 40 {product} {default} uintx MinRAMFraction 2 {product} {default} double MinRAMPercentage 50.000000 {product} {default} uintx MinSurvivorRatio 3 {product} {default} size_t MinTLABSize 2048 {product} {default} intx MultiArrayExpandLimit 6 {C2 product} {default} uintx NUMAChunkResizeWeight 20 {product} {default} size_t NUMAInterleaveGranularity 2097152 {product} {default} uintx NUMAPageScanRate 256 {product} {default} size_t NUMASpaceResizeRate 1073741824 {product} {default} bool NUMAStats false {product} {default} ccstr NativeMemoryTracking off {product} {default} bool NeverActAsServerClassMachine false {pd product} {default} bool NeverTenure false {product} {default} uintx NewRatio 2 {product} {default} size_t NewSize 1363144 {product} {default} size_t NewSizeThreadIncrease 5320 {pd product} {default} intx NmethodSweepActivity 10 {product} {default} intx NodeLimitFudgeFactor 2000 {C2 product} {default} uintx NonNMethodCodeHeapSize 7602480 {pd product} {ergonomic} uintx NonProfiledCodeHeapSize 122027880 {pd product} {ergonomic} intx NumberOfLoopInstrToAlign 4 {C2 product} {default} intx ObjectAlignmentInBytes 8 {product lp64_product} {default} size_t OldPLABSize 1024 {product} {default} size_t OldSize 5452592 {product} {default} bool OmitStackTraceInFastThrow true {product} {default} ccstrlist OnError {product} {default} ccstrlist OnOutOfMemoryError {product} {default} intx OnStackReplacePercentage 140 {pd product} {default} bool OptimizeFill false {C2 product} {default} bool OptimizePtrCompare true {C2 product} {default} bool OptimizeStringConcat true {C2 product} {default} bool OptoBundling false {C2 pd product} {default} intx OptoLoopAlignment 16 {pd product} {default} bool OptoRegScheduling true {C2 pd product} {default} bool OptoScheduling false {C2 pd product} {default} uintx PLABWeight 75 {product} {default} bool PSChunkLargeArrays true {product} {default} int ParGCArrayScanChunk 50 {product} {default} uintx ParallelGCBufferWastePct 10 {product} {default} uint ParallelGCThreads 13 {product} {default} size_t ParallelOldDeadWoodLimiterMean 50 {product} {default} size_t ParallelOldDeadWoodLimiterStdDev 80 {product} {default} bool ParallelRefProcBalancingEnabled true {product} {default} bool ParallelRefProcEnabled true {product} {default} bool PartialPeelAtUnsignedTests true {C2 product} {default} bool PartialPeelLoop true {C2 product} {default} intx PartialPeelNewPhiDelta 0 {C2 product} {default} uintx PausePadding 1 {product} {default} intx PerBytecodeRecompilationCutoff 200 {product} {default} intx PerBytecodeTrapLimit 4 {product} {default} intx PerMethodRecompilationCutoff 400 {product} {default} intx PerMethodTrapLimit 100 {product} {default} bool PerfAllowAtExitRegistration false {product} {default} bool PerfBypassFileSystemCheck false {product} {default} intx PerfDataMemorySize 32768 {product} {default} intx PerfDataSamplingInterval 50 {product} {default} ccstr PerfDataSaveFile {product} {default} bool PerfDataSaveToFile false {product} {default} bool PerfDisableSharedMem false {product} {default} intx PerfMaxStringConstLength 1024 {product} {default} size_t PreTouchParallelChunkSize 4194304 {pd product} {default} bool PreferContainerQuotaForCPUCount true {product} {default} bool PreferInterpreterNativeStubs false {pd product} {default} intx PrefetchCopyIntervalInBytes 576 {product} {default} intx PrefetchFieldsAhead 1 {product} {default} intx PrefetchScanIntervalInBytes 576 {product} {default} bool PreserveAllAnnotations false {product} {default} bool PreserveFramePointer false {pd product} {default} size_t PretenureSizeThreshold 0 {product} {default} bool PrintClassHistogram false {manageable} {default} bool PrintCodeCache false {product} {default} bool PrintCodeCacheOnCompilation false {product} {default} bool PrintCommandLineFlags false {product} {default} bool PrintCompilation false {product} {default} bool PrintConcurrentLocks false {manageable} {default} bool PrintExtendedThreadInfo false {product} {default} bool PrintFlagsFinal true {product} {command line} bool PrintFlagsInitial false {product} {default} bool PrintFlagsRanges false {product} {default} bool PrintGC false {product} {default} bool PrintGCDetails false {product} {default} bool PrintHeapAtSIGBREAK true {product} {default} bool PrintSharedArchiveAndExit false {product} {default} bool PrintSharedDictionary false {product} {default} bool PrintStringTableStatistics false {product} {default} bool PrintTieredEvents false {product} {default} bool PrintVMOptions false {product} {default} bool PrintWarnings true {product} {default} uintx ProcessDistributionStride 4 {product} {default} bool ProfileInterpreter true {pd product} {default} intx ProfileMaturityPercentage 20 {product} {default} uintx ProfiledCodeHeapSize 122027880 {pd product} {ergonomic} uintx PromotedPadding 3 {product} {default} uintx QueuedAllocationWarningCount 0 {product} {default} int RTMRetryCount 5 {ARCH product} {default} bool RangeCheckElimination true {product} {default} bool ReassociateInvariants true {C2 product} {default} bool RecordDynamicDumpInfo false {product} {default} bool ReduceBulkZeroing true {C2 product} {default} bool ReduceFieldZeroing true {C2 product} {default} bool ReduceInitialCardMarks true {C2 product} {default} bool ReduceSignalUsage false {product} {default} intx RefDiscoveryPolicy 0 {product} {default} bool RegisterFinalizersAtInit true {product} {default} bool RelaxAccessControlCheck false {product} {default} ccstr ReplayDataFile {product} {default} bool RequireSharedSpaces false {product} {default} uintx ReservedCodeCacheSize 251658240 {pd product} {ergonomic} bool ResizePLAB true {product} {default} bool ResizeTLAB true {product} {default} bool RestoreMXCSROnJNICalls false {product} {default} bool RestrictContended true {product} {default} bool RestrictReservedStack true {product} {default} bool RewriteBytecodes true {pd product} {default} bool RewriteFrequentPairs true {pd product} {default} bool SafepointTimeout false {product} {default} intx SafepointTimeoutDelay 10000 {product} {default} bool ScavengeBeforeFullGC false {product} {default} bool SegmentedCodeCache true {product} {ergonomic} intx SelfDestructTimer 0 {product} {default} ccstr SharedArchiveConfigFile {product} {default} ccstr SharedArchiveFile {product} {default} size_t SharedBaseAddress 34359738368 {product} {default} ccstr SharedClassListFile {product} {default} uintx SharedSymbolTableBucketSize 4 {product} {default} ccstr ShenandoahGCHeuristics adaptive {product} {default} ccstr ShenandoahGCMode satb {product} {default} bool ShowCodeDetailsInExceptionMessages true {manageable} {default} bool ShowMessageBoxOnError false {product} {default} bool ShrinkHeapInSteps true {product} {default} size_t SoftMaxHeapSize 7818182656 {manageable} {ergonomic} intx SoftRefLRUPolicyMSPerMB 1000 {product} {default} bool SplitIfBlocks true {C2 product} {default} intx StackRedPages 1 {pd product} {default} intx StackReservedPages 1 {pd product} {default} intx StackShadowPages 20 {pd product} {default} bool StackTraceInThrowable true {product} {default} intx StackYellowPages 2 {pd product} {default} uintx StartAggressiveSweepingAt 10 {product} {default} bool StartAttachListener false {product} {default} ccstr StartFlightRecording {product} {default} uint StringDeduplicationAgeThreshold 3 {product} {default} uintx StringTableSize 65536 {product} {default} bool SuperWordLoopUnrollAnalysis true {C2 pd product} {default} bool SuperWordReductions true {C2 product} {default} bool SuppressFatalErrorMessage false {product} {default} uintx SurvivorPadding 3 {product} {default} uintx SurvivorRatio 8 {product} {default} double SweeperThreshold 0.500000 {product} {default} uintx TLABAllocationWeight 35 {product} {default} uintx TLABRefillWasteFraction 64 {product} {default} size_t TLABSize 0 {product} {default} bool TLABStats true {product} {default} uintx TLABWasteIncrement 4 {product} {default} uintx TLABWasteTargetPercent 1 {product} {default} uintx TargetPLABWastePct 10 {product} {default} uintx TargetSurvivorRatio 50 {product} {default} uintx TenuredGenerationSizeIncrement 20 {product} {default} uintx TenuredGenerationSizeSupplement 80 {product} {default} uintx TenuredGenerationSizeSupplementDecay 2 {product} {default} intx ThreadPriorityPolicy 0 {product} {default} bool ThreadPriorityVerbose false {product} {default} intx ThreadStackSize 1024 {pd product} {default} uintx ThresholdTolerance 10 {product} {default} intx Tier0BackedgeNotifyFreqLog 10 {product} {default} intx Tier0InvokeNotifyFreqLog 7 {product} {default} intx Tier0ProfilingStartPercentage 200 {product} {default} intx Tier23InlineeNotifyFreqLog 20 {product} {default} intx Tier2BackEdgeThreshold 0 {product} {default} intx Tier2BackedgeNotifyFreqLog 14 {product} {default} intx Tier2CompileThreshold 0 {product} {default} intx Tier2InvokeNotifyFreqLog 11 {product} {default} intx Tier3BackEdgeThreshold 60000 {product} {default} intx Tier3BackedgeNotifyFreqLog 13 {product} {default} intx Tier3CompileThreshold 2000 {product} {default} intx Tier3DelayOff 2 {product} {default} intx Tier3DelayOn 5 {product} {default} intx Tier3InvocationThreshold 200 {product} {default} intx Tier3InvokeNotifyFreqLog 10 {product} {default} intx Tier3LoadFeedback 5 {product} {default} intx Tier3MinInvocationThreshold 100 {product} {default} intx Tier4BackEdgeThreshold 40000 {product} {default} intx Tier4CompileThreshold 15000 {product} {default} intx Tier4InvocationThreshold 5000 {product} {default} intx Tier4LoadFeedback 3 {product} {default} intx Tier4MinInvocationThreshold 600 {product} {default} bool TieredCompilation true {pd product} {default} intx TieredCompileTaskTimeout 50 {product} {default} intx TieredRateUpdateMaxTime 25 {product} {default} intx TieredRateUpdateMinTime 1 {product} {default} intx TieredStopAtLevel 4 {product} {default} bool TimeLinearScan false {C1 product} {default} ccstr TraceJVMTI {product} {default} intx TrackedInitializationLimit 50 {C2 product} {default} bool TrapBasedNullChecks false {pd product} {default} bool TrapBasedRangeChecks false {C2 pd product} {default} intx TypeProfileArgsLimit 2 {product} {default} uintx TypeProfileLevel 111 {pd product} {default} intx TypeProfileMajorReceiverPercent 90 {C2 product} {default} intx TypeProfileParmsLimit 2 {product} {default} intx TypeProfileWidth 2 {product} {default} intx UnguardOnExecutionViolation 0 {product} {default} bool UseAES true {product} {default} intx UseAVX 2 {ARCH product} {default} bool UseAdaptiveGenerationSizePolicyAtMajorCollection true {product} {default} bool UseAdaptiveGenerationSizePolicyAtMinorCollection true {product} {default} bool UseAdaptiveNUMAChunkSizing true {product} {default} bool UseAdaptiveSizeDecayMajorGCCost true {product} {default} bool UseAdaptiveSizePolicy true {product} {default} bool UseAdaptiveSizePolicyFootprintGoal true {product} {default} bool UseAdaptiveSizePolicyWithSystemGC false {product} {default} bool UseAddressNop true {ARCH product} {default} bool UseBASE64Intrinsics false {product} {default} bool UseBMI1Instructions true {ARCH product} {default} bool UseBMI2Instructions true {ARCH product} {default} bool UseBiasedLocking false {product} {default} bool UseBimorphicInlining true {C2 product} {default} bool UseCLMUL true {ARCH product} {default} bool UseCMoveUnconditionally false {C2 product} {default} bool UseCodeAging true {product} {default} bool UseCodeCacheFlushing true {product} {default} bool UseCompiler true {product} {default} bool UseCompressedClassPointers true {product lp64_product} {ergonomic} bool UseCompressedOops true {product lp64_product} {ergonomic} bool UseCondCardMark false {product} {default} bool UseContainerSupport true {product} {default} bool UseCountLeadingZerosInstruction true {ARCH product} {default} bool UseCountTrailingZerosInstruction true {ARCH product} {default} bool UseCountedLoopSafepoints true {C2 product} {default} bool UseCounterDecay true {product} {default} bool UseDivMod true {C2 product} {default} bool UseDynamicNumberOfCompilerThreads true {product} {default} bool UseDynamicNumberOfGCThreads true {product} {default} bool UseEmptySlotsInSupers true {product} {default} bool UseFMA true {product} {default} bool UseFPUForSpilling true {C2 product} {default} bool UseFastJNIAccessors true {product} {default} bool UseFastStosb false {ARCH product} {default} bool UseG1GC true {product} {ergonomic} bool UseGCOverheadLimit true {product} {default} bool UseHeavyMonitors false {product} {default} bool UseHugeTLBFS false {product} {default} bool UseInlineCaches true {product} {default} bool UseInterpreter true {product} {default} bool UseJumpTables true {C2 product} {default} bool UseLargePages false {pd product} {default} bool UseLargePagesIndividualAllocation false {pd product} {default} bool UseLinuxPosixThreadCPUClocks true {product} {default} bool UseLoopCounter true {product} {default} bool UseLoopInvariantCodeMotion true {C1 product} {default} bool UseLoopPredicate true {C2 product} {default} bool UseMaximumCompactionOnSystemGC true {product} {default} bool UseNUMA false {product} {default} bool UseNUMAInterleaving false {product} {default} bool UseNewLongLShift true {ARCH product} {default} bool UseNotificationThread true {product} {default} bool UseOnStackReplacement true {pd product} {default} bool UseOnlyInlinedBimorphic true {C2 product} {default} bool UseOprofile false {product} {default} bool UseOptoBiasInlining false {C2 product} {default} bool UsePSAdaptiveSurvivorSizePolicy true {product} {default} bool UseParallelGC false {product} {default} bool UsePerfData true {product} {default} bool UsePopCountInstruction true {product} {default} bool UseProfiledLoopPredicate true {C2 product} {default} bool UseRTMDeopt false {ARCH product} {default} bool UseRTMLocking false {ARCH product} {default} bool UseSHA true {product} {default} bool UseSHM false {product} {default} intx UseSSE 4 {ARCH product} {default} bool UseSSE42Intrinsics true {ARCH product} {default} bool UseSerialGC false {product} {default} bool UseSharedSpaces true {product} {default} bool UseShenandoahGC false {product} {default} bool UseSignalChaining true {product} {default} bool UseStoreImmI16 true {ARCH product} {default} bool UseStringDeduplication false {product} {default} bool UseSubwordForMaxVector true {C2 product} {default} bool UseSuperWord true {C2 product} {default} bool UseTLAB true {product} {default} bool UseThreadPriorities true {pd product} {default} bool UseTransparentHugePages false {product} {default} bool UseTypeProfile true {product} {default} bool UseTypeSpeculation true {C2 product} {default} bool UseUnalignedLoadStores true {ARCH product} {default} bool UseVectorCmov false {C2 product} {default} bool UseXMMForArrayCopy true {product} {default} bool UseXMMForObjInit true {ARCH product} {default} bool UseXmmI2D true {ARCH product} {default} bool UseXmmI2F true {ARCH product} {default} bool UseXmmLoadAndClearUpper true {ARCH product} {default} bool UseXmmRegToRegMoveAll true {ARCH product} {default} bool UseZGC false {product} {default} intx VMThreadPriority -1 {product} {default} intx VMThreadStackSize 1024 {pd product} {default} intx ValueMapInitialSize 11 {C1 product} {default} intx ValueMapMaxLoopSize 8 {C1 product} {default} intx ValueSearchLimit 1000 {C2 product} {default} bool VerifySharedSpaces false {product} {default} uintx YoungGenerationSizeIncrement 20 {product} {default} uintx YoungGenerationSizeSupplement 80 {product} {default} uintx YoungGenerationSizeSupplementDecay 8 {product} {default} size_t YoungPLABSize 4096 {product} {default} double ZAllocationSpikeTolerance 2.000000 {product} {default} double ZCollectionInterval 0.000000 {product} {default} double ZFragmentationLimit 25.000000 {product} {default} size_t ZMarkStackSpaceLimit 8589934592 {product} {default} bool ZProactive true {product} {default} bool ZUncommit true {product} {default} uintx ZUncommitDelay 300 {product} {default} bool ZeroTLAB false {product} {default}"},{"location":"reference/jvm/profile-tools/","title":"Profile JVM applications","text":"Profile applications on the JVM, visualising memory and CPU resources, identifying bottlenecks and areas of the code to review to optimise a running application.
Using FlameGraphs To Illuminate The JVM A Simple Approach to the Advanced JVM Profiling
"},{"location":"reference/jvm/profile-tools/#visualvm","title":"VisualVM","text":"VisualVM provides a simplified and robust profiling tool for Java applications, bundled with the Java Development Kit (JDK) and using JConsole, jstat, jstack, jinfo, and jmap.
UbuntuMacOSX Ubuntu / Debian includes VisualVM in the software center
sudo apt install visualvm\n
Download the macOS application bundle and double-click to install.
"},{"location":"reference/jvm/profile-tools/#jdk-flight-recorder","title":"JDK Flight Recorder","text":"JDK Flight Recorder is a production time profiling and diagnostics engine built into the JVM
- Extremely low overhead - no measurable impact on the running application
- High performance flight recording engine and high performance data collection
- Safe and reliable in production, tested on all platforms as part of the JVM/JDK-testing
- Time machine records data before, up to, and right after a problem occurs (even if the JVM process crashes)
jcmd
to access the flight recorder data from the command line
Mission control provides a graphical tool to visualise flight recorder data.
- Continuous Monitoring with JDK Flight Recorder
- JDK11 - Introduction to JDK Flight Recorder
- Production profiling with JDK Flight Recorder & JDK Mission Control
"},{"location":"reference/jvm/profile-tools/#mission-control","title":"Mission Control","text":"Mission Control is an open source desktop tool for visualising production time profiling and diagnostics from the JDK flight recorder tool. JDK Mission Control supports OpenJDK 11 and above.
JDK Mission Control consists of
- A JDK Flight Recorder (JFR) analyser and visualiser
- A Java Management Extensions (JMX) Console
- A heap dump (hprof format) analyzer (JOverflow)
Eclipse Mission Control from Adoptium
Java Mission Control demo - 2014 outated but might be useful if nothing newer
"},{"location":"reference/jvm/profile-tools/#profiling-guides","title":"Profiling guides","text":"Profiling your Java Application - A Beginner\u2019s Guide - Victor Rentea
Explore three of the best free tools for profiling a Java (Spring) application:
- Using Java Flight Recorder to profile method execution times
- Using Micrometer-Prometheus-Grafana to profile connection starvation issues
- Using Glowroot to identify long-running queries
"},{"location":"reference/jvm/profile-tools/#references","title":"References","text":"Java Profilers - Baeldung HotSpot Virtual Machine Garbage Collection Tuning Guide - Oracle
"},{"location":"reference/jvm/understanding-memory-usage/","title":"Memory usage","text":"Adjusting the heap size and Garbage Collection behaviour is often the simplest means to improving application performance and stability. A mismatch between the heap size.
Allocating additional memory to the HotSpot JVM is a relatively cheap way to improve the performance of an application.
Garbage collection cost is in the form of execution pauses while the HotSpot JVM cleans up the no-longer-needed heap allocations.
Report a full breakdown of the HotSpot JVM\u2019s memory usage upon exit using the following option combination:
JVM Memory Usage Report
```shell -XX:+UnlockDiagnosticVMOptions \u2011XX:NativeMemoryTracking=summary \u2011XX:+PrintNMTStatistics.
```
"},{"location":"reference/jvm/understanding-memory-usage/#out-of-memory-errors","title":"Out Of Memory errors","text":"When experiencing OutOfMemory
errors, consider how the HotSpot JVM should behave if the application runs out of memory.
-XX:+ExitOnOutOfMemoryError
- HotSpot JVM exits on the first OutOfMemory error, suitable if the JVM will be automatically restarted (such as in container services)
-XX:+HeapDumpOnOutOfMemoryError
- dump contents of heap to file, <java_pid>.hprof
, to help diagnose memory leaks
-XX:HeapDumpPath
defines the path for the heap dump, default is current directory
"},{"location":"reference/jvm/understanding-memory-usage/#choose-a-garbage-collector","title":"Choose A Garbage Collector","text":"The HotSpot Virtual Machine Garbage Collection Tuning Guide provides advice on selecting a suitable garbage collector (GC)
G1GC collector is the default used by the JDK ergonomics process on most hardware.
Other garbage collectors available include:
-XX:+UseSerialGC
- serial collector, performing all GC work on a single thread
-XX:+UseParallelGC
- parallel (throughput) collector, performs compaction using multiple threads.
-XX:+UseZGC
- ZGC collector scalable low latency garbage collector (experimental in JDK 11, so requires -XX:+UnlockExperimentalVMOptions
).
Enable garbage collection logging
-Xlog:gc
- basic GC logging
-Xlog:gc*
- verbose GC logging
"},{"location":"reference/jvm/understanding-memory-usage/#object-allocation","title":"Object Allocation","text":"Applications that create short-lived objects at a high allocation rates can lead to the premature promotion of short-lived objects to the old-generation heap space. There the objects will accumulate until a full garbage collection is needed
To avoid premature promotion:
-XX:NewSize=n
- initial size for the young generation
-XX:MaxNewSize=n
- maximum size for the young generation
-XX:MaxTenuringThreshold=n
- maximum number of young-generation collections an object can survive before it is promoted to the old generation
"},{"location":"reference/jvm/understanding-memory-usage/#just-in-time-optimisation","title":"Just In Time Optimisation","text":"Understand how the Just In Time (JIT) compiler optimises the code.
Once an application garbage collection pauses are an acceptable level, check the JIT compilers are optimizing the parts of your program you think are important for performance.
Enable compilation logging:
-XX:+PrintCompilation
print basic information about each JIT compilation to the console
-XX:+UnlockDiagnosticVMOptions \u2011XX:+PrintCompilation \u2011XX:+PrintInlining
- information about method in-lining
"},{"location":"reference/performance/","title":"Clojure Performance","text":"Two excellent presentations on Clojure performance
"},{"location":"reference/performance/#optimising-the-critical-path","title":"Optimising the critical path","text":"Using two of Clojure's fundamental building blocks, macros and higher order functions, Clojure code can be sped up significantly without sacrificing common idioms.
Premature optimisation is the root of all evil, this is not a reason to pass up opportunities to improve the critical 3%.
"},{"location":"reference/performance/#naked-performance","title":"Naked Performance","text":"Lessons learned on building Clojure/Script systems that are both ridiculously fast and will fail fast on errors.
Compare the performance of mutable, persistent & zero-copy data structures, showing how to use interpreters and compilers to build beautiful and performant abstractions.
A shot demo on building a simple non-blocking web server that runs idiomatic Clojure to serve millions of requests per second.
"},{"location":"reference/performance/#advanced-types","title":"Advanced types","text":"Clojure support for Java Primitives
Clojure has support for high-performance with Java primitive types in local contexts. All Java primitive types are supported: int, float, long, double, boolean, char, short, and byte. In the extremely rare occasions where this is needed, it is added via metadata and therefore only adds to the existing code without rewriting it.
Rather than write this Java:
static public float asum(float[] xs){\n float ret = 0;\n for(int i = 0; i < xs.length; i++)\n ret += xs[i];\n return ret;\n}\n
you can write this Clojure:
(defn asum [^floats xs]\n (areduce xs i ret (float 0)\n (+ ret (aget xs i))))\n
and the resulting code is exactly the same speed (when run with java -server).
"},{"location":"reference/performance/#optimization-tips-for-types","title":"Optimization tips for types","text":"All arguments are passed to Clojure fns as objects, so there's no point to putting non-array primitive type hints on fn args. Instead, use the let technique shown to place args in primitive locals if they need to participate in primitive arithmetic in the body. (let [foo (int bar)] ...) is the correct way to get a primitive local. Do not use ^Integer etc.
Don't rush to unchecked math unless you want truncating operations. HotSpot does a good job at optimizing the overflow check, which will yield an exception instead of silent truncation. On a typical example, that has about a 5% difference in speed - well worth it. Also, people reading your code don't know if you are using unchecked for truncation or performance - best to reserve it for the former and comment if the latter.
There's usually no point in trying to optimize an outer loop, in fact it can hurt you as you'll be representing things as primitives which just have to be re-boxed in order to become args to the inner call. The only exception is reflection warnings - you must get rid of them in any code that gets called frequently.
Almost every time someone presents something they are trying to optimize with hints, the faster version has far fewer hints than the original. If a hint doesn't improve things in the end - take it out.
Many people seem to presume only the unchecked- ops do primitive arithmetic - not so. When the args are primitive locals, regular + and * etc do primitive math with an overflow check - fast and safe.
So, the simplest route to fast math is to leave the operators alone and just make sure the source literals and locals are primitive. Arithmetic on primitives yields primitives. If you've got a loop (which you probably do if you need to optimize) make sure the loop locals are primitives first - then if you accidentally are producing a boxed intermediate result you'll get an error on recur. Don't solve that error by coercing your intermediate result, instead, figure out what argument or local is not primitive.
"},{"location":"reference/standard-library/","title":"Clojure Standard Library","text":"Examples of using the functions from the clojure.core
namespace and other important functions, macros and special forms that are part of the org.clojure/clojure
library.
There are approximately 700 functions and macros available in the clojure.core
namespace. These are referred to as the Clojure Standard Library.
Counting functions in clojure.core
To get an accurate number of functions, call the ns-publics
function with a namespace name
(count (ns-publics 'clojure.core))\n
Random Function is a simple project that prints out a random function from the given namespace, or from clojure.core by default."},{"location":"reference/standard-library/#functions-macros-and-special-forms","title":"Functions, Macros and Special forms","text":"The majority of times macros and special forms act just like any other defined function (i.e. fn
, defn
)
A macro is a piece of code that evaluates into a function when read by the macro reader, or by the developer using macroexpand
function. An expanded macro may also contain macros, so expansion could take place several levels (macroexpand-all
).
macros are not composable like functions, so functions like apply
reduce
map
cannot use a macro (use a function instead).
Special forms are built into the Clojure runtime, so will not be found in clojure.core
- Special forms:
if
do
let
quote
var
fn
loop
recur
throw
try
- Special forms for Java interop:
.
new
set!
"},{"location":"reference/standard-library/collections/","title":"Standard Library: Collections","text":"Functions to create and work with the Clojure collection types, mainly maps, vectors and sets
See Sequences for functions around lists and (lazy) sequences
"},{"location":"reference/standard-library/cond-thread-macro/","title":"Clojure cond->","text":"cond->
and cond->>
are versatile macros available since version 1.5, although its more of a nieche use, its really useful in that neiche
Usage: (cond-> expr & clauses)
Takes an expression and a set of test/form pairs.
Threads expr (via ->) through each form for which the corresponding test expression is true.
Note that, unlike cond branching, cond-> threading does not short circuit after the first true test expression.
"},{"location":"reference/standard-library/cond-thread-macro/#deconstruct","title":"Deconstruct","text":"(cond-> 10\n false inc)\n;; => 10\n
In the above example 10 is the expr mentioned in the docstring and everything after it are the clauses.
Each clause is a pair made up of a test and a form. There is a single clause with the value false as the test the function inc as the form.
Since the test evaluates to a false value the expression is not threaded into the form. As a result the original expression, 10, is returned.
Let\u2019s look at an example with a truthy test.
(cond-> 10\n true (- 2)\n;;=> 8\n
Once again, 10 is the starting expression. The single clause has a test that evaluates to true so the expression is threaded into the first position of the form (- 2). The result is 8 and this is returned.
An example of a cond->
with multiple clauses. Explanations are inline with the code.
(cond-> 10 ; start with 10\n ;; test evaluates to true, so apply inc to 10. Current value is now 11.\n true inc\n\n ;; (zero? 1) evaluates to false, do not perform action. Current value stays 11.\n (zero? 1) (+ 2)\n\n ;; (pos? 4) evaluates to true, thread 11 into first position of form.\n (pos? 4) (- 5))\n;; => 6 ; The result of (- 11 5) is 6.\n
If you understand the above example then you have a good grasp of cond->. But when is this functionality useful?
"},{"location":"reference/standard-library/cond-thread-macro/#when-to-use-cond-","title":"When to use cond->?","text":"Looking through the codebases I work on, I almost primarily see cond-> being used with the initial expression being a hash-map. It is being used in situations where we want to selectively assoc, update, or dissoc something from a map.
If cond-> did not exist you would accomplish those selective modifications with code similar to below.
(if (some-pred? q)\n (assoc m :a-key :a-value)\n m)\n
Rewrite the above with cond->.
(cond-> m\n (some-pred? q) (assoc :a-key :a-value))\n
If you\u2019re not used to seeing cond-> the above transformation might seem like a step backwards. I know it felt that way to me when I first saw cond->. Give yourself time to get familiar with it and you\u2019ll be glad you\u2019re using it.
A meatier example of using cond-> is demonstrated below. Here we\u2019re manipulating data structures designed for use with honeysql to generate SQL statements. We start with a base-query and selectively modify it based on incoming parameters.
(defn query [req-params]\n (let [and-clause (fnil conj [:and])\n base-query {:select [:name :job]\n :from [:person]}]\n (cond-> base-query\n (:job req-params) (update :where and-clause [:= :job (:job req-params)])\n (:name req-params) (update :where and-clause [:= :name (:name req-params)])\n (:min-age req-params) (update :where and-clause [:> :age (:min-age req-params)]))))\n
Hopefully this gives you a taste of cond->. I\u2019ve found it to be quite useful. It has a place in every Clojure developer\u2019s toolbox.
"},{"location":"reference/standard-library/destructuring/","title":"Destructuring","text":"Destructuring is a form of pattern matching where you return specific elements from a collection and assign those elements names. It is commonly used in function parameter lists or with the let
function.
Destructuring is also known as abstract structural binding
A simple example of destructuring is assigning the values of a collection, in this case a vector.
(def co-ordinates [5 7])\n\n(let [[x y] co-ordinates]\n (str \"x:\" x \"y:\" y))\n;; => x: 5 y: 7\n
;; Sometimes we do not need all the information, so we can just use the elements we need.
(def three-dee-co-ordinates [2 7 4])\n\n(let [[x y] three-dee-co-ordinates]\n (str \"I only need the 2D co-ordinates, X: \" x \" and Y: \" y ))\n;; => \"I only need the 2D co-ordinates, X: 2 and Y: 7\"\n
Its quite common to take the first element as a specific name and use another name for the rest of the elements
(def shopping-list [\"oranges\" \"apples\" \"spinach\" \"carrots\" \"potatoes\" \"beetroot\"])\n\n(defn get-item [items]\n (let [[next-item & other-items] items]\n (str \"The next item to get is: \" next-item)))\n\n(get-item shopping-list)\n;; => \"The next item to get is: oranges\"\n
This example seems a little redundant at first, however if we add recursion then we can iterate through the shopping list and it should make more sense
splitting a vector into a head and a tail. When defining a function with an arglist** you use an ampersand. The same is true in destructuring.
(def indexes [1 2 3])\n\n(let [[x & more] indexes]\n (println \"x:\" x \"more:\" more))\n;; => x: 1 more: (2 3)\n
It's also worth noting that you can bind the entire vector to a local using the :as directive.
(def indexes [1 2 3])\n
"},{"location":"reference/standard-library/destructuring/#userindexes","title":"'user/indexes","text":" (let [[x & more :as full-list] indexes]\n (println \"x:\" x \"more:\" more \"full list:\" full-list))\n;; => x: 1 more: (2 3) full list: [1 2 3]\n
Vector examples are the easiest; however, in practice I find myself using destructuring with maps far more often.
Simple destructuring on a map is as easy as choosing a local name and providing the key.
(def point {:x 5 :y 7})\n
"},{"location":"reference/standard-library/destructuring/#userpoint","title":"'user/point","text":" (let [{the-x :x the-y :y} point]\n (println \"x:\" the-x \"y:\" the-y))\n;; => x: 5 y: 7\n
As the example shows, the values of :x and :y are bound to locals with the names the-x and the-y. In practice we would never prepend \"the-\" to our local names; however, using different names provides a bit of clarity for our first example. In production code you would be much more likely to want locals with the same name as the key. This works perfectly well, as the next example shows.
(def point {:x 5 :y 7})\n
"},{"location":"reference/standard-library/destructuring/#userpoint_1","title":"'user/point","text":"user=> (let [{x :x y :y} point] (println \"x:\" x \"y:\" y)) x: 5 y: 7
While this works perfectly well, creating locals with the same name as the keys becomes tedious and annoying (especially when your keys are longer than one letter). Clojure anticipates this frustration and provides :keys directive that allows you to specify keys that you would like as locals with the same name.
user=> (def point {:x 5 :y 7})
"},{"location":"reference/standard-library/destructuring/#userpoint_2","title":"'user/point","text":"user=> (let [{:keys [x y]} point] (println \"x:\" x \"y:\" y)) x: 5 y: 7
There are a few directives that work while destructuring maps. The above example shows the use of :keys. In practice I end up using :keys the most; however, I've also used the :as directive while working with maps.
The following example illustrates the use of an :as directive to bind a local with the entire map.
user=> (def point {:x 5 :y 7})
"},{"location":"reference/standard-library/destructuring/#userpoint_3","title":"'user/point","text":"user=> (let [{:keys [x y] :as the-point} point] (println \"x:\" x \"y:\" y \"point:\" the-point)) x: 5 y: 7 point: {:x 5, :y 7}
We've now seen the :as directive used for both vectors and maps. In both cases the local is always assigned to the entire expression that is being destructured.
For completeness I'll document the :or directive; however, I must admit that I've never used it in practice. The :or directive is used to assign default values when the map being destructured doesn't contain a specified key.
user=> (def point {:y 7})
"},{"location":"reference/standard-library/destructuring/#userpoint_4","title":"'user/point","text":"user=> (let [{:keys [x y] :or {x 0 y 0}} point] (println \"x:\" x \"y:\" y)) x: 0 y: 7
Lastly, it's also worth noting that you can destructure nested maps, vectors and a combination of both.
The following example destructures a nested map
user=> (def book {:name \"SICP\" :details {:pages 657 :isbn-10 \"0262011530\"}})
"},{"location":"reference/standard-library/destructuring/#userbook","title":"'user/book","text":"user=> (let [{name :name {pages :pages isbn-10 :isbn-10} :details} book] (println \"name:\" name \"pages:\" pages \"isbn-10:\" isbn-10)) name: SICP pages: 657 isbn-10: 0262011530
As you would expect, you can also use directives while destructuring nested maps.
user=> (def book {:name \"SICP\" :details {:pages 657 :isbn-10 \"0262011530\"}})
"},{"location":"reference/standard-library/destructuring/#userbook_1","title":"'user/book","text":"user=> user=> (let [{name :name {:keys [pages isbn-10]} :details} book] (println \"name:\" name \"pages:\" pages \"isbn-10:\" isbn-10)) name: SICP pages: 657 isbn-10: 0262011530
Destructuring nested vectors is also very straight-forward, as the following example illustrates
user=> (def numbers [[1 2][3 4]])
"},{"location":"reference/standard-library/destructuring/#usernumbers","title":"'user/numbers","text":"user=> (let [[[a b][c d]] numbers] (println \"a:\" a \"b:\" b \"c:\" c \"d:\" d)) a: 1 b: 2 c: 3 d: 4
Since binding forms can be nested within one another arbitrarily, you can pull apart just about anything -- http://clojure.org/special_forms\n
The following example destructures a map and a vector at the same time.
user=> (def golfer {:name \"Jim\" :scores [3 5 4 5]})
"},{"location":"reference/standard-library/destructuring/#usergolfer","title":"'user/golfer","text":"user=> (let [{name :name [hole1 hole2] :scores} golfer] (println \"name:\" name \"hole1:\" hole1 \"hole2:\" hole2)) name: Jim hole1: 3 hole2: 5
The same example can be rewritten using a function definition to show the simplicity of using destructuring in parameter lists.
user=> (defn print-status [{name :name [hole1 hole2] :scores}] (println \"name:\" name \"hole1:\" hole1 \"hole2:\" hole2))
"},{"location":"reference/standard-library/destructuring/#userprint-status","title":"'user/print-status","text":"user=> (print-status {:name \"Jim\" :scores [3 5 4 5]}) name: Jim hole1: 3 hole2: 5
There are other (less used) directives and deeper explanations available on http://clojure.org/special_forms and in The Joy of Clojure. I recommend both.
**(defn do-something [x y & more] ... ) Posted by Jay Fields at 7:44 AM Email ThisBlogThis!Share to TwitterShare to FacebookShare to Pinterest Labels: clojure, destructuring 10 comments:
fogus8:26 AM\n\nNice post. One other note that naturally follows from the end of your post is that destructuring forms the basis of Clojure's named arguments:\n\n(defn print-status [& {name :name [hole1 hole2] :scores}]\n(println \"name:\" name \"hole1:\" hole1 \"hole2:\" hole2))\n\n(print-status :name \"Joey\" :scores [42 18])\n\n\nYou can also use pre-conditions to check if certain arguments are passed in:\n\n\n(defn print-status [& {name :name [hole1 hole2] :scores}]\n{:pre [name]}\n(println \"name:\" name \"hole1:\" hole1 \"hole2:\" hole2))\n\n(print-status :scores [42 18])\n; java.lang.AssertionError: Assert failed: name\n\n(print-status :name \"Joey\" :scores [42 18])\n; name: Joey hole1: 42 hole2: 18\n\n\n:f\nReply\nJay Fields9:08 AM\n\nGood stuff Fogus, thanks.\n\nCheers, Jay\nReply\nMatt Todd5:31 PM\n\nCan you combine :as and :or et al?\nReply\nAnonymous7:29 PM\n\nYes, all the directives can be used at the same time.\n\nCheers, Jay\nReply\nLaurent PETIT3:08 AM\n
Hi, one note about using destructuring for function arguments : by doing so, you're quite explicitly establishing a more detailed contract with the consumer of the function. That is, you open the internals of the passed arguments.
Depending on the fact that the user may or may not be aware of the internals of the arguments, it may or may not be a good idea.
So I tend to think about the use of destructuring function arguments directly in the function signature, depending on whether the \"layout\" of the arguments of the function is part of the user API. Reply
"},{"location":"reference/standard-library/destructuring/#clojure-destructuring-tutorial-and-cheat-sheet","title":"Clojure Destructuring Tutorial and Cheat Sheet","text":"(Related blog post)
Simply put, destructuring in Clojure is a way extract values from a data structure and bind them to symbols, without having to explicitly traverse the data structure. It allows for elegant and concise Clojure code.
"},{"location":"reference/standard-library/destructuring/#vectors","title":"Vectors","text":"Syntax: [symbol another-symbol] [\"value\" \"another-value\"]
(def my-vector [:a :b :c :d])\n(def my-nested-vector [:a :b :c :d [:x :y :z]])\n\n(let [[a b c d] my-vector]\n (println a b c d))\n;; => :a :b :c :d\n\n(let [[a _ _ d [x y z]] my-nested-vector]\n (println a d x y z))\n;; => :a :d :x :y :z\n
You don't have to match the full vector.
(let [[a b c] my-vector]\n (println a b c))\n;; => :a :b :c\n
You can use & the-rest
to bind the remaining part of the vector to the-rest
.
(let [[a b & the-rest] my-vector]\n (println a b the-rest))\n;; => :a :b (:c :d)\n
When a destructuring form \"exceeds\" a vector (i.e. there not enough items in the vector to bind to), the excess symbols will be bound to nil
.
(let [[a b c d e f g] my-vector]\n (println a b c d e f g))\n;; => :a :b :c :d nil nil nil\n
You can use :as some-symbol
as the last two items in the destructuring form to bind the whole vector to some-symbol
(let [[:as all] my-vector]\n (println all))\n;; => [:a :b :c :d]\n\n(let [[a :as all] my-vector]\n (println a all))\n;; => :a [:a :b :c :d]\n\n(let [[a _ _ _ [x y z :as nested] :as all] my-nested-vector]\n (println a x y z nested all))\n;; => :a :x :y :z [:x :y :z] [:a :b :c :d [:x :y :z]]\n
You can use both & the-rest
and :as some-symbol
.
(let [[a b & the-rest :as all] my-vector]\n (println a b the-rest all))\n;; => :a :b (:c :d) [:a :b :c :d]\n
"},{"location":"reference/standard-library/destructuring/#optional-arguments-for-functions","title":"Optional arguments for functions","text":"With destructuring and the & the-rest
form, you can specify optional arguments to functions.
(defn foo [a b & more-args]\n (println a b more-args))\n(foo :a :b) ;; => :a :b nil\n(foo :a :b :x) ;; => :a :b (:x)\n(foo :a :b :x :y :z) ;; => :a :b (:x :y :z)\n\n(defn foo [a b & [x y z]]\n (println a b x y z))\n(foo :a :b) ;; => :a :b nil nil nil\n(foo :a :b :x) ;; => :a :b :x nil nil\n(foo :a :b :x :y :z) ;; => :a :b :x :y :z\n
"},{"location":"reference/standard-library/destructuring/#maps","title":"Maps","text":"Syntax: {symbol :key, another-symbol :another-key} {:key \"value\" :another-key \"another-value\"}
(def my-hashmap {:a \"A\" :b \"B\" :c \"C\" :d \"D\"})\n(def my-nested-hashmap {:a \"A\" :b \"B\" :c \"C\" :d \"D\" :q {:x \"X\" :y \"Y\" :z \"Z\"}})\n\n(let [{a :a d :d} my-hashmap]\n (println a d))\n;; => A D\n\n(let [{a :a, b :b, {x :x, y :y} :q} my-nested-hashmap]\n (println a b x y))\n;; => A B X Y\n
Similar to vectors, if a key is not found in the map, the symbol will be bound to nil
.
(let [{a :a, not-found :not-found, b :b} my-hashmap]\n (println a not-found b))\n;; => A nil B\n
You can provide an optional default value for these missing keys with the :or
keyword and a map of default values.
(let [{a :a, not-found :not-found, b :b, :or {not-found \":)\"}} my-hashmap]\n (println a not-found b))\n;; => A :) B\n
The :as some-symbol
form is also available for maps, but unlike vectors it can be specified anywhere (but still preferred to be the last two pairs).
(let [{a :a, b :b, :as all} my-hashmap]\n (println a b all))\n;; => A B {:a A :b B :c C :d D}\n
And combining :as
and :or
keywords (again, :as
preferred to be the last).
(let [{a :a, b :b, not-found :not-found, :or {not-found \":)\"}, :as all} my-hashmap]\n (println a b not-found all))\n;; => A B :) {:a A :b B :c C :d D}\n
There is no & the-rest
for maps.
"},{"location":"reference/standard-library/destructuring/#shortcuts","title":"Shortcuts","text":"Having to specify {symbol :symbol}
for each key is repetitive and verbose (it's almost always going to be the symbol equivalent of the key), so shortcuts are provided so you only have to type the symbol once.
Here are all the previous examples using the :keys
keyword followed by a vector of symbols:
(let [{:keys [a d]} my-hashmap]\n (println a d))\n;; => A D\n\n(let [{:keys [a b], {:keys [x y]} :q} my-nested-hashmap]\n (println a b x y))\n;; => A B X Y\n\n(let [{:keys [a not-found b]} my-hashmap]\n (println a not-found b))\n;; => A nil B\n\n(let [{:keys [a not-found b], :or {not-found \":)\"}} my-hashmap]\n (println a not-found b))\n;; => A :) B\n\n(let [{:keys [a b], :as all} my-hashmap]\n (println a b all))\n;; => A B {:a A :b B :c C :d D}\n\n(let [{:keys [a b not-found], :or {not-found \":)\"}, :as all} my-hashmap]\n (println a b not-found all))\n;; => A B :) {:a A :b B :c C :d D}\n
There are also :strs
and :syms
alternatives, for when your map has strings or symbols for keys (instead of keywords), respectively.
(let [{:strs [a d]} {\"a\" \"A\", \"b\" \"B\", \"c\" \"C\", \"d\" \"D\"}]\n (println a d))\n;; => A D\n\n(let [{:syms [a d]} {'a \"A\", 'b \"B\", 'c \"C\", 'd \"D\"}]\n (println a d))\n;; => A D\n
"},{"location":"reference/standard-library/destructuring/#keyword-arguments-for-function","title":"Keyword arguments for function","text":"Map destructuring also works with lists (but not vectors).
(let [{:keys [a b]} '(\"X\", \"Y\", :a \"A\", :b \"B\")]\n(println a b))\n;; => A B\n
This allows your functions to have optional keyword arguments.
(defn foo [a b & {:keys [x y]}]\n (println a b x y))\n(foo \"A\" \"B\") ;; => A B nil nil\n(foo \"A\" \"B\" :x \"X\") ;; => A B X nil\n(foo \"A\" \"B\" :x \"X\" :y \"Y\") ;; => A B X Y\n
"},{"location":"reference/standard-library/predicate-functions/","title":"Clojure Predicate functions","text":"A predicate function takes a single argument and returns a truthy value, e.g. true
or false
There are over 70 predicate functions provided by the clojure.core
namespace.
clojure.core
predicates Description >0? (^:private) >1? (^:private) any? associative? boolean? bound? bytes? chunked-seq? (^:static) class? coll? contains? counted? decimal? delay? distinct? double? empty? even? every? false? fits-table? (defn-) float? fn? future? future-cancelled? future-done? ident? identical? ifn? indexed? inst? int? integer? isa? is-annotation? (defn-) is-runtime-annotation? (defn-) keyword? libspec? (defn-) list? map-entry? nat-int? neg? neg-int? nil? number? odd? pos? pos-int? qualified-ident? qualified-keyword? qualified-symbol? ratio? rational? reader-conditional? realized? reduced? reversible? seqable? sequential? set? simple-ident? simple-keyword? simple-symbol? some? sorted? special-symbol? symbol? tagged-literal? thread-bound? true? uri? uuid? var? volatile? zero?"},{"location":"reference/standard-library/sequences/","title":"Standard Library: Sequences","text":"Functions to create and work with the Clojure sequences, including lists and sequence generators
"},{"location":"reference/standard-library/sequences/#sequence-access","title":"Sequence access","text":"Function Description first
second
rest
last
butlast
nth
"},{"location":"reference/standard-library/sequences/#infinite-sequence-generators","title":"Infinite sequence generators","text":"Function Description range
cycle
iterate
"},{"location":"reference/standard-library/regular-expressions/","title":"Regular Expressions - regex","text":"Regular expressions are a powerful and compact way to find specific patterns in text strings. Clojure provides a simple syntax for Java regex patterns.
#\"pattern\"
is the literal representation of a regular expressions in Clojure, where pattern
is the regular expression.
Create regular expression pattern
(re-pattern pattern)
will return the Clojure literal representation of a given regex pattern.
A string can become a regular expression pattern, e.g. \":\"
becomes the regex pattern #\":\"
(re-pattern \":\")\n
The regular expression syntax cheatsheet by Mozilla is an excellent reference for regular expression patterns.
"},{"location":"reference/standard-library/regular-expressions/#regular-expressions-overview","title":"Regular expressions overview","text":"Regular expressions in Clojure
Find the most common word in a book using regular expressions
Double escaping not required The Clojure syntax means you do not need to double escape special characters, eg. \\\\
, and keeps the patterns clean and simple to read. In other languages, backslashes intended for consumption by the regex compiler must be doubled.
(java.util.regex.Pattern/compile \"\\\\d\")\n;;=> #\"\\d\"\n
The rules for embedding unusual literal characters or predefined character classes are listed in the Javadoc for Pattern.
"},{"location":"reference/standard-library/regular-expressions/#host-platform-support","title":"Host platform support","text":"Clojure runs on the Java Virtual Machine and uses Java regular expressions.
Regular expressions in Clojure create a java.util.regex.Pattern type
(type #\"pattern\")\n;;=> java.util.regex.Pattern\n
ClojureScript runs on JavaScript engines and uses Javascript regular expressions.
"},{"location":"reference/standard-library/regular-expressions/#option-flags","title":"Option flags","text":"Regular expression option flags can make a pattern case-insensitive or enable multiline mode. Clojure's regex literals starting with (?) set the mode for the rest of the pattern. For example, the pattern #\"(?i)yo\"
matches the strings \u201cyo\u201d
, \u201cyO\u201d
, \u201cYo\u201d
, and \u201cYO\u201d
.
Flags that can be used in Clojure regular-expression patterns, along with their long name and a description of what they do. See Java's documentation for the java.util.regex.Pattern class for more details.
Flag Flag Name Description d UNIX_LINES ., ^, and $ match only the Unix line terminator '\\n'. i CASE_INSENSITIVE ASCII characters are matched without regard to uppercase or lower-case. x COMMENTS Whitespace and comments in the pattern are ignored. m MULTILINE ^ and $ match near line terminators instead of only at the beginning or end of the entire input string. s DOTALL . matches any character including the line terminator. u UNICODE_CASE Causes the i flag to use Unicode case insensitivity instead of ASCII. The re-seq function is Clojure's regex workhorse. It returns a lazy seq of all matches in a string, which means it can be used to efficiently test whether a string matches or to find all matches in a string or a mapped file:
(re-seq #\"\\w+\" \"one-two/three\")\n;;=> (\"one\" \"two\" \"three\")\n
The preceding regular expression has no capturing groups, so each match in the returned seq is a string. A capturing group (subsegments that are accessible via the returned match object) in the regex causes each returned item to be a vector:
(re-seq #\"\\w*(\\w)\" \"one-two/three\")\n([\"one\" \"e\"] [\"two\" \"o\"] [\"three\" \"e\"])\n
"},{"location":"reference/standard-library/regular-expressions/#references","title":"References","text":"4Clojure #37 - regular expressions Regex in Clojure - purelyfunctional.tv
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/","title":"Common Regular Expression patterns","text":"Common string formats used in software development and examples of regular expressions to check their correctness.
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#username-regular-expression-pattern","title":"Username Regular Expression Pattern","text":"A 8 to 24 character passwords that can include any lower case character or digit (number). Only the underscore and dash special characters can be used.
(re-matches #\"^[a-z0-9_-]{8,24}$\" \"good-username\")\n
Breakdown the regex pattern:
^[a-z0-9_-]{8,24}$\n\n^ # Start of the line\n [a-z0-9_-] # Match characters and symbols in the list, a-z, 0-9 , underscore , hyphen\n {8,24} # Length at least 8 characters and maximum length of 24\n$ # End of the line\n
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#password-regular-expression-pattern","title":"Password Regular Expression Pattern","text":"A password should be 8 to 24 character string with at least one digit, one upper case letter, one lower case letter and one special symbol, @#$%
.
(re-matches #\"((?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%]).{8,24})\" \"G00d @ username\")\n
The order of the grouping formulas does not matter
Breakdown the regex pattern:
((?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%]).{8,24})\n\n( # Start of group\n (?=.*\\d) # must contains one digit from 0-9\n (?=.*[a-z]) # must contains one lowercase characters\n (?=.*[A-Z]) # must contains one uppercase characters\n (?=.*[@#$%]) # must contains one special symbols in the list \"@#$%\"\n . # match anything with previous condition checking\n {8,24} # length at least 8 characters and maximum of 24\n) # End of group\n
?=
means apply the assertion condition, which is meaningless by itself and works in combination with others.
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#hexadecimal-color-code-regular-expression-pattern","title":"Hexadecimal Color Code Regular Expression Pattern","text":"The string must start with a #
symbol , follow by a letter from a
to f
, A
to Z
or a digit from 0
to 9
with a length of exactly 3 or 6.` This regular expression pattern is very useful for the Hexadecimal web colors code checking.
(re-matches #\"^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$\" \"#FFAABB\")\n
Breakdown the regex pattern:
^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$\n\n^ #start of the line\n # # must contain a \"#\" symbols\n ( # start of group #1\n [A-Fa-f0-9]{3} # any strings in the list, with length of 3\n | # ..or\n [A-Fa-f0-9]{6} # any strings in the list, with length of 6\n ) # end of group #1\n$ #end of the line\n
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#email-regular-expression-pattern","title":"Email Regular Expression Pattern","text":"The account side of an email address starts with _A-Za-z0-9-\\\\+
optional follow by .[_A-Za-z0-9-]
, ending with an @
symbol.
The domain starts with A-Za-z0-9-
, follow by first level domain, e.g .org
, .io
and .[A-Za-z0-9]
optionally follow by a second level domain, e.g. .ac.uk
, .com.au
or \\\\.[A-Za-z]{2,}
, where second level domain must start with a dot .
and length must equal or more than 2 characters.
(re-matches\n #\"^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$\"\n \"jenny.jenn@jetpack.com.au\")\n
Breakdown the regex pattern:
^[_A-Za-z0-9-]+(\\\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9]+(\\\\.[A-Za-z0-9]+)*(\\\\.[A-Za-z]{2,})$\n\n^ #start of the line\n [_A-Za-z0-9-]+ # must start with string in the bracket [ ], must contains one or more (+)\n ( # start of group #1\n \\\\.[_A-Za-z0-9-]+ # follow by a dot \".\" and string in the bracket [ ], must contains one or more (+)\n )* # end of group #1, this group is optional (*)\n @ # must contains a \"@\" symbol\n [A-Za-z0-9]+ # follow by string in the bracket [ ], must contains one or more (+)\n ( # start of group #2 - first level TLD checking\n \\\\.[A-Za-z0-9]+ # follow by a dot \".\" and string in the bracket [ ], must contains one or more (+)\n )* # end of group #2, this group is optional (*)\n ( # start of group #3 - second level TLD checking\n \\\\.[A-Za-z]{2,} # follow by a dot \".\" and string in the bracket [ ], with minimum length of 2\n ) # end of group #3\n$ #end of the line\n
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#hintdouble-escaping-special-characters","title":"Hint::Double escaping special characters","text":"Double escaping of special characters is not required in the Clojure syntax.
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#image-file-name-and-extension-regular-expression-pattern","title":"Image File name and Extension Regular Expression Pattern","text":"A file extension name is 1 or more characters without white space, follow by dot .
and string end in jpg
or png
or gif
or bmp
. The file name extension is case-insensitive.
Change the combination (jpg|png|gif|bmp)
for other file extension.
(re-matches #\"(?i)([^\\s]+(\\.(jpg|png|gif|bmp))$)\" \"clojure-logo.png\")\n
Breakdown the regex pattern:
([^\\s]+(\\.(?i)(jpg|png|gif|bmp))$)\n\n( #Start of the group #1\n [^\\s]+ # must contains one or more anything (except white space)\n ( # start of the group #2\n \\. # follow by a dot \".\"\n (?i) # ignore the case sensitive checking\n ( # start of the group #3\n jpg # contains characters \"jpg\"\n | # ..or\n png # contains characters \"png\"\n | # ..or\n gif # contains characters \"gif\"\n | # ..or\n bmp # contains characters \"bmp\"\n ) # end of the group #3\n ) # end of the group #2\n $ # end of the string\n) #end of the group #1\n
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#hintin-line-modifiers-not-supported-in-javascript","title":"Hint::in-line modifiers not supported in JavaScript","text":"The REPL above uses ClojureScript, hosted on JavaScript. JavaScript does not support in-line modifier flags such as (?i)
for a case insensitive pattern. In-line flags will be converted by the ClojureScript reader if they are the first element in the literal regular expression pattern, or if the js/RegExp
function is used to create the regular expression.
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#ip-address-regular-expression-pattern","title":"IP Address Regular Expression Pattern","text":"An IP address comprises of 4 groups of numbers between 0 and 255, with each group separated by a dot.
Example IP address are: 192.168.0.1
, 127.0.0.1
, 192.120.240.100
(re-matches\n #\"^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])$\"\n \"192.168.0.1\")\n
Breakdown the regex pattern:
^([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])\\\\.([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])\\\\.\n([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])\\\\.([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])$\n\n^ #start of the line\n ( # start of group #1\n [01]?\\\\d\\\\d? # Can be one or two digits. If three digits appear, it must start either 0 or 1\n # e.g ([0-9], [0-9][0-9],[0-1][0-9][0-9])\n | # ...or\n 2[0-4]\\\\d # start with 2, follow by 0-4 and end with any digit (2[0-4][0-9])\n | # ...or\n 25[0-5] # start with 2, follow by 5 and end with 0-5 (25[0-5])\n ) # end of group #2\n \\. # follow by a dot \".\"\n.... # repeat with 3 time (3x)\n$ #end of the line\n
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#time-format-regular-expression-pattern","title":"Time Format Regular Expression Pattern","text":"Time in 12-Hour Format Regular Expression Pattern. The 12-hour clock format start between 0-12, then a semi colon, :
, follow by 00-59
. The pattern ends with am
or pm
.
(re-matches #\"(?i)(1[012]|[1-9]):[0-5][0-9](\\s)?(am|pm)\" \"12:59am\")\n
Breakdown the regex pattern:
(1[012]|[1-9]):[0-5][0-9](\\\\s)?(?i)(am|pm)\n\n( #start of group #1\n 1[012] # start with 10, 11, 12\n | # or\n [1-9] # start with 1,2,...9\n) #end of group #1\n : # follow by a semi colon (:)\n [0-5][0-9] # follow by 0..5 and 0..9, which means 00 to 59\n (\\\\s)? # follow by a white space (optional)\n (?i) # next checking is case insensitive\n (am|pm) # follow by am or pm\n
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#time-in-24-hour-format-regular-expression-pattern","title":"Time in 24-Hour Format Regular Expression Pattern","text":"The 24-hour clock format start between 0-23 or 00-23, then a semi colon :
and follow by 00-59.
(re-matches #\"(([01]?[0-9]|2[0-3]):[0-5][0-9])\" \"23:58\")\n
Breakdown the regex pattern:
([01]?[0-9]|2[0-3]):[0-5][0-9]\n\n( #start of group #1\n [01]?[0-9] # start with 0-9,1-9,00-09,10-19\n | # or\n 2[0-3] # start with 20-23\n) #end of group #1\n : # follow by a semi colon (:)\n [0-5][0-9] # follow by 0..5 and 0..9, which means 00 to 59\n
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#date-format-ddmmyyyy-regular-expression-pattern","title":"Date Format (dd/mm/yyyy) Regular Expression Pattern","text":"Date format in the form dd/mm/yyyy
. Validating a leap year and if there is 30 or 31 days in a month is not simple though.
(re-matches #\"(0?[1-9]|[12][0-9]|3[01])/(0?[1-9]|1[012])/((19|20)\\d\\d)\" \"20/02/2020\")\n
Breakdown the regex pattern:
(0?[1-9]|[12][0-9]|3[01])/(0?[1-9]|1[012])/((19|20)\\\\d\\\\d)\n\n( #start of group #1\n 0?[1-9] # 01-09 or 1-9\n | # ..or\n [12][0-9] # 10-19 or 20-29\n | # ..or\n 3[01] # 30, 31\n) #end of group #1\n / # follow by a \"/\"\n ( # start of group #2\n 0?[1-9] # 01-09 or 1-9\n | # ..or\n 1[012] # 10,11,12\n ) # end of group #2\n / # follow by a \"/\"\n ( # start of group #3\n (19|20)\\\\d\\\\d # 19[0-9][0-9] or 20[0-9][0-9]\n ) # end of group #3\n
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#reference","title":"Reference","text":" - https://mkyong.com/regular-expressions/10-java-regular-expression-examples-you-should-know
"},{"location":"reference/standard-library/regular-expressions/matching-sub-sequences/","title":"Matching sub sequences","text":""},{"location":"reference/standard-library/regular-expressions/matching-sub-sequences/#matching-sub-sequences","title":"Matching sub-sequences","text":"re-seq
returns a lazy seq of all of the matches. The elements of the seq are the results that re-find
would return.
(re-seq #\"s+\" \"Helloween\")\n
"},{"location":"reference/standard-library/regular-expressions/matching-sub-sequences/#most-common-word","title":"Most common word","text":"re-seq
is used in the most common word challenge to split a string into individual words.
Extract from Project Guttenburg the text of The importance of being Earnest by Oscar Wilde. This returns a string of the whole book.
The book is broken down into a collection of individual words using re-seq
and a regular expression pattern.
The collection of words is converted to lower case, so that The
and the
are not counted as separate words. frequencies
returns a collection of tuples, each tuple being a word and a value representing how often it occurs. This collection is sorted by the value in descending order to show the word with the most occurrences at the top.
(->> (slurp \"http://www.gutenberg.org/cache/epub/844/pg844.txt\")\n (re-seq #\"[a-zA-Z0-9|']+\")\n (map #(clojure.string/lower-case %))\n frequencies\n (sort-by val dec))\n
TODO: add link to complete example.
"},{"location":"reference/standard-library/regular-expressions/matching-sub-strings/","title":"Matching sub strings","text":""},{"location":"reference/standard-library/regular-expressions/matching-sub-strings/#matching-sub-strings","title":"Matching sub-strings","text":"re-find
returns the first match within the string, using return values similar to re-matches.
nil
is returned when the pattern does not find a match.
(re-find #\"pump\" \"Halloween\")\n
A matching pattern without groups returns the matched string
(re-find #\"e+\" \"Halloween\")\n
Match with groups returns a vector of results
(re-find #\"s+(.*)(s+)\" \"success\")\n
"},{"location":"reference/standard-library/regular-expressions/matching-with-groups/","title":"Matching with regex groups","text":"rematches
takes a pattern and compares it with a string.
If the pattern does not match the string then nil
is returned to show the function returned a false value.
(re-matches #\"pumpkin\" \"Halloween pumpkin\")\n ```\n\nIf there is an exact match and there are no groups (parens) in the regex, then the matched string is returned.\n\n```clojure\n(re-matches #\"pumpkin\" \"pumpkin\")\n
If the pattern matches but there are groups, a vector of matching strings is returned. The first element in the vector is the entire match. The remaining elements are the group matches.
(re-matches #\"Halloween(.*)\" \"Halloween pumpkin\")\n
"},{"location":"reference/standard-library/regular-expressions/string-replace-with-regex/","title":"String replace with regex","text":""},{"location":"reference/standard-library/regular-expressions/string-replace-with-regex/#string-replace-with-regex-pattern","title":"String replace with regex pattern","text":"clojure.string/replace
takes a string, a pattern and a substring that will replace matching patterns.
(clojure.string/replace \"mississippi\" #\"i..\" \"obb\")\n
Groups can be referred to in the substring replacement
(clojure.string/replace \"mississippi\" #\"(i)\" \"$1$1\")\n
Replace with the value of a function applied to the match:
(clojure.string/replace \"mississippi\" #\"(.)i(.)\"\n (fn [[_ b a]]\n (str (clojure.string/upper-case b)\n \"--\"\n (clojure.string/upper-case a))))\n \"M--SS--SS--Ppi\"\n
clojure.string/replace-first
is a variation where just the first occurrence is replaced.
"},{"location":"reference/standard-library/regular-expressions/string-split-with-regex/","title":"String split with regex","text":""},{"location":"reference/standard-library/regular-expressions/string-split-with-regex/#string-splitting-using-a-regex-pattern","title":"String splitting using a regex pattern","text":"clojure.string/split
takes a string to be split and a pattern to split the string with.
(clojure.string/split \"This is a string that I am splitting.\" #\"\\s+\")\n [\"This\" \"is\" \"a\" \"string\" \"that\" \"I\" \"am\" \"splitting.\"]\n
"},{"location":"reference/standard-library/regular-expressions/string-split-with-regex/#most-common-words-example","title":"Most common words example","text":"Extract a list of the most commonly used English words, returned as a string of words that are separated by a comma.
The #\",\"
regex pattern splits the string of words to form a collection of individual words, each word being its own string.
(def common-english-words\n (set\n (clojure.string/split\n (slurp\n \"http://www.textfixer.com/resources/common-english-words.txt\")\n #\",\")))\n
TODO: add link to complete example.
"},{"location":"reference/standard-library/regular-expressions/sub-expression-matches/","title":"Sub-expression Matches","text":"Pattern Description ^ Matches beginning of line. $ Matches end of line. . Matches any single character except newline. Using m option allows it to match newline as well. [...] Matches any single character in brackets. [^...] Matches any single character not in brackets \\A Beginning of entire string \\z End of entire string \\Z End of entire string except allowable final line terminator. re* Matches 0 or more occurrences of preceding expression. re+ Matches 1 or more of the previous thing re? Matches 0 or 1 occurrence of preceding expression. re{ n} Matches exactly n number of occurrences of preceding expression. re{ n,} Matches n or more occurrences of preceding expression. re{ n, m} Matches at least n and at most m occurrences of preceding expression. a b (re) Groups regular expressions and remembers matched text. (?: re) Groups regular expressions without remembering matched text. (?> re) Matches independent pattern without backtracking. \\w Matches word characters. \\W Matches nonword characters. \\s Matches whitespace. Equivalent to [\\t\\n\\r\\f]. \\S Matches nonwhitespace. \\d Matches digits. Equivalent to [0-9]. \\D Matches nondigits. \\A Matches beginning of string. \\Z Matches end of string. If a newline exists, it matches just before newline. \\z Matches end of string. \\G Matches point where last match finished. \\n Back-reference to capture group number \"n\" \\b Matches word boundaries when outside brackets. Matches backspace (0x08) when inside brackets. \\B Matches nonword boundaries. \\n, \\t, etc. Matches newlines, carriage returns, tabs, etc. \\Q Escape (quote) all characters up to \\E \\E Ends quoting begun with \\Q"},{"location":"reference/tagged-literals/","title":"Tagged Literals","text":"Frequently used value types are afforded a \"tagged literal\" syntax. It is similar to a constructor, but this special syntax makes it de/serializable and easier to read at the REPL.
Tagged literals start with a # followed by a symbol and a literal:
#js [...]
- JavaScript array literal
#js {...}
- JavaScript object literal
#inst \"...\"
- JavaScript date literal
#uuid \"...\"
- UUID literal
#queue [...]
- queue literal
"},{"location":"reference/tagged-literals/uuid/","title":"uuid tag literal","text":"A universally unique identifier (UUID).
#uuid \"8-4-4-4-12\" - numbers represent the number of hex digits\n#uuid \"97bda55b-6175-4c39-9e04-7c0205c709dc\" - actual example\n
Representing UUIDs with #uuid rather than just a plain string has the following benefits:
the reader will throw an exception on malformed UUIDs\nits UUID type is preserved and shown when serialized to edn.\n
"},{"location":"reference/tagged-literals/uuid/#creating-uuids-clojure","title":"Creating UUIDs - Clojure","text":"In Clojure, call the randomUUID method of the java.util.UUID class
(java.util.UUID/randomUUID)\n
This returns a UUID tagged literal.
(java.util.UUID/randomUUID)\n;; => #uuid \"44f3ffd7-6702-4b8a-af25-11bee4b5ec4f\"\n
Looking at the type we can see its a Java object from the java.util.UUID class:
(type (java.util.UUID/randomUUID))\n;; => java.util.UUID\n
"},{"location":"reference/tagged-literals/uuid/#creating-uuids-clojurescript","title":"Creating UUIDs - ClojureScript","text":"Randomly generate a UUID in ClojureScript:
cljs.core/random-uuid
To label a value as a UUID:
cljs.core/uuid
"},{"location":"reference/tagged-literals/uuid/#hintuuid-does-not-validate-the-value","title":"Hint::uuid does not validate the value","text":"The ClojureScript documentation states that uuid? does not perform validation.
"},{"location":"reference/tagged-literals/uuid/#testing-for-a-uuid","title":"Testing for a uuid","text":"uuid?
tests a given value and returns true if it is a uuid tagged literal value.
tagged-literal?
is the more general function for any tagged values.
"},{"location":"simple-projects/","title":"Small Projects","text":"An effective way to get comfortable with Clojure is to start writing small projects. In this section several small projects are used to walk the audience through how to create and develop a project, as well as learn some Clojure functions and functional programming techniques along the way.
Project Topics Description Random Clojure Function namespace vars print a random function from the Clojure standard library Encoding and decoding hash-map dictionaries transforming messages between one form and another Data Transformation transforming larger and larger data sets Test Driven Development and Kata Unit testing Unit testing and solving challenges using different approaches Create a Clojure project
Clojure CLI tools and clj-new to create a new Clojure project.
"},{"location":"simple-projects/generate-web-page/","title":"Generate Web Page","text":"Generate a web page from Clojure code, using Hiccup
Generate a full HTML webpage with content.
Add a CSS library (bulma.io, bootstrap) to improve generation
"},{"location":"simple-projects/generate-web-page/#summary","title":"Summary","text":"Generating a web page in Clojure shows how easy it is to structure data and transform that data into other structures.
Although this kind of project is easy enough to just do in a REPL directly, using a Clojure aware editor with a Clojure project makes changes to the code far simpler, without loosing any of the immediate feedback of the REPL.
Most Clojure developers use the REPL by evaluating code in the editor showing the source code from the project.
Practicalli Web Services book shows how to build websites, create self-documented API's, manage Web Application servers and use databases to persist data.
"},{"location":"simple-projects/random-clojure-function/","title":"Random Clojure Function","text":"A simple application that returns a random function from the clojure.core
namespace, along with the function argument list and its description (from the doc-string)
There are 659 functions in clojure.core
namespace and 955 in the standard library (as of June 2020). These functions are learned over time as experience is gained with Clojure.
Project: Random Clojure function practicalli/random-clojure-function repository contains a Clojure project with an example solution
"},{"location":"simple-projects/random-clojure-function/#live-coding-video-walk-through","title":"Live Coding Video walk-through","text":"A Live coding video walk-through of this project shows how this application was developed, using Spacemacs editor and CircleCI for continuous integration.
-M flag superseeds -A flag The -M
flag replaced the -A
flag when running code via clojure.main
, e.g. when an alias contains :main-opts
. The -A
flag should be used only for the specific case of including an alias when starting the Clojure CLI built-in REPL.
"},{"location":"simple-projects/random-clojure-function/#create-a-project","title":"Create a project","text":"Use the :project/create
Practicalli Clojure CLI Config to create a new Clojure project.
clojure -T:project/create :template app :name practicalli/random-function\n
This project has a deps.edn
file that includes the aliases
:test
- includes the test/
directory in the class path so unit test code is found :runner
to run the Cognitect Labs test runner which will find and run all unit tests
"},{"location":"simple-projects/random-clojure-function/#repl-experiments","title":"REPL experiments","text":"Open the project in a Clojure-aware editor or start a Rebel terminal UI REPL
Clojure EditorRebel REPL Open the src/practicalli/random-function.clj
file in a Clojure aware editor and start a REPL process (jack-in)
Optionally create a rich comment form that will contain the expressions as the design of the code evolves, moving completed functions out of the comment forms so they can be run by evaluating the whole namespace.
(ns practicalli.random-function)\n\n(comment\n ;; add experimental code here\n)\n
Open a terminal and change to the root of the Clojure project created, i.e. the directory that contains the deps.edn
file.
Start a REPL that provides a rich terminal UI
clojure -M:repl/reloaded\n
require
will make a namespace available from within the REPL (require '[practicalli.random-function])\n
Change into the random-function
namespace to define functions (in-ns 'practicalli.random-function')\n
Reload changes made to the src/practicalli/random_function.clj
file using the require
function with the :reload
option. :reload
forces the loading of all the definitions in the namespace file, overriding any functions of the same name in the REPL. (require '[practicalli.random-function] :reload)\n
Copy finished code into the source code files
Assuming the code should be kept after the REPL is closed, save the finished versions of function definitions into the source code files. Use Up and Down keys at the REPL prompt to navigate the history of expressions
List all the public functions in the clojure.core
namespace using the ns-publics
function
(ns-publics 'clojure.core)\n
The hash-map keys are function symbols and the values are the function vars
{+' #'clojure.core/+',\n decimal? #'clojure.core/decimal?,\n sort-by #'clojure.core/sort-by,\n macroexpand #'clojure.core/macroexpand\n ,,,}\n
The meta
function will return a hash-map of details about a function, when given a function var.
(meta #'map)\n
The hash-map has several useful pieces of information for the application, including :name
, :doc
, and :arglists
;; => {:added \"1.0\",\n;; :ns #namespace[clojure.core],\n;; :name map,\n;; :file \"clojure/core.clj\",\n;; :static true,\n;; :column 1,\n;; :line 2727,\n;; :arglists ([f] [f coll] [f c1 c2] [f c1 c2 c3] [f c1 c2 c3 & colls]),\n;; :doc\n;; \"Returns a lazy sequence consisting of the result of applying f to\\n the set of first items of each coll, followed by applying f to the\\n set of second items in each coll, until any one of the colls is\\n exhausted. Any remaining items in other colls are ignored. Function\\n f should accept number-of-colls arguments. Returns a transducer when\\n no collection is provided.\"}\n
To use the meta
function, the values from the ns-publics
results should be used.
(vals (ns-publics 'clojure.core))\n
rand-nth
will return a random function var from the sequence of function vars
(rand-nth (vals (ns-publics 'clojure.core)))\n
A single function var is returned, so then the specific meta data can be returned.
(meta (rand-nth (vals (ns-publics 'clojure.core))))\n
"},{"location":"simple-projects/random-clojure-function/#define-a-name-for-all-functions","title":"Define a name for all functions","text":"Edit the src/practicalli/random-function.clj
file and define a name for the collection of all public functions from clojure.core
(def standard-library\n \"Fully qualified function names from clojure.core\"\n (vals (ns-publics 'clojure.core)))\n
"},{"location":"simple-projects/random-clojure-function/#write-unit-tests","title":"Write Unit Tests","text":"From the REPL experiments we have a basic approach for the application design, so codify that design by writing unit tests. This will also highlight regressions during the course of development.
Edit the file test/practicalli/random_function_test.clj
and add unit tests.
The first test check the standard-library-functions contains entries.
The second test checks the -main function returns a string (the function name and details).
src/practicalli/random_function_test.clj(ns practicalli.random-function-test\n (:require [clojure.test :refer [deftest is testing]]\n [practicalli.random-function :as random-fn]))\n\n(deftest standard-library-test\n (testing \"Show random function from Clojure standard library\"\n (is (seq random-fn/standard-library-functions))\n (is (string? (random-fn/-main)))))\n
"},{"location":"simple-projects/random-clojure-function/#update-the-main-function","title":"Update the main function","text":"Edit the src/practicalli/random-function.clj
file. Change the -main
function to return a string of the function name and description.
src/practicalli/random-function.clj(defn -main\n \"Return a function name from the Clojure Standard library\"\n [& args]\n (let [function-details (meta (rand-nth standard-library-functions))]\n (str (function-details :name) \"\\n \" (function-details :doc)))\n )\n
Cognitect Test Runner Run the tests with the Congnitect test runner via the test
function in the build.clj
file. ```shell clojure -T:build test
```
Kaocha Test Runner Run the tests with the Kaocha test runner using the alias :test/run
from Practicalli Clojure CLI config ```shell clojure -M:test/run
```
"},{"location":"simple-projects/random-clojure-function/#running-the-application","title":"Running the application","text":"Use the clojure command with the main namespace of the application. Clojure will look for the -main function and evaluate it.
clojure -M -m practicalli.random-function\n
This should return a random function name and its description. However, nothing is returned. Time to refactor the code.
"},{"location":"simple-projects/random-clojure-function/#improving-the-code","title":"Improving the code","text":"The tests pass, however, no output is shown when the application is run.
The main function returns a string but nothing is sent to standard out, so running the application does not return anything.
The str
expression could be wrapped in a println, although that would make the result harder to test and not be very clean code. Refactor the -main
to a specific function seems appropriate.
Replace the -main-test
with a random-function-test
that will be used to test a new function of the same name which will be used for retrieving the random Clojure function.
(deftest random-function-test\n (testing \"Show random function from Clojure standard library\"\n (is (seq SUT/standard-library-functions))\n (is (string? (SUT/random-function SUT/standard-library-functions)))))\n
Create a new function to return a random function from a given collection of functions, essentially moving the code from -main
.
The function extracts the function :name
and :doc
from the metadata of the randomly chosen function.
(defn random-function\n [function-list]\n (let [function-details (meta (rand-nth function-list))]\n (str (function-details :name) \"\\n \" (function-details :doc) \"\\n \")))\n
Update the main function to call this new function.
(defn -main\n \"Return a function name from the Clojure Standard library\"\n [& args]\n (println (random-function standard-library-functions)))\n
Run the tests again.
If the tests pass, then run the application again
clojure -M -m practicalli.random-function\n
A random function and its description are displayed.
"},{"location":"simple-projects/random-clojure-function/#adding-the-function-signature","title":"Adding the function signature","text":"Edit the random-function
code and add the function signature to the string returned by the application.
Format the code so it is in the same structure of the output it produces, making the code clearer to understand.
(defn random-function\n [function-list]\n (let [function-details (meta (rand-nth function-list))]\n (str (function-details :name)\n \"\\n \" (function-details :doc)\n \"\\n \" (function-details :arglists))))\n
"},{"location":"simple-projects/random-clojure-function/#add-more-namespaces","title":"Add more namespaces","text":"All current namespaces on the classpath can be retrieved using the all-ns
function. This returns a lazy-seq, (type (all-ns))
(all-ns)\n
Using the list of namespace the ns-publics
can retrieve all functions across all namespaces.
Create a helper function to get the functions from a namespace, as this is going to be used in several places.
(defn function-list\n [namespace]\n (vals (ns-publics namespace)))\n
This function can be mapped over all the namespaces to get a sequence of all function vars. Using map
creates a sequence for each namespace, returned as a sequence of sequences. Using mapcat
will concatenate the nested sequences and return a flat sequence of function vars.
(mapcat #(vals (ns-publics %)) (all-ns))\n
Bind the results of this expression to the name all-public-functions
.
(def available-namespaces\n (mapcat #(vals (ns-publics %)) (all-ns)))\n
"},{"location":"simple-projects/random-clojure-function/#control-which-namespaces-are-consulted","title":"Control which namespaces are consulted","text":"There is no way to control which library we get the functions from, limiting the ability of our application.
Refactor the main namespace to act differently based on arguments passed:
-
If no arguments are passed then all public functions are used to pull a random function from.
-
If any argument is passed, the argument should be used as the namespace to pull a random function from. The argument is assumed to be a string.
ns-publics
function needs a namespace as a symbol, so the symbol
function is used to convert the argument.
(symbol \"clojure.string\")\n
The -main
function uses [& args]
as a means to take multiple arguments. All arguments are put into a vector, so the symbol function should be mapped over the elements in the vector to create symbols for all the namespaces.
Use an anonymous function to convert the arguments to symbols and retrieve the list of public functions from each namespace. This saves mapping over the arguments twice.
mapcat
the function-list function over all the namespaces, converting each namespace to a symbol.
(mapcat #(function-list (symbol %)) args)\n
Update the main function with an if statement. Use seq
as the condition to test if a sequence (the argument vector) has any elements (namespaces to use).
If there are arguments, then get the functions for the specific namespaces.
Else return all the functions from all the namespaces.
(defn -main\n \"Return a function name from the Clojure Standard library\"\n [& args]\n (if (seq args)\n (println (random-function (mapcat #(function-list (symbol %)) args)))\n (println (random-function standard-library-functions))))\n
"},{"location":"simple-projects/random-clojure-function/#use-the-fully-qualified-name-for-the-namespace","title":"Use the fully qualified name for the namespace","text":"Now that functions can come from a range of namespaces, the fully qualified namespace should be used for the function, eg. domain/namespace
(:ns (meta (rand-nth standard-library-functions)))\n
Update the random function to return the domain part of the namespace, separated by a /
(defn random-function\n [function-list]\n (let [function-details (meta (rand-nth function-list))]\n (str (function-details :ns) \"/\" (function-details :name)\n \"\\n \" (function-details :arglists)\n \"\\n \" (function-details :doc))))\n
"},{"location":"simple-projects/random-clojure-function/#use-all-available-namespaces-by-default","title":"Use all available namespaces by default","text":"Define a name to represent the collection of all available namespaces, in the context of the running REPL.
(def all-public-functions\n \"Fully qualified function names from available\"\n (mapcat #(vals (ns-publics %)) (all-ns)))\n
Update the -main
function to use all available namespaces if no arguments are passed to the main function.
(defn -main\n \"Return a random function and its details\n from the available namespaces\"\n [& args]\n (if (seq args)\n (println (random-function (mapcat #(function-list (symbol %)) args)))\n (println (random-function all-public-functions))))\n
"},{"location":"simple-projects/random-clojure-function/#follow-on-idea-convert-to-a-web-service","title":"Follow-on idea: Convert to a web service","text":"Add http-kit server and send information back as a plain text, html, json and edn
"},{"location":"simple-projects/random-clojure-function/#follow-on-idea-convert-to-a-library","title":"Follow-on idea: Convert to a library","text":"Convert the project to a library so this feature can be used as a development tool for any project.
Add functionality to list all functions from all namespaces or a specific namespace, or functions from all namespaces of a particular domain, e.g practicalli
or practicalli.app
"},{"location":"simple-projects/split-the-bill/","title":"Split the bill","text":"In a restaurant a group of friends and relatives are having a reunion dinner after a year of not seeing each other.
Once the meal comes to an end, its time to pay the bill. So how would you write code to split the bill?
Start with the simplest possible approach, with everyone paying the same.
"},{"location":"simple-projects/split-the-bill/#create-a-new-clojure-project","title":"Create a new Clojure project","text":" Pracitcalli Clojure CLI Config provides the :project/create
alias to create projects using deps-new project.
clojure -T:project/create :template app :name practicalli/split-the-bill\n
(str \"Create code to calculate the bill, including what each person should pay\")\n
Tke a look at the Who am I section for ideas on how to model the bill. Also look at More Than Average for ideas on how to write code to work out how to pay the bill.
"},{"location":"simple-projects/split-the-bill/#paying-what-was-ordered","title":"Paying what was ordered","text":"As not everyone had eaten the same amount of food or arrived at the same time, then there was an ask for everyone to pay just what they ordered.
So create a collection to capture what each person ordered and create an itemised bill so each person knows what they should pay.
Define a detailed bill based on what each person ordered, then create an itemised bill based on each persons order
Now it was realised that what everyone ordered is not what everyone ate. So now we need to take the order and create an itemised bill based on what everyone actually ate (lets suspend believe here a little and assume everyone knows exactly what they ate, and is honest about it).
Define a detailed bill based on what each person ordered, then create an itemised bill based on each person actually ate
"},{"location":"simple-projects/split-the-bill/#spliting-the-bill-with-a-social-group","title":"Spliting the bill with a Social Group","text":"Extend the exercise by splitting bills over multiple events and activities with multiple people.
"},{"location":"simple-projects/tripple-lock/","title":"Triple Lock","text":"A new safe too keep all the richest you will gain from becoming a Clojure developer (hopefully). The safe has a 3 combination lock to protect your new found wealth, but just how safe is the safe?
"},{"location":"simple-projects/tripple-lock/#create-a-new-clojure-project","title":"Create a new Clojure project","text":" Pracitcalli Clojure CLI Config provides the :project/create
alias to create projects using deps-new project.
clojure -T:project/create :template app :name practicalli/triple-lock\n
"},{"location":"simple-projects/tripple-lock/#designing-the-combination-lock","title":"Designing the combination lock","text":"Lets consider how we would create such a combination lock in Clojure.
- The combination is managed by three tumbler wheels
- Each tumbler wheel has the same range of numbers on then, 0 to 9
Each tumbler wheel could have all the numbers it contains within a Collection in Clojure. The simplest approach would be to put the numbers 0 to 9 into a Vector (an array-like collection).
[0 1 2 3 4 5 6 7 8 9]\n
As the numbers on the tumbler wheel are just a range between 0 and 9, then rather than type out all the numbers we can use the range
function to generate all the numbers for us.
When we give the range function one argument, it will create all the whole numbers from 0 to the number before that of the argument. In the following example, we give range
the argument of 10 and we receive the numbers from 0 to 9.
(range 10)\n
You can also give range
two arguments, such as '(range 5 15)'.
Be careful not to call the range
function by itself, or it will try and generate an infinite range of numbers (until your computer memory is all used up).
"},{"location":"simple-projects/tripple-lock/#create-all-the-combinations","title":"Create all the Combinations","text":"Complete the following code (replacing the ,,,) to generate all the possible combinations of the lock
(for [tumbler-1 (range 10)\n ,,, ,,,\n ,,, ,,,]\n [tumbler-1 ,,, ,,,])\n
Instead of showing all the possible combinations, count all the combinations and return the total number of combinations
Take the code from the combinations and wrap it in the count function
;; now count the possible combinations\n(count\n\n )\n
To make our lock harder to break into, we should only allow the combinations where each tumbler wheel has a different number. So you cannot have combinations like 1-1-1, 1-2-2, 1-2-1, etc.
How many combinations does that give us?
Complete the following code to create a 3-tumbler wheel combination lock, where none of the numbers are the same
Hint: Beware not to enter (range) without an argument as Clojure may try and evaluate infinity
(count (for [tumbler-1 (range 10)\n ,,, ,,,\n ,,, ,,,\n :when (or (= tumbler-1 tumbler-2)\n ,,,\n ,,,)]\n [tumbler-1 ,,, ,,,]))\n
Suggested solution Suggested solution to the completed 3-lock challenges.
;; a 3 combination padlock\n\n;; model the combinations\n(for [tumbler-1 (range 10)\n tumbler-2 (range 10)\n tumbler-3 (range 10)]\n [tumbler-1 tumbler-2 tumbler-3])\n\n\n;; now count the possible combinations\n(count (for [tumbler-1 (range 10)\n tumbler-2 (range 10)\n tumbler-3 (range 10)]\n [tumbler-1 tumbler-2 tumbler-3]))\n\n\n(count (for [tumbler-1 (range 10)\n tumbler-2 (range 10)\n tumbler-3 (range 10)\n :when (or (= tumbler-1 tumbler-2)\n (= tumbler-2 tumbler-3)\n (= tumbler-3 tumbler-1))]\n [tumbler-1 tumbler-2 tumbler-3]))\n\n;; lets look at the combinations again, we can see that there is always at least 2 matching values. This is probably the opposite of what we want in real life.\n(for [tumbler-1 (range 10)\n tumbler-2 (range 10)\n tumbler-3 (range 10)\n :when (or (= tumbler-1 tumbler-2)\n (= tumbler-2 tumbler-3)\n (= tumbler-3 tumbler-1))]\n [tumbler-1 tumbler-2 tumbler-3])\n
"},{"location":"simple-projects/data-transformation/","title":"Data Transformation","text":"In a sense all Clojure project are about data transformation, however, these projects will introduce you to many techniques used to transform larger and larger data sets.
Project Topics Overview Most common word regex filter re-seq sort-by Find the most common word in a give book that is not a common English word"},{"location":"simple-projects/data-transformation/most-common-word/","title":"Most common word","text":"In this challenge we would like you to find the most used word in a book. The word should not be part of the common English words (i.e. the, a, i, is).
This functionality is useful for generating word maps or identifying patterns across data sets.
Copyright free books for use are available via Project Guttenburg, e.g. \u201cThe Importance of Being Earnest\u201d by Oscar Wilde.
"},{"location":"simple-projects/data-transformation/most-common-word/#suggested-approach","title":"Suggested approach","text":"A suggested approach to find the most common word:
- Pull the content of the book into a collection
- Use a regular expression to create a collection of individual words - eg.
#\u201d[a-zA-Z0-9|\u2019]+\u201d
- Remove the common English words used in the book
- Convert all the words to lower case so they match with common words source
- Count the occurrences of the remaining words (eg. each word is associated with the number of times it appears in the book)
- Sort the words by the number of the occurrences
- Reverse the collection so the most commonly used word is shown first
"},{"location":"simple-projects/data-transformation/most-common-word/#create-a-project","title":"Create a project","text":" Pracitcalli Clojure CLI Config provides the :project/create
alias to create projects using deps-new project.
clojure -T:project/create :template app :name practicalli/common-words\n
"},{"location":"simple-projects/data-transformation/most-common-word/#get-the-book-contents","title":"Get the book contents","text":"clojure.core/slurp
will read in a local file or a remote resource (file, web page, etc) and return a single string of the contents.
(slurp \"https://www.gutenberg.org/files/844/844-0.txt\")\n
Wrap the slurp
expression in a def
to bind a name to the book.
(def being-earnest (slurp \"https://www.gutenberg.org/files/844/844-0.txt\"))\n
Project Gutenberg now compresses the books with GZip, so a stream can be created to read the file and decompress it. Then slurp is used to read in the uncompressed text of the book into a string.
(def being-earnest\n (with-open [uncompress-text (java.util.zip.GZIPInputStream.\n (clojure.java.io/input-stream\n \"https://www.gutenberg.org/cache/epub/844/pg844.txt\"))]\n (slurp uncompress-text)))\n ```\n\n## Individual words from the book\n\nThe book contents should be broken down into individual words.\n\nA regular expression can be used to identify word boundaries, especially where there are apostrophes and other characters.\n\n`clojure.core/re-seq` returns a new lazy sequence containing the successive matches of a pattern from a given string. So given a sentence\n\nUsing `re-seq` to convert the first sentence of the `being-earnest` book using a regex word boundary pattern, `\\w+`.\n\n```clojure\n(re-seq #\"\\w+\" \"Morning-room in Algernon's flat in Half-Moon Street.\")\n\n;; => (\"Morning\" \"room\" \"in\" \"Algernon\" \"s\" \"flat\" \"in\" \"Half\" \"Moon\" \"Street\")\n
The result is a sequence of the individual words, however, the hyphenated words and the apostrophes have been split into separate words.
Extending the regex pattern the results can be refined.
(re-seq #\"[\\w|'-]+\" \"Morning-room in Algernon's flat in Half-Moon Street.\")\n\n;; => (\"Morning-room in Algernon's flat in Half-Moon Street\")\n
(re-seq #\"[\\w|'-]+\" being-earnest)\n
The #\"[\\w|'-]+\" is the same pattern as the more explicit pattern #\"[a-zA-Z0-9|'-]+\"
"},{"location":"simple-projects/data-transformation/most-common-word/#removing-common-english-words","title":"Removing common English words","text":"In any book the most common word its highly likely to be a common English word (the, a, and, etc.). To make the most common word in any book more specific, the common English words should be removed.
common-english-words.csv
contains comma separate words.
Using slurp and a regular expression the individual words can be extracted into a collection.
Rather than re-seq
the clojure.string/split
can be used. This is a more specific function for splitting a string using a regular expression pattern, in this case the pattern for a comma, #\",\"
.
(clojure.string/split (slurp \"common-english-words.csv\") #\",\")\n
An additional step is to place the common English words into a Clojure set, a data structure which contains a unique set of values.
(set (clojure.string/split (slurp \"common-english-words.csv\") #\",\"))\n
The advantage of using a set for the common English words is that the data structure can be used as a predicate to remove matching words. So a common English words set can be used to remove the common English words from being-earnest
book.
Define a name for the common English words set.
(def common-english-words (set (clojure.string/split (slurp \"common-english-words.csv\") #\",\")))\n
This can also be written using the threading macro, to show the sequential nature of the data transformation.
(def common-english-words\n (-> (slurp \"common-english-words.csv\")\n (clojure.string/split #\",\")\n set))\n
The common-english-words
set can now be used with the being-earnest
book.
(remove common-english-words (re-seq #\"[\\w|'-]+\" being-earnest))\n
"},{"location":"simple-projects/data-transformation/most-common-word/#counting-occurrences","title":"Counting Occurrences","text":"clojure.core/frequencies
takes a collection and returns a map where the keys are the unique elements from the collection and the value for each key is the number of times that element occurred in the collection.
(filter (remove common-english-words (re-seq #\"[\\w|'-]+\" being-earnest)))\n
The resulting hash-map is not in any order. clojure.core/sort-by
will return the same results but sorted by a given function. To sort a hash-map the key
and val
functions are function that will sort by key and value respectively. As it is the value that has the number of occurrences, then val
is the function to use.
(sort-by val (filter (remove common-english-words (re-seq #\"[\\w|'-]+\" being-earnest))))\n
The result is sorted from smallest to largest value. The result could be reversed using clojure.core/reverse
or by supplying an extra function to the sort-by
expression. Using greater-than, >
the result will be returned in descending order.
(sort-by val dec (filter (remove common-english-words (re-seq #\"[\\w|'-]+\" being-earnest))))\n
"},{"location":"simple-projects/data-transformation/most-common-word/#assembling-the-most-common-word-function","title":"Assembling the most-common-word function","text":"Define a function called most-common-word
that assembles all the previous steps. The function should take all the values it needs for the calculation as arguments, creating a pure function without side effects.
(defn most-common-word\n [book common-words]\n (sort-by val >\n (frequencies\n (remove common-words\n (map #(clojure.string/lower-case %)\n (re-seq #\"[\\w|'-]+\" book))))))\n
This may seem a little hard to parse, so the function definition can be re-written using a threading macro.
(defn most-common-word\n [book common-words]\n (->> book\n (re-seq #\"[\\w|'-]+\" ,,,)\n (map #(clojure.string/lower-case %))\n (remove common-words)\n frequencies\n (sort-by val >)))\n
Call this function with the being-earnest
book and the common-english-words
(most-common-word being-earnest common-english-words)\n
"},{"location":"simple-projects/data-transformation/most-common-word/#running-from-the-command-line","title":"Running from the command line","text":"Update the code to take the book reference from the command line.
Remove the def
that hard-coded the being-earnest book.
In the most-common-word
wrap the book with slurp
to read the book reference in and convert it to a string, to be processed by the rest of the expressions.
Add a -main
function that takes a reference for the source of the book and the source of the common words.
(ns practicalli.common-word)\n\n(defn decode-book\n [book-gzip]\n (with-open\n [uncompress-text (java.util.zip.GZIPInputStream.\n (clojure.java.io/input-stream book-gzip))]\n (slurp uncompress-text)))\n\n(defn common-words\n [csv]\n (-> (slurp csv)\n (clojure.string/split #\",\")\n set))\n\n(defn most-common-word\n [book-gzip common-words]\n (->> (decode book-gzip)\n (re-seq #\"[\\w|'-]+\")\n (map #(clojure.string/lower-case %))\n (remove common-words)\n frequencies\n (sort-by val >)))\n\n(defn -main\n [book-gzip common-word-csv]\n (most-common-word book-gzip (common-words common-word-csv)))\n
Now call the code on the command line.
clojure -m practicalli.common-word \"https://www.gutenberg.org/cache/epub/844/pg844.txt\" \"common-english-words.csv\"\n
"},{"location":"simple-projects/encode-decode/","title":"Encoding and Decoding with Clojure","text":"Projects that use a range of ciphers, from simple to more complex, to encode and decode text.
A common approach to encoding and decoding text is to use a dictionary lookup, defined in Clojure as a hash-map. Each key-value pair provides a mapping for encoding and decoding. Looking up a a character as a key in the map provides a value that is the encrypted character.
These projects show several ways to transform data in Clojure.
Project Topics Description Boolean names to 0 or 1 hash-map get Convert boolean values to classic 1 or 0 values Caesar cipher - ROT13 seq cycle zipmap A simple alphabet rotation cipher RNA / DNA converter Convert between DNA and RNA Clacks telegram Encoding and decoding messages with Clacks"},{"location":"simple-projects/encode-decode/#examples-of-encoding","title":"Examples of Encoding","text":" - Portable Network Graphics for image compression
- Vorbis for music and video compression plus several commercial compression encoders
- Enigma machine - encrypted communications
"},{"location":"simple-projects/encode-decode/caesar-cipher-rot13/","title":"Caesar Cipher ROT13","text":"ROT13 is one of the simplest ciphers which uses an alphabet as a circle of characters, swapping each character with a character 13 positions later in the alphabet, assuming 26 character of an English alphabet.
A dictionary can be generated to translate between the original alphabet and the rotated alphabet, providing a simple way to generate an encrypted message.
"},{"location":"simple-projects/encode-decode/caesar-cipher-rot13/#create-a-project","title":"Create a project","text":" Pracitcalli Clojure CLI Config provides the :project/create
alias to create projects using deps-new project.
clojure -T:project/create :template app :name practicalli/caesar-cipher\n
"},{"location":"simple-projects/encode-decode/caesar-cipher-rot13/#define-an-alphabet","title":"Define an alphabet","text":"Define an alphabet to use as a basis for conversion. Take the string of all characters and convert to a sequence of character types.
src/practicalli/caesar-cipher.clj(def english-alphabet\n (seq \"abcdefghijklmnopqrstuvwxyz\"))\n
"},{"location":"simple-projects/encode-decode/caesar-cipher-rot13/#generate-a-cypher","title":"Generate a cypher","text":"To convert a character, first build up a cypher. A cypher in this case is simply a hash-map that creates a dictionary lookup defining what each character should be changed to.
cycle
creates a lazy sequence of the alphabet that continually cycles. This provides an 'infinite' sequence from which we will take only the characters needed.
(cycle \"abcdefghijklmnopqrstuvwxyz\")\n
The dictionary is composed of the original alphabet and a new alphabet that is offset by 13 characters, half the number of characters in the dictionary.
(drop 13 (cycle alphabet))
will drop the first 13 characters. As cycle
creates an 'infinite' alphabet, there are still plenty of characters to make a second alphabet.
(take 26 (drop 13 (cycle alphabet)))
will get a new alphabet of 26 characters, starting from the 14th character, n
.
zipmap
is used to join two collections together to form a hash-map, e.g. the lookup dictionary. In this case the original alphabet and the new alphabet.
(zipmap alphabet (take 26 (drop 13 (cycle alphabet))))
This expression is nested and can be harder to parse by those new to Clojure. The code can be written using a threading marco, that demonstrated the flow of data transformation.
Using the thread last macro, ->>
, the result of each expression becomes the last argument for the next expression.
(->> (cycle alphabet)\n (drop 13)\n (take 26)\n (zipmap alphabet))\n
Using the clojure.core/replace function with the cypher hash-map and a string of text returns a converted string of text.
"},{"location":"simple-projects/encode-decode/caesar-cipher-rot13/#define-a-function","title":"Define a function","text":"Define a rot13
function with the algorithm created. The function takes the alphabet and the text to be encrypted. Passing both pieces of data as arguments ensures that the function is pure, i.e. free from side effects.
src/practicalli/caesar-cipher.clj(defn rot13 [alphabet text]\n (let [cipher (->> (cycle alphabet)\n (drop 13)\n (take 26)\n (zipmap alphabet))]\n (apply str (replace cipher text))))\n
Call the rot13 function with the english-alphabet
and a sentence as a string.
(rot13 english-alphabet \"The Quick Brown Fox Jumped Over The Lazy Dog!\")\n
An encrypted copy of the sentence is returned.
"},{"location":"simple-projects/encode-decode/caesar-cipher-rot13/#idiomatic-improvements","title":"Idiomatic improvements","text":"clojure.string
library is more idiomatic approach when working with string types.
In the practicalli.cypher-rot13
solution apply str
was used to join a sequence of characters into a string. clojure.string/join
combines a sequence of characters into a string.
Require the clojure.string
namespace to use the functions contained within. Add the require to the namespace definition of practicalli.cypher-rot13
src/practicalli/caesar-cipher.clj(ns practicalli.ceaser-cypher\n (:gen-class)\n (:require [clojure.string :as string]))\n
Update the rot13
function to use clojure.string/join
rather than apply str
.
(defn rot13 [alphabet text]\n (let [cipher (->> (cycle alphabet)\n (drop 13)\n (take 26)\n (zipmap alphabet))]\n (string/join (replace cipher text))))\n
"},{"location":"simple-projects/encode-decode/clacks/","title":"Clacks Messages","text":"In the 33rd Discworld novel, Going Postal, messages are sent faster than a speeding horse via the Clacks system. The Clacks system composes of a series of towers spread across a continent with each tower sending a light signal to the next tower using combinations of lights for each character in the message. Each tower sees a grid of lights from a distant tower and sends the message on to the next tower.
The Clacks system was introduced in the 24th Discworld novel called \"The Fifth Elephant\". \"Going Postal\" elaborates the full history of the Clacks system.
"},{"location":"simple-projects/encode-decode/clacks/#the-challenge","title":"The Challenge","text":"Create a Clacks encoder that converts any English language message into its corresponding clacks signal, based on the Clacks alphabet as defined by the board game of the same name.
The board game defines the alphabet as a 2 by 3 grid (although in the Discworld its actually 8 large squares). Naturally, the interpreter also converts the Clacks signal back into an English message too.
"},{"location":"simple-projects/encode-decode/clacks/#create-a-project","title":"Create a project","text":" Pracitcalli Clojure CLI Config provides the :project/create
alias to create projects using deps-new project.
clojure -T:project/create :template app :name practicalli/clacks-messenger\n
This project has a deps.edn
file that includes the aliases
:test
- includes the test/
directory in the class path so unit test code is found :runner
to run the Cognitect Labs test runner which will find and run all unit tests
"},{"location":"simple-projects/encode-decode/clacks/#representing-a-clack","title":"Representing a Clack","text":"For each clack, the light pattern is read from the top of the first column to the bottom, then from the top of the second column to the bottom. A light in a position represents a 1 value and no light represents a 0 value. This gives us our 6 number pattern for each clack in the alphabet.
The initial data structure chosen was essentially just modelling each individual clack. Since a clack is a 2x3 structure, the simplest way to represent a clacks is to have a vector that contains 2 vectors, each with three elements.
So a simple expression of the letter a in the clacks alphabet would be:
[[0 1 0][0 0 1]]\n
Therefore we could define a single letter of our alphabet as follows:
(def a [[0 1 0][0 0 1]])\n
Before defining the complete alphabet using this data structure, test this is the right data structure for the conversion process.
"},{"location":"simple-projects/encode-decode/clacks/#test-simple-conversion","title":"Test simple conversion","text":"Define a function to convert a single character into a clack:
(defn character->clack [character]\n (if (= character \"a\")\n a\n (str \"Sorry, character is not yet in the alphabet, please create a pull request\")))\n
Calling the function converts a string into the corresponding clack
(character->clack \"a\")\n
Clojure function naming
->
is a naming convention to indicate a function is specifically transforming to a data format. For example, json->clj-map
would be a generic function for transforming json to Clojure hash-map
The code is simple for a single character, however, would require a lot of redundant code to convert the whole alphabet. We would need either a deeply nested set of if statements or a very long cond
function, neither of which seems to be a particularly functional approach or idiomatic Clojure.
If a cond
statement was used, how would a clacks be converted back into a character?
So perhaps we need to change the data structure, one that provides an easy way to map to values together.
Also, there seems no value in mapping values to a 2x3 grid as long as we consistently express a clack.
"},{"location":"simple-projects/encode-decode/clacks/#define-the-alphabet","title":"Define the alphabet","text":"A hash map associates each key with a value and are used to create self-describing data. For example a person could be described as a hash-map
{:name \"Jenny Jetpack\" :age \"21\" :twitter \"jenjetpack\"}\n
Clojure keywords are often used for the keys because keywords can be used as a function with a map as an argument. This will return the value associated with the keyword in the map.
The new design for the clacks data structure associates a keyword of each letter of the alphabet with its corresponding clacks light pattern code
{:a [0 1 0 0 0 1]}\n
Test the design by defining enough letters for the clacks alphabet to convert some simple words, i.e bat
(def alphabet {:a [0 1 0 0 0 1]\n :b [0 0 1 0 1 0]\n :t [1 0 0 1 1 1]})\n
"},{"location":"simple-projects/encode-decode/clacks/#testing-the-map-design","title":"Testing the map design","text":"Use a keyword to lookup the value of its clack code
(:a alphabet)\n\n;; => [0 1 0 0 0 1]\n
Create a simple function to convert a single character to its Clacks representation, referred to a clack.
(defn character->clack [character]\n ((keyword character) alphabet))\n
The ->
character is part of the function name. This is a Clojure naming convention used when the function you are defining converts from one type to another.
And call the function as follows
(character->clack \"a\")\n\n;; => [0 1 0 0 0 1]\n
"},{"location":"simple-projects/encode-decode/clacks/#converting-a-word","title":"Converting a word","text":"Now we want to convert a whole word to a clacks sequence. It seemed the easiest way to convert a whole word was to convert each letter at a time using the map to look up each clack code, returning all the clacks codes in a sequence.
So we redefined the string->clacks
function to take in a whole word.
We used the map
function to apply a conversion function over each element in the word (each element of the string). This conversion function called clacksify
.
(defn clacksify [letter]\n (let [character (str letter)]\n (alphabet (keyword character))))\n\n(defn string->clacks [word]\n (map clacksify word))\n
Now we could convert any word that used the letters of our limited alphabet. We chose bat as a simple word.
(string->clacks \"bat\")\n
As we are passing a string and not a keyword to the clacksify
function, then we first convert the string to a keyword using the keyword
function.
"},{"location":"simple-projects/encode-decode/clacks/#converting-the-clack-to-a-string","title":"Converting the clack to a string","text":"Is there a simple way to look up a key given a value that is unique in the map?
All Clack codes are unique in the map, but there did not seem to be a simple expression to find the key when given a value.
We could have created a second mapping, however having two maps seemed redundant and a potential cause for silly bugs.
The answer was simple once we found it. As the clack codes are unique, they could be used as keys for the letter values, we just needed to swap the map around. Swapping a map's keys and values was done by writing a reverse-map
function.
(defn reverse-map\n \"Reverse the keys and value pairs in a map.\n Allows the map to be used to convert from a clack to a letter without defining a second map\"\n [m]\n (into {} (map (fn [[a b]] [b a]) m)))\n
So we defined the function declacksify
which takes a clack code and returns its corresponding character. The clack code returns the corresponding keyword rather than a character, so we use the name
function to convert the keyword into a character name.
(defn declacksify [clack]\n (name ((reverse-map alphabet) clack)))\n\n(defn clacks->string [clacks]\n (map declacksify clacks))\n
So calling these functions with a clacks
(declacksify [1 0 0 1 1 1])\n;; => \"t\"\n\n(clacks->string [[0 0 1 0 1 0] [0 1 0 0 0 1] [1 0 0 1 1 1]])\n;; => (\"b\" \"a\" \"t\")\n
At this point you may be thinking that using keywords to represent the characters of the alphabet may not be the most effective. Using keywords has required more code to be written, adding to the complexity of the solution.
"},{"location":"simple-projects/encode-decode/clacks/#tidy-output","title":"Tidy output","text":"clacks->string
function returns the right result, but not quite in the format required. Rather than a single string a sequence of characters is returned.
Using the map
function we can apply the str
function over the resulting characters to give a single string.
(defn clacks->string [clacks]\n(map str (map declacksify clacks)))\n
Using clojure.string/join
is a more idiomatic approach to converting a sequence of characters to a string
(require '[clojure.string :as string])\n\n(defn clacks->string [clacks]\n (string/join (map declacksify clacks)))\n
"},{"location":"simple-projects/encode-decode/clacks/#refactor-dictionary-design","title":"Refactor dictionary design","text":"Converting characters to keywords and back again seem redundant when characters themselves can be used as keys in a hash-map.
Using keywords is problematic when it comes to the space character as a keyword cannot be a space. Using :-
to represent a space required the clacksification and declacksification functions to convert between :-
and the space character. This also prevents hyphenated words working in the Clacks system.
Refactor the dictionary to use a string for each character as the keys in the map, instead of Clojure keywords. This solves the issue with space and other special characters.
(def dictionary\n {\"a\" [0 1 1 0 0 0 0 1]\n \"b\" [0 1 1 0 0 0 1 0]\n \"c\" [0 1 1 0 0 0 1 1]\n \"d\" [0 1 1 0 0 1 0 0]\n \"e\" [0 1 1 0 0 1 0 1]\n ,,,})\n
"},{"location":"simple-projects/encode-decode/clacks/#refactor-namespace","title":"Refactor namespace","text":"As the dictionary can be quite large to represent in code, move the dictionary definition to its own namespace.
Use a more specific name for the dictionary, describing what languages the dictionary is used for
(def english->clacks\n {\"a\" [0 1 1 0 0 0 0 1]\n \"b\" [0 1 1 0 0 0 1 0]\n \"c\" [0 1 1 0 0 0 1 1]\n \"d\" [0 1 1 0 0 1 0 0]\n \"e\" [0 1 1 0 0 1 0 1]\n ,,,})\n
A dictionary is required to translate from Clacks to English to decode the messages. Rather than write the reverse mappings for each character in the dictionary, in effect creating a second directory for the same two languages, use a function to invert the map by swapping keys and values.
clojure.set/map-invert
will swap each key/value pair in the map so the key becomes the value and the value becomes the key.
Define a clacks->english
dictionary that holds the result of the map-invert
function call
(ns practicalli.clacks-dictionary\n (:require [clojure.set]))\n\n(def clacks->english { ,,, })\n\n(def clacks->english (clojure.set/map-invert english->clacks))\n
Require the dictionary namespace using the alias dictionary
to give the dictionary names context when used in the main namespace.
Also require clojure.string
using the alias string
to use the join function.
(ns practicalli.clacks-messenger\n (:require [practicalli.clacks-dictionary :as dictionary]\n [clojure.string :as string]))\n
"},{"location":"simple-projects/encode-decode/clacks/#removing-side-effects","title":"Removing side effects","text":"Designing pure functions, those that receive all their data via arguments, is a common way to remove side effects.
Include the dictionary as an argument to each of the functions. This ensures that each function is pure and prevents side effects (side causes).
(defn character->clack [char dictionary]\n (let [character (str char)]\n (get dictionary character)))\n\n(defn message->clacks [message dictionary]\n (map #(character->clack % dictionary) message))\n\n(defn clack->character [clack dictionary]\n (get (clojure.set/map-invert dictionary) clack))\n\n(defn clack->character [clack dictionary]\n (get dictionary-inverted clack))\n\n;; Create a clacks code back to a message\n\n(defn clacks->message [clacks dictionary]\n (string/join (map #(clack->character % dictionary) clacks)))\n
Test the updated functions by calling them via the REPL
(message->clacks \"cab\" dictionary/english->clacks)\n;; => ([0 1 1 0 0 0 1 1] [0 1 1 0 0 0 0 1] [0 1 1 0 0 0 1 0])\n\n(message->clacks \"cab cab\" dictionary/english->clacks)\n;; => ([0 1 1 0 0 0 1 1] [0 1 1 0 0 0 0 1] [0 1 1 0 0 0 1 0] [0 0 0 0 0 0 0 0] [0 1 1 0 0 0 1 1] [0 1 1 0 0 0 0 1] [0 1 1 0 0 0 1 0])\n\n;; Create a character from a clack code\n\n;; test data\n(clacks->message '([0 1 1 0 0 0 1 1] [0 1 1 0 0 0 0 1] [0 1 1 0 0 0 1 0]) dictionary/english->clacks)\n\n(clacks->message\n '([0 1 1 0 0 0 1 1] [0 1 1 0 0 0 0 1] [0 1 1 0 0 0 1 0] [0 0 0 0 0 0 0 0] [0 1 1 0 0 0 1 1] [0 1 1 0 0 0 0 1] [0 1 1 0 0 0 1 0]) dictionary)\n
"},{"location":"simple-projects/encode-decode/clacks/#use-alternative-dictionaries","title":"Use alternative dictionaries","text":"Thanks to a flexible design with no side effects or side causes then its really easy to replace the English language alphabet with another language that can be encoded into Clack codes.
All that is required is to define a dictionary for another language. So languages based on the greek, latin or cyrillic alphabet could be send if a suitable alphabet with clack codes is supplied.
"},{"location":"simple-projects/encode-decode/convert-boolean-values/","title":"Convert boolean values","text":""},{"location":"simple-projects/encode-decode/convert-boolean-values/#convert-boolean-true-false-to-1-and-0","title":"Convert boolean true false to 1 and 0","text":"A very simple example of encoding and decoding is converting the Clojure values of true
and false
to 1
and 0
respectively.
Using 1
for true and 0
for false has been a common idiom in programming languages, especially where a language did not include true
and false
syntax.
"},{"location":"simple-projects/encode-decode/convert-boolean-values/#define-an-association-between-values","title":"Define an association between values","text":"Define a Clojure hash-map
to associate the Clojure boolean true
an false
values to 1
and 0
respectively
{false 0\n true 1}\n
"},{"location":"simple-projects/encode-decode/convert-boolean-values/#find-an-associated-value-for-the-conversion","title":"Find an associated value for the conversion","text":"Using the get
function the boolean-value
is used to find a matching key in the map and if found the value that key is associated is returned.
(get {false 0 true 1} boolean-value)\n
Example:
(get {false 0 true 1} true)\n
A map can be called, just like a function. the boolean-value
is passed to the map as a function argument. As with the get
expression, if the map contains the key the associated value is returned.
({false 0 true 1} boolean-value)\n
Example:
({false 0 true 1} true)\n
"},{"location":"simple-projects/encode-decode/convert-boolean-values/#convert-multiple-boolean-values","title":"Convert multiple boolean values","text":"If there are a collection of boolean values to convert, the map
function can be used to convert them all to 1 or 0.
Map this over a collection of values
(map {false 0 true 1} [collection-of-boolean-values])\n
Example:
(map {false 0 true 1} [true false false true true true false false true false true false false true])\n
"},{"location":"simple-projects/encode-decode/convert-boolean-values/#how-does-this-work","title":"How does this work?","text":"The map
function takes two arguments, a function and a collection. The map
function calls the function given as an argument and calls it with each element of the collection in turn. The result of each call is remembered by the map
function and when the last element of the collection has been used, a new collection of all the results is returned.
In the above example, the hash-map {false 0 true 1} acts as a function.
({false 0 true 1} true)\n
A hash-map acts as a function in that it can return an associated value when given a key as an argument.
Calling {false 0 true 1}
with true
as an argument returns the value 1
.
"},{"location":"simple-projects/encode-decode/rna-dna/","title":"RNA to DNA transcription","text":"Given a DNA strand, return its RNA complement (RNA transcription).
Both DNA and RNA strands are a sequence of nucleotides.
The four nucleotides found in DNA are adenine (A), cytosine (C), guanine (G) and thymine (T).
The four nucleotides found in RNA are adenine (A), cytosine (C), guanine (G) and uracil (U).
Given a DNA strand, its transcribed RNA strand is formed by replacing each nucleotide with its complement:
G -> C\nC -> G\nT -> A\nA -> U\n
Inspired by Exercism.io challenge This project was inspired by the RNA Transcription exercise on Exercism.io. Related exercises include Nucleotide Count and Hamming.
"},{"location":"simple-projects/encode-decode/rna-dna/#create-a-project","title":"Create a project","text":" Pracitcalli Clojure CLI Config provides the :project/create
alias to create projects using deps-new project.
clojure -T:project/create :template app :name practicalli/rna-transcription\n
"},{"location":"simple-projects/encode-decode/rna-dna/#define-unit-tests","title":"Define unit tests","text":"Open the test/practicalli/rna-transcription.clj
and add the following tests
(ns practicalli.rna-transcription-test\n (:require [clojure.test :refer [deftest is testing]]\n [rna-transcription :as SUT]))\n\n(deftest rna-transcription-test\n (testing \"transcribe cytosine to guanine\"\n (is (= \"G\" (SUT/to-rna \"C\"))))\n\n (testing \"transcribe guanine to cytosine\"\n (is (= \"C\" (SUT/to-rna \"G\"))))\n\n (testing \"transcribe adenine to uracil\"\n (is (= \"U\" (SUT/to-rna \"A\"))))\n\n (testing \"transcribe thymine to adenine\"\n (is (= \"A\" (SUT/to-rna \"T\"))))\n\n (testing \"transcribe all nucleotides\"\n (is (= \"UGCACCAGAAUU\" (rna-transcription/to-rna \"ACGTGGTCTTAA\"))))\n\n (testing \"validate dna strands\"\n (is (thrown? AssertionError (rna-transcription/to-rna \"XCGFGGTDTTAA\")))))\n
"},{"location":"simple-projects/encode-decode/rna-dna/#code-the-rna-transcription","title":"Code the RNA transcription","text":"Edit the src/practicalli/rna-transcription.clj
file and require the clojure.string
library. The library is part of the Clojure standard library, so does not need to be added as a project dependency.
(ns practicalli.rna-transcription\n (:require [clojure.string :as string]))\n
Define a dictionary to convert from DNA nucleotide to its RNA complement
(def dictionary-dna->rna\n \"Convert DNA to RNA\"\n {\"G\" \"C\"\n \"C\" \"G\"\n \"T\" \"A\"\n \"A\" \"U\"}\n )\n
Define a function to convert a single DNA nucleotide (one of G
, C, T, A) into its RNA complement, using the dictionary.
The algorithm is a simple hash-map lookup using the DNA nucleotide as the Key and returning the RNA complement as the value.
(defn convert-nucleotide\n \"Convert a specific nucleotide from a DNA strand,\n into a nucleotide for an RNA strand\"\n [dictionary nucleotide]\n (get dictionary (str nucleotide)))\n
Now a single nucleotide can be converted, another function can be defined to convert all DNA nucleotides in a given sequence.
(defn to-rna [dna-sequence]\n (if (clojure.string/includes? dna-sequence \"X\")\n (throw (AssertionError.))\n (apply str\n (map #(convert-nucleotide dictionary-dna-rna %) dna))))\n
Although apply str
provides the correct answer, it is more idiomatic to use the clojure.string/join
function.
(defn to-rna [dna-sequence]\n (if (clojure.string/includes? dna-sequence \"X\")\n (throw (AssertionError.))\n (string/join\n (map #(convert-nucleotide dictionary-dna-rna %) dna))))\n
The functions provide the correct answer, however, to-rna
is not a pure function as the dictionary is pulled in as a side cause.
Update all the tests in test/practicalli/rna-transcription.clj
to call SUT/to-rna
with a dictionary included in the argument.
(ns practicalli.rna-transcription-test\n (:require [clojure.test :refer [deftest is testing]]\n [rna-transcription :as SUT]))\n\n(deftest rna-transcription-test\n (testing \"transcribe cytosine to guanine\"\n (is (= \"G\" (SUT/to-rna SUT/dictionary-dna->rna \"C\"))))\n\n (testing \"transcribe guanine to cytosine\"\n (is (= \"C\" (SUT/to-rna SUT/dictionary-dna->rna \"G\"))))\n\n (testing \"transcribe adenine to uracil\"\n (is (= \"U\" (SUT/to-rna SUT/dictionary-dna->rna \"A\"))))\n\n (testing \"transcribe thymine to adenine\"\n (is (= \"A\" (SUT/to-rna SUT/dictionary-dna->rna \"T\"))))\n\n (testing \"transcribe all nucleotides\"\n (is (= \"UGCACCAGAAUU\" (SUT/to-rna SUT/dictionary-dna->rna \"ACGTGGTCTTAA\"))))\n\n (testing \"validate dna strands\"\n (is (thrown?\n AssertionError\n (SUT/to-rna SUT/dictionary-dna->rna \"XCGFGGTDTTAA\")))))\n
Update to-rna
to be a pure function by including the dictionary as an argument and also pass the updated tests.
(defn to-rna [dictionary dna-sequence]\n (if (clojure.string/includes? dna-sequence \"X\")\n (throw (AssertionError.))\n (string/join\n (map #(convert-nucleotide dictionary %) dna))))\n
"},{"location":"simple-projects/encode-decode/rna-dna/#idiomatic-improvements","title":"Idiomatic improvements","text":"The to-rna
function is not pure, as it relies on a shared value in the namespace, the dictionary-dna-rna
transcription map.
Passing dictionary-dna-rna
as an argument to the to-rna
function as well as the dna sequence would make to-rna
a pure function. It would also allow use of a range of transcription maps.
(defn to-rna\n \"Transcribe each nucleotide from a DNA strand into its RNA complement\n Arguments: string representing DNA strand\n Return: string representing RNA strand\"\n [transcription dna]\n (string/join\n (map #(or (transcription %)\n (throw (AssertionError. \"Unknown nucleotide\")))\n dna )))\n
The change to the to-rna
function will break all the tests.
Updated unit tests that call to-rna
with both arguments
(ns rna-transcription-pure-test\n (:require [clojure.test :refer [deftest is]]\n [rna-transcription-pure :as SUT]\n [rna-transcription :as data]))\n\n(deftest transcribes-cytosine-to-guanine\n (is (= \"G\" (SUT/dna->rna data/dna-nucleotide->rna-nucleotide \"C\"))))\n\n(deftest transcribes-guanine-to-cytosine\n (is (= \"C\" (SUT/dna->rna data/dna-nucleotide->rna-nucleotide \"G\"))))\n\n(deftest transcribes-adenine-to-uracil\n (is (= \"U\" (SUT/dna->rna data/dna-nucleotide->rna-nucleotide \"A\"))))\n\n(deftest it-transcribes-thymine-to-adenine\n (is (= \"A\" (SUT/dna->rna data/dna-nucleotide->rna-nucleotide \"T\"))))\n\n(deftest it-transcribes-all-nucleotides\n (is (= \"UGCACCAGAAUU\" (SUT/dna->rna data/dna-nucleotide->rna-nucleotide \"ACGTGGTCTTAA\"))))\n\n(deftest it-validates-dna-strands\n (is (thrown? AssertionError (SUT/dna->rna data/dna-nucleotide->rna-nucleotide \"XCGFGGTDTTAA\"))))\n
"},{"location":"simple-projects/encode-decode/rna-dna/#hintexercisim-project-and-the-pure-function","title":"Hint::Exercisim project and the pure function","text":"If you wish to keep the Exercisim project passing, then add a new namespace to the project by create a new file called rna-transcript-pure.clj
. Add the new design of the to-rna
function to that namespace. Copy the tests into a new namespace by creating a file called rna-transcription-pure.clj
and update the tests to use two arguments when calling to-rna
"},{"location":"simple-projects/encode-decode/rna-dna/#summary","title":"Summary","text":"This exercise has covered the concept of using a Clojure hash-map structure as a dictionary lookup.
"},{"location":"simple-projects/mutating-state/","title":"Mutating State in a Controlled way","text":"Mutating state should be used carefully and sparingly in Clojure (and all other programming languages).
atom
is a mutable container that can manage any value. The atom ensures that only one call at a time can affect the value it manages. This is part of the software transactions memory system in Clojure.
As the atom is mutable in that the value it manages can be changed, however, this must be done with special commands (swap!, reset!, compare-and-set!, swap-vals!).
Even though the atom is mutable, the values it manages are not. They are normal immutable (unchangeable) Clojure values.
ref
is similar to atom
and can manage transactions, ensuring that all changes happen or no changes happen.
Project Topics Overview Mutants assemble atom swap! reset! Using an atom to manage state changes Undo/Redo atom add-watch Traversing the history of an atom Poker game atom swap! reset! ref Simple transaction management using atom and ref in a card game, using constraints on an atom"},{"location":"simple-projects/mutating-state/#references","title":"References","text":" - Atoms - clojure.org
- Refs and Transactions - clojure.org
- Agents - clojure.org
"},{"location":"simple-projects/mutating-state/mutants-assemble/","title":"Mutants Assemble","text":"In this section you will apply changes to values, how to define your own simple functions.
We will also introduce the following functions for the first time:
function Description atom
create an anonymous function, one without a name deref
, @
assign a name to a function"},{"location":"simple-projects/mutating-state/mutants-assemble/#create-a-new-clojure-project","title":"Create a new Clojure project","text":" Pracitcalli Clojure CLI Config provides the :project/create
alias to create projects using deps-new project.
clojure -T:project/create :template app :name practicalli/mutants-assemble\n
Open the src/practicalli/mutants_assemble.clj
file in a Clojure aware editor and start the REPL.
"},{"location":"simple-projects/mutating-state/mutants-assemble/#define-an-atom","title":"Define an atom","text":"Use the def
function to bind a name to an atom.
The atom wraps data, initially an empty vector.
(def mutants (atom []))\n
The vector remains an immutable value, even though it is contained within a mutable atom container
Define a function using defn
which takes a mutant as an argument and updates the value managed by the atom. The reference to the atom is also an argument, making this a pure function and more generic as any given atom can be updated with this function.
(defn add-mutant [mutants mutant]\n (swap! mutants conj mutant))\n
swap!
uses a function to create a new value for the atom to manage. In this case the conj
function is used to join the value of mutant with the existing mutants atom value, creating a new vector.
swap!
is a macro so the syntax is a little different. Essentially this is the same as an expression (conj mutants mutant)
, with the value this returns swapped into the atom.
Call the function with the mutants
atom and a mutant to add, which is a string containing the name of a mutant character.
(add-mutant mutants \"Black Widow\")\n
The value the atom is managing has been swapped for a new value. The original value was not modified (vectors are immutable) so the atom now points to a new value, a vector containing a string.
"},{"location":"simple-projects/mutating-state/mutants-assemble/#viewing-the-value-managed-by-the-atom","title":"Viewing the value managed by the atom","text":"Use the deref
function to see the value the atom is managing.
(deref mutants)\n
It is idiomatic to use @
which is a syntax alias for the deref
function, rather than explicitly using deref
.
@mutants\n
"},{"location":"simple-projects/mutating-state/mutants-assemble/#reset-the-atom-value","title":"Reset the atom value","text":"reset!
will change the value managed by the atom by providing the new value. This is simpler than using swap!
as it does not use the existing value in the atom.
(reset! mutants [])\n
Now all the mutants are gone (and we can start looking for new ones to add).
"},{"location":"simple-projects/tdd-kata/","title":"Kata challenges","text":"A kata is a small challenge that you attempt to solve in different ways, so experiment with your solutions to these challenges.
Kata are often coupled with Test Driven Development approach.
Project Topics Overview Recent song-list TDD Keep a list of recent songs played, without duplicates Salary Slip Generator TDD Generate play slips for an employee Code Kata Website
"},{"location":"simple-projects/tdd-kata/recent-song-list/","title":"TDD Kata Recent Song-list","text":"Create a recent song list to hold a unique set of songs that have been played.
The most recently played song is at the start of the list, the least recently played song is the last in the list.
- A recently-used-list is initially empty.
- Songs in the list are unique, so repeatedly played songs should only appear once in the list
- Songs can be looked up by index, which counts from zero.
- The song list can be transitory (starting from empty each time) or persistent within a REPL session (examples use a transitory approach)
Optional extras:
- Empty song names are not allowed.
- Add a limit to the number of songs the list contains, with the least recently added items dropped when that limit is reached.
"},{"location":"simple-projects/tdd-kata/recent-song-list/#create-project","title":"Create Project","text":"Create a new project using clj-new
clojure -T:project/create practicalli/song-list\n
"},{"location":"simple-projects/tdd-kata/recent-song-list/#run-repl","title":"Run REPL","text":"Start a Clojure REPL via a Clojure editor or via the command line from the root of the project directory
Start rich terminal UI Clojure REPL
clojure -M:repl/rebel\n
"},{"location":"simple-projects/tdd-kata/recent-song-list/#unit-tests","title":"Unit Tests","text":"clojure.test
library is part of Clojure standard library and is the most common way to write unit tests in Clojure
Open test/playground/song_list_test.clj
file in your editor and update the namespace definition to include clojure.test
Require clojure.test namespace
test/playground/song_list_test.clj(ns practicalli.song-list-test\n (:require [clojure.test :refer [deftest is testing]]\n [playground.song-list :as song-list]))\n
"},{"location":"simple-projects/tdd-kata/recent-song-list/#run-tests","title":"Run Tests","text":"Evaluate the practicalli.song-list
and practicalli.song-list-test
namespaces to load their code into the REPL
Call the run-tests
function in the REPL to get a report back on all of the tests in our current namespace (song-list
)
Kaocha test runnerclojure.test runner Practicall Clojure CLI Config provides the :test/run
alias to run the Kaocha test runner.
Kaocha test runner
Open a command line in the root directory of the project and run the following command.
clojure -X:test/run\n
Kaocha runs all the tests, stopping should a test fail.
Kaocha test runner with file watch
Use the :test/watch
alias to automatically run tests when ever a file is saved
clojure -X:test/run\n
Evaluate the project code and evaluate the run-tests
function from clojure.test
from within the REPL
clojure.test runner
(run-tests)\n
"},{"location":"simple-projects/tdd-kata/recent-song-list/#test-song-list-exists","title":"Test song-list exists","text":"Write a test to see if a recent song list exists.
This is an opportunity to think about what kind of data structure you want to use to hold your recent song list.
Try write the code first and then check that code with the examples provided (click to expand each code example box)
Test song-list exists A simple test that checks for a recent-songs
list src/playground/song_list.clj
(deftest song-list-exists-test\n (testing \"Does a recent song list exist\"\n (is (vector? song-list/recent-songs))))\n
recent-songs
should be defined in src/playground/recent-song-list.clj
before running the test, otherwise a compilation error will be returned."},{"location":"simple-projects/tdd-kata/recent-song-list/#define-a-recent-song-list","title":"Define a recent song list","text":"Edit src/playground/song_list.clj
and define a name for the collection of recent songs
Use an empty collection to start with. Which collection type will you use though (hash-map {}
, list ()
, set #{}
, vector []
)?
recent-songs collection Define a recent-song name for an empty vector src/playground/song_list.clj
(def recent-songs [])\n
Test First Approach For a strict test first approach, a recent-songs
name (symbol) would be defined that returns false
or a falsy value, e.g. nil
A name (symbol) must be defined for use in the test so that the Clojure code can compile
"},{"location":"simple-projects/tdd-kata/recent-song-list/#test-song-list-is-empty","title":"Test song-list is empty","text":"The recent song list should be empty to start with.
Check song list is empty A simple test that compares an empty vector with the value of recent-songs
src/playground/song_list.clj
(deftest song-list-empty-test\n (testing \"Is song list empty if we haven't added any songs\"\n (is\n (= [] song-list/recent-songs))))\n
Here is the same test using the empty?
function instead of the =
function.
src/playground/song_list.clj
(deftest song-list-empty-test\n (testing \"Is song list empty if we haven't added any songs\"\n (is\n (empty? song-list/recent-songs))))\n
Either of these tests could replace the test that the song list exists, as these tests would fail if the song list did not exist."},{"location":"simple-projects/tdd-kata/recent-song-list/#test-adding-a-song-to-the-list","title":"Test adding a song to the list","text":"Add a song to the collection, for example Tubular Bells - Mike Oldfield
Test adding a song to the list test/playground/song_list_test.clj(deftest add-songs-test\n\n (testing \"add song returns a song list with entries\"\n (is\n (not (empty?\n (add-song \"Barry Manilow - Love on the rocks\" song-list/recent-songs)))))\n\n (testing \"add multiple song returns a song list with entries\"\n (is\n (not (empty?\n (->> song-list/recent-songs\n (add-song \"Mike Oldfield - Tubular Bells Part 1\")\n (add-song \"Barry Manilow - Love on the rocks\")\n (add-song \"Phil Colins - Sususudio\" )))))))\n
Other songs are avialbe and Practicalli makes no recommendation as to what songs should be used or listened too.
"},{"location":"simple-projects/tdd-kata/recent-song-list/#function-to-add-song","title":"Function to add song","text":"Create a function to add a song to the start of the song list.
Function to add song to list The add-song
function takes the name of a song and the song list to which it will be added.
A Thread-last macro ->>
is used to pass the song list over two functions.
The song-list
is first passed to the remove
expression as its last argument. This expression will remove any occurrence of the new song we want to add from the song-list
.
The results of the remove
expression are then passed to the cons
expression as its last argument. The cons
expression simply adds the new song to the start of the list, making it the most recent song.
src/playground/song_list.clj(def recent-songs [])\n\n(defn add-song [song song-list]\n (cons song song-list))\n
recent-songs
is passed into the add-song
function as an argument, song-list
to keep the design of add-song
function pure (no side-effects). This design also provides greater scope to using the add-song
function, as any song list can be added to, rather than hard-coding recent-songs
list.
"},{"location":"simple-projects/tdd-kata/recent-song-list/#test-song-added-to-top-of-list","title":"Test song added to top of list","text":"As the song list shows recently played songs, new songs added should be at the top of the list.
The list should not contain duplicate entries for a song.
Test songs added to top of list test/playground/song_list_test.clj(deftest recently-added-song-first-test\n\n (testing \"most recent song should be first in the list when empty list\"\n (is (=\n (first (add-song \"Daft Punk - Get Lucky\" recent-songs))\n \"Daft Punk - Get Lucky\")))\n\n (testing \"most recent song should be first in list when adding multiple songs\"\n (is (=\n (first\n (->> recent-songs\n (add-song \"Daft Punk - Get Lucky\")\n (add-song \"Pharrell Williams - Happy\")))\n \"Pharrell Williams - Happy\")))\n\n (testing \"most recent song should be first in list when adding a repeated song\"\n (is (=\n (first\n (->> recent-songs\n (add-song \"Pharrell Williams - Happy\")\n (add-song \"Daft Punk - Get Lucky\")\n (add-song \"Pharrell Williams - Happy\")))\n \"Pharrell Williams - Happy\")))\n\n (testing \"most recent song should be first in list when adding a repeated song\"\n (is (not=\n (last\n (->> recent-songs\n (add-song \"Pharrell Williams - Happy\")\n (add-song \"Daft Punk - Get Lucky\")\n (add-song \"Pharrell Williams - Happy\")))\n \"Pharrell Williams - Happy\"))))\n
"},{"location":"simple-projects/tdd-kata/recent-song-list/#add-song-to-start-of-list","title":"Add song to start of list","text":"Create a function to add a song to the start of the song list.
Function to add song to list The add-song
function takes the name of a song and the song list to which it will be added.
A Thread-last macro ->>
is used to pass the song list over two functions.
The song-list
is first passed to the remove
expression as its last argument. This expression will remove any occurrence of the new song we want to add from the song-list
.
The results of the remove
expression are then passed to the cons
expression as its last argument. The cons
expression simply adds the new song to the start of the list, making it the most recent song.
src/playground/song_list.clj(def recent-songs [])\n\n(defn add-song [song song-list]\n (->> song-list\n (remove #(= song %))\n (cons song)))\n
recent-songs
is passed into the add-song
function as an argument, song-list
to keep the design of add-song
function pure (no side-effects). This design also provides greater scope to using the add-song
function, as any song list can be added to, rather than hard-coding recent-songs
list.
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/","title":"Salary Slip Kata","text":" Pracitcalli Clojure CLI Config provides the :project/create
alias to create projects using deps-new project.
clojure -T:project/create :template app :name practicalli/salary-calculator\n
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/#problem-description","title":"Problem description","text":"A typical salary slip contains employee details like employee id, employee name and their monthly salary details like their gross salary, national insurance contributions, tax-free allowance, taxable income and tax payable.
Salary slips are generated each month for every employee.
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/#acceptance-criteria","title":"Acceptance criteria","text":" - Salary slip generator should receive an employee with its Employee Id, Employee Name and Annual Gross Salary
- Salary slip should contain the Employee ID, Employee Name, Gross Salary, National Insurance contributions, Tax-free allowance, Taxable income and Tax payable for the month
- The entry point should be the following public function API
(defn salary-slip-generator\n \"\"\n [employee]\n ,,,)\n
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/#iterations","title":"Iterations","text":"Each iteration adds more rules to the calculation. Some iterations also introduce new fields to the salary slip.
In a given iteration, all the salary slips contain the same number fields for each employee (if a tax or contribution does not apply for a given employee, just put \u00a30.00).
This means that for each iteration you will need to add fields to the SalarySlip
class. In the first iteration, SalarySlip
only contains the Employee ID, Employee Name and Monthly Gross Salary.
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/#iteration-1-for-an-annual-salary-of-500000","title":"Iteration 1: for an annual salary of \u00a35,000.00","text":"This is the most basic case.
Calculation rules:
- Monthly Gross Salary: The monthly gross salary is the employee's annual gross salary divided by 12
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/#iteration-2-for-an-annual-gross-salary-of-906000","title":"Iteration 2: for an annual gross salary of \u00a39,060.00","text":"Here we introduce the National Insurance contribution
The monthly salary slip should contain the below:
Employee ID: 12345\n Employee Name: John J Doe\n Gross Salary: \u00a3755.00\n National Insurance contributions: \u00a310.00\n
Calculation rules:
- National Insurance contributions: Any amount of money earned above a gross annual salary of \u00a38,060.00 is subject to a National Insurance contribution of 12%
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/#iteration-3-for-an-annual-gross-salary-of-1200000","title":"Iteration 3: for an annual gross salary of \u00a312,000.00","text":"This employee also needs to pay taxes
The monthly salary slip should contain the below:
Employee ID: 12345\n Employee Name: John J Doe\n Gross Salary: \u00a31,000.00\n National Insurance contributions: \u00a339.40\n Tax-free allowance: \u00a3916.67\n Taxable income: \u00a383.33\n Tax Payable: \u00a316.67\n
Calculation rules:
- Taxable income: Any amount of money earned above a gross annual salary of \u00a311,000.00 is taxed at 20%
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/#iteration-4-for-an-annual-gross-salary-of-4500000","title":"Iteration 4: for an annual gross salary of \u00a345,000.00","text":"This employee pays a higher band of National Insurance and Income Tax.
The monthly salary slip should contain the below:
Employee ID: 12345\n Employee Name: John J Doe\n Gross Salary: \u00a33,750.00\n National Insurance contributions: \u00a3352.73\n Tax-free allowance: \u00a3916.67\n Taxable income: \u00a32,833.33\n Tax Payable: \u00a3600.00\n
Calculation rules:
- Taxable income (higher rate): Any amount of money earned above a gross annual salary of \u00a343,000.00 is taxed at 40%
- National Insurance (higher contributions): Any amount of money earned above a gross annual salary of \u00a343,000.00 is only subject to a 2% NI contribution
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/#iteration-5-for-annual-gross-salaries-of-10100000-11100000-12200000-and-15000000","title":"Iteration 5: for annual gross salaries of \u00a3101,000.00; \u00a3111,000.00; \u00a3122,000.00 and \u00a3150,000.00","text":"For high earners, the tax-free allowance decreases.
The monthly salary slips should contain the below (respectively):
Employee ID: 12345\n Employee Name: John J Doe\n Gross Salary: \u00a38,416.67\n National Insurance contributions: \u00a3446.07\n Tax-free allowance: \u00a3875.00\n Taxable income: \u00a37,541.67\n Tax Payable: \u00a32,483.33\n\n\n Employee ID: 12345\n Employee Name: John J Doe\n Gross Salary: \u00a39,250.00\n National Insurance contributions: \u00a3462.73\n Tax-free allowance: \u00a3458.33\n Taxable income: \u00a38,791.67\n Tax Payable: \u00a32,983.33\n\n\n Employee ID: 12345\n Employee Name: John J Doe\n Gross Salary: \u00a310,166.67\n National Insurance contributions: \u00a3481.07\n Tax-free allowance: \u00a30.00\n Taxable income: \u00a310,166.67\n Tax Payable: \u00a33,533.33\n\n\n Employee ID: 12345\n Employee Name: John J Doe\n Gross Salary: \u00a312,500.00\n National Insurance contributions: \u00a3527.73\n Tax-free allowance: \u00a30.00\n Taxable income: \u00a312,500.00\n Tax Payable: \u00a34,466.67\n
Calculation rules:
- Tax-free allowance: When the Annual Gross Salary exceeds \u00a3100,000.00, the tax-free allowance starts decreasing. It decreases by \u00a31 for every \u00a32 earned over \u00a3100,000.00. And this excess is taxed at the Higher rate tax.
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/#iteration-6-for-an-annual-gross-salary-of-16000000","title":"Iteration 6: for an annual gross salary of \u00a3160,000.00","text":"The employee goes into the additional rate band.
The monthly salary slip should contain the below:
Employee ID: 12345\n Employee Name: John J Doe\n Gross Salary: \u00a313,333.33\n National Insurance contributions: \u00a3544.40\n Tax-free allowance: \u00a30.00\n Taxable income: \u00a313,333.33\n Tax Payable: \u00a34,841.67\n
Calculation rules:
- Income tax (additional rate band) : Any amount of money earned above a gross annual salary of \u00a3150,000.00 is taxed at 45%
Practicalli Salary Slip Kata Salary slip kata - Devoxx 2019
(ns salary-slip-kata.core\n \"Developer Anarchy by Fred George\n - made devs write the same solution in different languages\n -- helps devs master the business domain\n -- helps devs master technology domain\")\n\n(defn- national-insurance-contribution\n \"Calculate the national insurance contribution due for a given annual salary.\n\n ---------------------+-------------------------+--------\n Band | NI deductible income | NI Rate\n ---------------------+-------------------------+--------\n No contributions | Up to \u00a38,060.00 | 0%\n Basic contributions | \u00a38,060.00 to \u00a343,000.00 | 12%\n Higher contributions | over \u00a343,000.00 | 2%\n ---------------------+-------------------------+-------- \"\n\n [annual-gross-salary]\n ;; add a cond statement to return the calculate the value with respect to the band.\n (* annual-gross-salary 0.12))\n\n\n;; taxable income\n;; ---------------------+---------------------------+---------\n;; Band | Taxable income | Tax rate\n;; ---------------------+---------------------------+---------\n;; Personal Allowance* | Up to \u00a311,000.00 | 0%\n;; Basic rate | \u00a311,000.00 to \u00a343,000.00 | 20%\n;; Higher rate | \u00a343,000.00 to \u00a3150,000.00 | 40%\n;; Additional rate | over \u00a3150,000.00 | 45%\n;; ---------------------+---------------------------+---------\n\n\n(defn salary-slip\n \"Creates a salary slip for a person\n\n Specifically for employee of 24K annual salary\"\n\n [{:keys [employee-id\n employee-name\n annual-gross-salary]}]\n (let [tax-free-allowance 11000\n taxable-income (- annual-gross-salary\n tax-free-allowance)]\n {:employee-id employee-id\n :employee-name employee-name\n :gross-salary (/ annual-gross-salary 12)\n :national-insurance (national-insurance-contribution annual-gross-salary)\n :tax-free-allowance tax-free-allowance\n :taxable-income taxable-income\n :tax-payable (* taxable-income 0.20)}))\n
"},{"location":"testing/","title":"Testing in Clojure","text":"Testing is supported in Clojure with a range of testing libraries and test runners.
"},{"location":"testing/#unit-test","title":"Unit Test","text":"The unit of test in Clojure is the function, so functions are defined that test other functions.
clojure.test namespace is part of the Clojure standard library and provides assertion based testing and functions to run tests.
Practicalli Unit Testing Guide Using Test Runners
"},{"location":"testing/#generative-testing","title":"Generative testing","text":"Define specifications for values to confirm information coming into and leaving a Clojure service are of the correct form.
Generate test data from value specifications to verify the behaviour of functions, creating diverse sets of data for extensive testing.
Define specifications for functions to validate the correct form of values are passed and returned from a function.
Define value and function specifications with Cloure Spec Generative Testing with Clojure Spec
"},{"location":"testing/#performance-testing","title":"Performance testing","text":"Test individual expressions through to application and load testing one or more services.
- time - simple results of evaluating an expression
- criterion - a realistic measure of performance for clojure expressions
- Gatling - open source & commercial load test tool for web applications
- clj-gatling - wrapper around Gatling which enables tests to be expressed in Clojure.
"},{"location":"testing/#behaviour-driven-development-frameworks","title":"Behaviour Driven Development frameworks","text":"Although not widely used in the Clojure community, there are several approaches to develop and test software using Behaviour Driven Development.
BDD: Given, When, Then and scenario approach to outside in software testing
- Scenari - executable specification / BDD in Clojure
- kaocha-cucumber - support for Cucumber tests in the gerkin format
- speclj - TDD/BDD framework for Clojure and ClojureScript based on RSpec.
Alternative BDD libraries are discussed at https://github.com/gphilipp/bdd-guide-clojure
"},{"location":"testing/#articles-on-testing-in-clojure","title":"Articles on testing in Clojure","text":" - Clojure test runner of my dreams
- Example based unit testing in Clojure
- TDD in Clojure at Funding Circle
- Bolth - a more humane test runner
- Announcing kaocha a new and improved clojure test runner
- Scenarios as code - Clojure Remote presentation
- Load testing with Gatling and Clojure - JUXT.pro
"},{"location":"testing/clojure-test/","title":"Unit Testing with clojure.test
","text":"clojure.test
is a test library that is already part of Clojure and test package hierarchy is typically created (e.g. when generating Clojure projects with Leiningen).
As with other unit testing libraries you use clojure.test
to write test. These tests are defined as functions that contain one or more assertions.
As a general guideline, a Clojure test function should test a specific Clojure function.
"},{"location":"testing/clojure-test/#hintwhat-to-test","title":"Hint::What to test","text":"Define a deftest
for every public functions within a namespace, so the contract / api for each namespace is testable and will highlight obvious regressions. The testing
function can be used to group assertions for a particular deftest
, so different aspects of the tests can be grouped together.
Test reports contain only the names of the deftest
functions, as there are no names with testing
clojure.spec
provides another way to define a contract around your functions and data structures. It also includes generative testing approach to broaden the test data used to test your functions.
"},{"location":"testing/clojure-test/#test-namespaces","title":"Test namespaces","text":"clojure.test
needs to be included in the namespace in order to use the functions that namespace provides.
The recommended syntax is to :refer
the specific functions which makes those functions available as if they were defined in the current namespace.
The namespace that is under test also needs to be included and and its recommended that you use the alias SUT
for system under test. The test namespace matches the namespace you are testing, with the addition of -test
to the name.
(ns my-clojure-app.core-test\n (:require [clojure.test :refer [deftest deftest- testing is]]\n [my-clojure-app.core :as SUT ]))\n
"},{"location":"testing/clojure-test/#writing-an-assertion","title":"Writing an assertion","text":"An assertion is where you compare an expected result with the result of calling a function. If the assertion is true, then then it is a pass. If the assertion is false, then its a fail.
The form of an assertion takes a form (is (comparator expected-value function-call))
Some simple examples include
(is (= 42 (* 6 7)))\n\n(is (not= 24 (* 6 7)))\n
"},{"location":"testing/clojure-test/#defining-a-test","title":"Defining a test","text":"deftest
is used to define a function that will test a similarly named function from the src
tree. The test function name should match the function it is testing with -test
added to the end.
testing
function allows you to group one or more assertions
is
defines an assertion
(deftest adder-test\n (testing \"Using a range of numbers to test the adder\"\n #_(is (= 0 1))\n (is (= (+ 1 2) (adder 1 2)) \"Adding 1 and 2\")\n (is (= (+ 1 -2) (adder 1 -2)) \"Adding 1 and -2\")\n #_(is (not (= (+ 1 2)) (adder \"a\" \"b\")) \"Adding strings as negative test\")\n (is (false? (= 0 1)) \"A simple failing test\")\n (is (false? (= 0 (adder 3 4))) \"Purposefully using failing data\")))\n
"},{"location":"testing/integration-testing/","title":"Integration Testing","text":"See the continuous integration section
"},{"location":"testing/test-runners/","title":"Test Runners","text":"A test runner is used to run one or more unit tests in a project and report the results. When there are failing tests, a test runners show details of how the failing test, showing actual and expected values.
Test runners may support specification testing with clojure.spec, checking data and functions conform to their specifications.
Test runners are called from either a Clojure editor, as a command line tool or within a continuous integration workflow.
Regularly run tests in a project to ensure implementations and design decisions made so far have not regressed.
Test runner Type Summary cognitect-labs test runner clj Simple test runner cljs-test-runner cljs Run all ClojureScript tests with one simple command. Kaocha clj, cljs Full featured test runner CIDER test runner clj CIDER built in test runner CIDER test runner is ideal if using Emacs for Clojure development, as its build into CIDER.
Practicalli Recommends Kaocha test runner
Kaocha is a very feature rich test runner for Clojure and ClojureScript, BDD style cucumber tests, coverage and junit style reporting.
Practicalli Clojure CLI Config - test runner aliases
Practicalli Clojure CLI Config contains several aliases for test runners
:test/env
adds the test
directory to the class path and org.clojure/test.check
library clojure -X:test/run
run Kaocha test runner clojure -X:test/watch
run Kaocha test runner in watch mode, running on file changes clojure -M:test/cljs
run Kaocha ClojureScript test runner clojure -X:test/cognitect
simple to use Cognitect test runner :lib/kaocha
adds Kaocha as a library to the class path, enabling Kaocha to run from an editor, e.g. Emacs Cider with Kaocha test runner
Practicalli REPL Reloaded aliases :repl/reloaded
& :dev/reloaded
also support Kaocha test runner
"},{"location":"testing/test-runners/aero/","title":"Aero","text":"juxt/aero is used to read the kaocha configuration, so reader literals such as #env, #merge, #ref, and #include can be used.
Set up profiles for different stages of the development workflow, dev, test, prod, etc. Each profile has a different configuration making it very easy to switch
{:port 8000\n :database #profile {:prod \"datomic:dev://localhost:4334/my-prod-db2\"\n :test \"datomic:dev://localhost:4334/my-test-db\"\n :default \"datomic:dev://localhost:4334/my-db\"}\n :known-users [{:name \"Alice\"} {:name \"Betty\"}]}\n
Then in application startup function or a component lifecycle library (mount, component, integrant) read in a specific profile
(aero.core/read-config \"config.edn\" {:profile :prod})\n
"},{"location":"testing/test-runners/congnitect-labs-test-runner/","title":"Cognitect Labs Test Runner","text":"Cognitect Labs test-runner is a test runner for Clojure projects defined with deps.edn
and using clojure.test
library which is part of the Clojure standard library.
test-runner aims to provide a standard way to discover and run unit and property-based tests, in a simple to use and lightweight tool.
"},{"location":"testing/test-runners/congnitect-labs-test-runner/#add-test-runner","title":"Add test-runner","text":"Make test-runner available to all projects by adding it to ~/.clojure/deps.edn
. Or add test-runner to specific projects by adding an alias to the project deps.edn
file. Include :extra-paths
configuration to include the standard test
directory so that the runner has access to the test code.
Practicalli Clojure CLI ConfigAlias Definition Practicalli Clojure CLI Config provides aliases for test runner tools, including :test/congnitect
for running Cognitect Labs test runner
Add an alias to run Cognitect Labs test runner, either in the project or user deps.edn
configuration file. ```clojure :test/cognitect {:extra-paths [\"test\"] :extra-deps {com.cognitect/test-runner {:git/url \"https://github.com/cognitect-labs/test-runner.git\" :sha \"f7ef16dc3b8332b0d77bc0274578ad5270fbfedd\"}} :main-opts [\"-m\" \"cognitect.test-runner\"]}
```
"},{"location":"testing/test-runners/congnitect-labs-test-runner/#run-test-runner","title":"Run test runner","text":"Run the Cognitect Labs test runner via the command line
clojure -M:test/cognitect\n
The cognitect.test-runner/-main
function is called which scans the test
directory of the current project tests defined using clojure.test
, running all tests found.
A summary is returned with the results of running the tests.
"},{"location":"testing/test-runners/congnitect-labs-test-runner/#command-line-options","title":"Command line options","text":"Flag Description -d, --dir DIRNAME Name of the directory containing tests. Defaults to \"test\". -n, --namespace SYMBOL Symbol indicating a specific namespace to test. -r, --namespace-regex REGEX Regex for namespaces to test. Defaults to #\".*-test$\" (i.e, only namespaces ending in '-test' are evaluated) -v, --var SYMBOL Symbol indicating the fully qualified name of a specific test. -i, --include KEYWORD Run only tests that have this metadata keyword. -e, --exclude KEYWORD Exclude tests with this metadata keyword. -H, --test-help Display this help message Options can be used multiple times in one command, for a logical OR effect. For example, the following command runs all tests in the practicalli.data.survey
and practicalli.services.survey-report
namespaces that are found in the src
and test
directories
clojure -M:test/cognitect -d test -d src -n practicalli.data.survey -n practicalli.services.survey-report\n
"},{"location":"testing/test-runners/congnitect-labs-test-runner/#test-selectors","title":"Test Selectors","text":"Selectively running tests by including and excluding test categories, from the meta-data added to test definitions, i.e. deftest
.
(deftest ^:integration test-live-system\n (is (= 200 (:status (http/get \"http://example.com\")))))\n
Use the --include
flag with the test runner to specify specific categories of tests
clojure -M:test-runner-cognitect --include :integration\n
The --include
flag can be used multiple times, defining a test category with each include.
clojure -M:test-runner-cognitect --include :integration --include :persistence\n
Clojure Unit Test - categories example integration and develop tests
--exclude
flag runs all tests except those in the given category
clojure -M:test/cognitect --exclude :integration\n
Exclusions take priority over inclusions if both flags are included.
"},{"location":"testing/test-runners/example-projects/","title":"Example projects","text":" - TDD Kata: Recent Song-list - simple tests examples
- Codewars: Rock Paper Scissors (lizard spock) solution -
and
examples - practicalli/numbers-to-words - overly verbose example, ripe for refactor
- practicalli/codewars-guides - deps.edn projects
- practicalli/exercism-clojure-guides - Leiningen projects
"},{"location":"testing/test-runners/example-projects/#sean-corfield-user-manager","title":"Sean Corfield - user manager","text":"User manager has unit tests that also include an embedded database. Tests can run with the Cognitect Labs test runner.
:test
alias includes the test path and a dependency for the H2 database
Cognitect Labs test runner included in the project deps.edn
file as :runner
clojure -M:test:runner
will run the Cognitect Labs runner and include the dependency to run the in-memory database used for the tests.
"},{"location":"testing/test-runners/example-projects/#using-koacha-with-sean-corfield-user-manager","title":"Using koacha with Sean Corfield user manager","text":"Adding a test.edn
file is not sufficient for testing this project with lambdaisland/kaocha, as the H2 dependency is also needed.
Create a bin/koacha
script and add the extra alias
#!/usr/bin/env bash\nclojure -M:test:test-runner-kaocha \"$@\"\n
"},{"location":"testing/test-runners/example-projects/#status-monitor","title":"Status Monitor","text":"Status monitor is a Leiningen project.
Include a :kaocha
profile in the project.clj
file, adding the koacha dependency. The :kaocha
alias sets the main namespace and uses the kaocha profile.
{:dev {:dependencies [[javax.servlet/servlet-api \"2.5\"]\n [ring/ring-mock \"0.3.2\"]]}\n :kaocha {:dependencies [[lambdaisland/kaocha \"1.0.632\"]]}}\n :aliases {\"kaocha\" [\"with-profile\" \"+kaocha\" \"run\" \"-m\" \"kaocha.runner\"]}\n
lein kaocha
will run all the tests
"},{"location":"testing/test-runners/kaocha-test-runner/","title":"LambdaIsland Kaocha Test Runner","text":" lambdaisland/kaocha (cow-cha) is a comprehensive test runner that support unit testing and clojure.spec
generative testing. Clojure and ClojureScript languages are supported.
Kaocha is highly configurable via a tests.edn
configuration file in the root of the project.
"},{"location":"testing/test-runners/kaocha-test-runner/#clojure-cli-config","title":"Clojure CLI Config","text":"Practicalli Clojure CLI ConfigAlias Definition Practicalli Clojure CLI Config configuration contains aliases to run kaocha test runner, using either the -X
or -M
execution flag.
:test/run
- run all tests in the project, stopping on first failing test :test/watch
- watching for file changes and run all tests in the project, stopping on first failing test :test/env
- add supporting paths and libraries for testing projects
Each alias includes :extra-paths [\"test\"]
to include the test
directory on the class path, enabling Koacha test runner to find the unit test code.
Define an alias in the project or user deps.edn
configuration.
For CI services such as CircleCI or GitLabs, add an alias for kaocha to the project deps.edn
file.
Alias definitions for LambdaIsland/Kaocha test runner
Aliases support the -M
(clojure.main) and -X
(clojure.exec) execution options with Clojure CLI.
:test/run\n{:extra-paths [\"test\"]\n :extra-deps {lambdaisland/kaocha {:mvn/version \"1.77.1236\"}}\n :main-opts [\"-m\" \"kaocha.runner\"]\n :exec-fn kaocha.runner/exec-fn\n :exec-args {:fail-fast? true\n :randomize? false}}\n\n;; Kaocha test runner in watch mode\n;; clojure -X:test/watch\n:test/watch\n{:extra-paths [\"test\"]\n :extra-deps {lambdaisland/kaocha {:mvn/version \"1.77.1236\"}}\n :main-opts [\"-m\" \"kaocha.runner\" \"--watch\" \"--fail-fast\" \"--skip-meta\" \":slow\"]\n :exec-fn kaocha.runner/exec-fn\n :exec-args {:watch? true\n :randomize? false\n :fail-fast? true}}\n
Libraries and directories containing code to support testing projects can be added to the :test/env
alias
:test/env\n{:extra-paths [\"test\"]\n :extra-deps {org.clojure/test.check {:mvn/version \"1.1.1\"}}}\n
Alias definitions should include :extra-paths [\"test\"]
to add the test
directory on the class path, enabling Koacha test runner to find the unit test code.
"},{"location":"testing/test-runners/kaocha-test-runner/#run-kaocha","title":"Run Kaocha","text":"Kaocha can be run via make tasks, Clojure CLI, or by creating a kaocha
script.
Babashka task runner could also be used to develop tasks to run kaocha
MakeClojure CLIKaocha script Practialli Makefile contains tasks for testing Clojure projects with Kaocha (and many other common Clojure development tasks)
Practicalli Makefile targets for unit testing Practicalli Makefile includes the following targets for Kaocha test runner Makefile
# ------- Testing -------------------- #\n\ntest-config: ## Run unit tests - stoping on first error\n $(info --------- Runner Configuration ---------)\n clojure -M:test/env:test/run --print-config\n\ntest-profile: ## Profile unit test speed, showing 3 slowest tests\n $(info --------- Runner Profile Tests ---------)\n clojure -M:test/env:test/run --plugin kaocha.plugin/profiling\n\ntest: ## Run unit tests - stoping on first error\n $(info --------- Runner for unit tests ---------)\n clojure -X:test/env:test/run\n\n\ntest-all: ## Run all unit tests regardless of failing tests\n $(info --------- Runner for all unit tests ---------)\n clojure -X:test/env:test/run :fail-fast? false\n\ntest-watch: ## Run tests when changes saved, stopping test run on first error\n $(info --------- Watcher for unit tests ---------)\n clojure -X:test/env:test/run :watch? true\n\ntest-watch-all: ## Run all tests when changes saved, regardless of failing tests\n $(info --------- Watcher for unit tests ---------)\n clojure -X:test/env:test/run :fail-fast? false :watch? true\n\n# ------------------------------------ #\n
Run all tests using the following command from the root of the Clojure project. Kaocha stops if there is a failing task, saving time on running the whole test suite.
make test\n
Use the test-all
target to run all unit tests regardless of failures (execept compiler errors)
make test-all\n
Continually run tests by watching for changes using the :test/watch
alias. If a test fails, Koacha will stop the test run and restart from the failing test when a change is detected. Use watch-all
if all tests should run regardless of failure.
make test-watch\n
Practicalli Clojure CLI Config configuration contains aliases to run kaocha test runner, using either the -X
or -M
execution flag.
Run Kaocha using the clojure
command in a terminal, using the :test/run
which runs all the tests in a project unless a test fails, then kaocha will stop.
clojure -X:test/run\n
Pass :fail-fast? false
as an argument to run all tests regardless of test failure.
clojure -X:test/run :fail-fast? false\n
Continually run tests by watching for changes using the :test/watch
alias. If a test fails, Koacha will stop the test run and restart from the failing test when a change is detected.
clojure -X:test/watch\n
Kaocha recommends adding a bin/kaocha
script to each project, although this is optional. The script calls clojure
with a suitable alias and allows for arguments to be passed to the command using \"$@\"
. Command line options will over-ride the same options in the tests.edn
file.
bin/kaocha
#!/usr/bin/env bash\nclojure -M:test/runner \"$@\"\n
Use the -M
execution option to pass command line flags to the Kaocha test runner. kaocha --fail-fast\n
"},{"location":"testing/test-runners/kaocha-test-runner/#configuring-kaocha","title":"Configuring Kaocha","text":"Kaocha can be configure by options in a tests.edn
configuration file and options passed via the command line (typically added to the bin/kaocha
script).
Create a tests.edn
file in the root of the project directory and add the default configuration.
tests.edn#kaocha/v1 {}\n
The tests.edn
file and command line options combine to make the complete configuration for the projects in the test.
make test-config
runs clojure -M:test/run --print-config
to print out the current kaocha configuration.
Use the default configuration as the basis for customising kaocha test runner for the current project.
Alternative kaocha configuration with aero juxt/aero reader literals such as #env, #merge, #ref, and #include can be used to provide different options to the kaocha configuration. For example, a file change watcher can be configured to run unless kaocha is running in CI server environment.
:kaocha/watch #profile {:default true :ci false}
"},{"location":"testing/test-runners/kaocha-test-runner/#plugins","title":"Plugins","text":"Much of the functionality of Kaocha is provide by plugins
- profiling - lists the slowest tests for each test category
- cucumber - bdd style test
- junit-xml reports - format used by Continuous Integration servers to display results
"},{"location":"testing/test-runners/kaocha-test-runner/#profiling","title":"Profiling","text":"Show the 3 slowest tests for each category of test, after the test results
MakeClojure CLIKaocha Script As a command line option:
make test-profile\n
Pass the profiling plugin as an argument to the Clojure CLI alias using the -M
(clojure.main) execution option
clojure -M:test/env:test/run --plugin kaocha.plugin/profiling\n
As a command line option:
bin/kaocha --plugin kaocha.plugin/profiling\n
Or add the profile plugin to the test.edn
configuration
#kaocha/v1\n{:plugins [:kaocha.plugin/profiling]}\n
"},{"location":"testing/test-runners/kaocha-test-runner/#example-testsedn","title":"Example tests.edn","text":" Practicalli Banking-on-Clojure project is a web application backed by a relational database, using kaocha as the test runner.
:kaocha/tests
defines two types of tests. The hash-map containing :kaocha.testable/id :unit
defines the configuration for unit tests using clojure.test
. The hash-map containing :kaocha.testable/id :generative-fdef-checks
are generative tests using clojure spec.
:kaocha/color?
and :kaocha/watch
use a value dependent on the #profile
kaocha is run under.
Banking on Clojure project - Kaocha test.edn configuration
#kaocha/v1\n{:kaocha/tests\n [{:kaocha.testable/id :unit\n :kaocha.testable/type :kaocha.type/clojure.test\n :kaocha/ns-patterns [\"-test$\"],\n :kaocha/source-paths [\"src\"],\n :kaocha/test-paths [\"test\"],\n :kaocha.filter/skip-meta [:kaocha/skip]}\n\n {:kaocha.testable/id :generative-fdef-checks\n :kaocha.testable/type :kaocha.type/spec.test.check\n :kaocha/source-paths [\"src\"]\n :kaocha.spec.test.check/checks [{:kaocha.spec.test.check/syms :all-fdefs\n :clojure.spec.test.check/instrument? true\n :clojure.spec.test.check/check-asserts? true\n :clojure.spec.test.check/opts {:num-tests 10}}]}\n ]\n\n :kaocha/reporter [kaocha.report/documentation]\n\n :kaocha/color? #profile {:default true\n :ci false}\n\n ;; Run tests of file changes, unless running in CI server\n :kaocha/watch #profile {:default true :ci false}\n\n :kaocha/fail-fast? true\n\n :kaocha.plugin.randomize/randomize? false\n\n :kaocha/plugins\n [:kaocha.plugin/randomize\n :kaocha.plugin/filter\n :kaocha.plugin/capture-output\n :kaocha.plugin.alpha/spec-test-check]\n\n :kaocha.plugin.capture-output/capture-output? true\n }\n
The configuration shows how to explicitly configure different sections, although configuration could be streamlined by using more default values.
"},{"location":"testing/unit-testing/","title":"Clojure Unit Testing","text":"The function is the unit under test in Clojure. All public functions that form the API of their respective namespace should have a matching test, i.e. (deftest)
definition.
clojure.test
namespace provides functions for defining and running unit tests and is available in the Clojure library for any project to use.
"},{"location":"testing/unit-testing/#unit-test-principles","title":"Unit Test Principles","text":" - A
test
namespace for each src
namespace under test - A
deftest
function for each function under test, named after the function its testing with -test
at the end of the name - Multiple assertions (
is
are
) for one function is
defines an assertion returning true (test pass) or false (test fail), typically a comparison between a known value and the result of a function call are
to testing similar functionality with different data sets (or use generative testing) testing
to logically group assertions and provide a meaningful description of that grouping (easier to identify tests when they fail) use-fixtures
to call fixture functions that setup and tear down any state required for test(s) to run - Test API rather than implementation
- test generic helper or private functions through public functions of each namespace (minimise test churn and time to run all tests)
^:helper
meta-data on deftest
for more generic functions, to skip those tests via a test selector - Use generative testing to create more maintainable test code with more extensive range of data
- Use test selectors with a test runner to selectively run tests and optimise speed of test runs
- Limit mocking of systems to integration tests (although mocking data is good everywhere)
Code should evaluate or have line comments All Clojure code should be valid syntax and able to be evaluated (compiled), even code within a (comment )
expression or after a #_
reader comment.
Code commented with a line comment, ;;
, will not be read by Clojure and cannot cause compilation errors when evaluated
"},{"location":"testing/unit-testing/#running-tests","title":"Running tests","text":"Test runners can run be run in the REPL used for development or run separately via the command line and continuous integration tasks.
"},{"location":"testing/unit-testing/#run-tests-in-editor-connected-repl","title":"Run tests in Editor connected REPL","text":"Using an editor connected REPL keeps the workflow in one tool and helps maintain focus. Using editor commands to run the tests and navigable error reports provides an effective flow to run and debug issues.
Ensure test
directory is on the class path when evaluating tests in the REPL, otherwise the (deftest)
test definitions may not be found.
If functions or their associated tests are changed, they should be evaluated in the REPL before running tests to ensure those changes are loaded into the REPL.
If renaming a function or deftest
, the original name should be removed from the REPL to avoid phantom tests (older definitions of tests that were evaluated in the REPL and still run, even though those tests are no longer in the source code).
Editors may include a command to remove function or test definitions, e.g. CIDER has undef
command
The original name can also be removed using Clojure (ns-unmap 'namespace 'name)
, where namespace is where the name of the function or test is defined and name is the name of the function or test.
(ns practicalli.system-monitor) ; namespace definition\n(defn dashboard [] ,,,) ; original function name\n(defn dashboard-page [] ,,,) ; new function name\n(undef 'practicalli.system-monitor 'dashboard) ; remove original function name\n
Stop and start the REPL process ensures all function and tests are correctly loaded
"},{"location":"testing/unit-testing/#command-line-test-runners","title":"Command line test runners","text":"Command line test runners (i.e. koacha, Cognitect Labs) load function and test definitions from the source code files each time, ensuring tests are run and a clean REPL state is created on each run. This clearly defined REPL state is especially valuable for running repeatable integration tests.
Automate running the tests using a watch process, giving instant fast feedback, especially when displaying both the editor and test runner command line.
test runner can be configure to run only selective tests (i.e kaocha)
Run all tests (including integration tests) via the command line before pushing commits to ensure all changes to the code have been tested.
If tests are not running in the REPL or are returning unexpected errors, a command line test runner is a useful way to diagnose if it is the test code or test tools causing the error.
The CLI approach is also more robust for longer running tests than running within an editor.
Avoid stale tests
Running tests via a command line test runner will never experience stale tests, as long as all relevant changes are saved to the source code files.
"},{"location":"testing/unit-testing/#run-tests-in-the-repl","title":"Run tests in the REPL","text":"clojure.test
includes the run-tests
function that runs tests (deftest
definitions) in given namespaces and run-all-tests
which runs all tests in all namespaces.
(run-all-tests) ; run all tests in all namespaces\n\n(run-tests 'practicalli.system-monitor-test) ; run all tests in practicalli.system-monitor-test\n
run-tests
and run-all-tests
are a less common approach as the command line and editor driven test runners provide a rich set of features
"},{"location":"testing/unit-testing/#project-structure-with-tests","title":"Project structure with tests","text":"For each source code file in src
there should be a corresponding file in test
directory with the same name and _test
postfix.
For example, code to test the src/codewars/rock_paper_scissors.clj
is saved in the file src/codewars/rock_paper_scissors_test.clj
file.
Example project: CodeWars: Rock Paper Scissors
"},{"location":"testing/unit-testing/#source-and-test-namespaces","title":"Source and Test Namespaces","text":"As with file names, the namespaces for each test code file is the same as the source code it is testing, with a -test
postfix.
codewars/rock-paper-scissors
source code namespace will have a matching codewars/rock-paper-scissors-test
namespace.
Create Projects from templates Templates typically include a parallel test
and src
directory structure. The clj-new
tool has build it templates (app, lib) and will create src
and test
directories in the projects it creates.
clojure -T:project/new :template app :name practicalli/rock-paper-scissors-lizard-spock
"},{"location":"testing/unit-testing/#project-examples-code-challenges-with-unit-tests","title":"Project Examples: Code challenges with unit tests","text":" - TDD Kata: Recent Song-list - simple tests examples
- Codewars: Rock Paper Scissors (lizard spock) solution -
and
examples - practicalli/numbers-to-words - overly verbose example, ripe for refactor
- practicalli/codewars-guides - deps.edn projects
- practicalli/exercism-clojure-guides - Leiningen projects
"},{"location":"testing/unit-testing/#references","title":"References","text":" - Example based unit testing in Clojure - PurelyFunctional.tv
"},{"location":"testing/unit-testing/clojure-test-expectations/","title":"Clojure test Expectations","text":"clojure.test.expectations
uses the same tooling as clojure.test
and only depends on that library.
"},{"location":"testing/unit-testing/clojure-test-expectations/#using-a-depsedn-alias","title":"Using a deps.edn alias","text":" Practicalli Clojure CLI Config
"},{"location":"testing/unit-testing/clojure-test-expectations/#add-dependency","title":"Add dependency","text":"Edit the deps.edn file for the current project
"},{"location":"testing/unit-testing/configure-projects-for-tests/","title":"Configure Unit Testing for deps.edn projects","text":"clojure.test
namespace is part of the Clojure standard library, so the Clojure library is the only dependency required in the project.
{:deps {org.clojure/clojure {:mvn/version \"1.10.3\"}}}\n
Unit tests code should reside under the test
directory of a project. The test
directory should not be part of the main classpath, otherwise test classes would be included in the project packaging and deployed to production.
Use an alias to add the test
directory, either from a user level configuration or the Clojure project deps.edn
configuration file.
{% tabs practicalli=\"practicalli/clojure-deps-edn\", deps=\"Manual deps.edn projects\" %}
{% content \"practicalli\" %}
"},{"location":"testing/unit-testing/configure-projects-for-tests/#adding-test-path","title":"Adding test path","text":" Practicalli Clojure CLI Config user-level configuration contains several aliases for Clojure and ClojureScript test runners, each alias includes the test
directory as an :extra-path
.
:test/env
alias is also provided, which simply adds the test
directory to the class path. The :test/env
alias is useful in concert with other aliases or for editors that have their own built in test runners (e.g. CIDER).
"},{"location":"testing/unit-testing/configure-projects-for-tests/#using-kaocha-test-runner","title":"Using kaocha test runner","text":"lambdaisland/kaocha is a fast and comprehensive test runner for Clojure and ClojureScript.
:test/run
alias runs all tests from the source code files, called with the clojure
command in the root of the Clojure project. The alias includes test
as an extra path and calls the Kaocha test runner.
clojure -X:test/run\n
Kaocha can also watch for changes saved to file and re-run the tests.
clojure -X:test/watch\n
Both kaocha aliases are configured to stop if a test fails. When re-running kaocha, only failed tests and tests that have changed are run (including tests where the code they are testing has changed).
"},{"location":"testing/unit-testing/configure-projects-for-tests/#alias-to-include-the-test-directory","title":"Alias to include the test directory","text":"Add the following aliases to the Clojure CLI tools user wide configuration, (e.g. ~/.clojure/deps.edn
), or to the project deps.edn
file.
To use a test runners with a deps.edn
projects, the test
directory should be on the classpath.
practicalli/clojure-deps-edn defines an environment alias to include the test path.
:aliases\n{\n :test/env\n {:extra-paths [\"test\"]}\n}\n
"},{"location":"testing/unit-testing/configure-projects-for-tests/#cognitect-labs-clojure-test-runner","title":"Cognitect labs Clojure test runner","text":":test/cognitect
is a simple to use test runner for Clojure projects.
clojure -X:test/cognitect\n
"},{"location":"testing/unit-testing/configure-projects-for-tests/#kaocha-unit-test-and-clojure-spec-runner","title":"Kaocha unit test and clojure spec runner","text":":test/kaocha
alias unit test runner that also supports Clojure Spec functional tests. the kaocha test runner on the current project. Add a test.edn
file to configure which tests are run by kaocha.
clojure -X:test/kaocha\n
"},{"location":"testing/unit-testing/configure-projects-for-tests/#references","title":"References","text":" - Practicalli Spacemacs - Unit testing with Cider and Kaocha in Emacs
- lambdaisland/kaocha is a test runner that supports Clojure CLI, Leiningen and Boot project configuration.
- Leiningen project configuration for unit testing
"},{"location":"testing/unit-testing/fixtures/","title":"Test Fixtures","text":"Unit tests may require the system to be in a particular state before running a test. The state may need to be reset after running a test such as a database
Fixtures allow you to run code before and after tests, to set up the context in which tests should be run. Consider when fixtures should be run, especially fixtures that take a noticeable time to setup or tear down.
Slow running unit tests lead to unit tests not being run so often and therefore limit their value.
Organise tests with test selectors
Tests with fixtures may be slower to run so separate them by using a test selector, a piece of meta data attached to a deftest
definition. For example, add the ^:persistence
meta data to test that require database fixtures (deftest ^:database db-bulk-upload). The test runner can be instructed to skip or focus on tests with specific meta data.
"},{"location":"testing/unit-testing/fixtures/#defining-a-fixture","title":"Defining a fixture","text":"Require the use-fixtures
function in the require expression for clojure.test
(ns domain.application-test\n (:require [clojure.test :refer [deftest is testing use-fixtures]]))\n
A fixture is a standard Clojure function which takes a function as an argument. The function passed as an argument is either an individual test or all tests in the namespace, depending on how the fixture is used.
(defn my-fixture [test-run]\n ;; Setup: define bindings, create state, etc.\n\n (test-run) ;; Run the relevant tests for the fixture (see `use-fixtures`)\n\n ;; Tear-down: reset state to a known value\n )\n
"},{"location":"testing/unit-testing/fixtures/#when-to-run-fixtures","title":"When to run fixtures","text":"The use-fixtures
function defines when a fixture should be called when running the unit tests in each namespace. All Clojure unit test runners should support the use-fixtures
definitions when running the tests.
When Description (use-fixtures :once fixture1 fixture2)
Run the fixtures once for the namespace. (use-fixtures :each fixture1 fixture2)
Run the fixtures for each deftest
in the namespace Once
The setup in the fixture is run, followed by all the deftest
functions in the namespace, then the fixture tear-down is run.
Running a fixture once per namespace is useful for establishing a database connection or creating a particular state of data for all the unit tests to use.
Each
The fixture setup is run before each deftest
function in the namespace. The fixture tear-down is run after each deftest
function.
"},{"location":"testing/unit-testing/fixtures/#anonymous-function-fixture","title":"Anonymous function fixture","text":"The use-fixtures
function can also include anonymous function as well as a namespace scoped functions (deftest
).
(use-fixtures :each (fn [f] #_setup... (f) #_teardown))\n
defn
functions are usually recommended unless the fixture code is relatively terse.
Development database
Define a fixture to reset the database before running a test and clear the database after each test.
The create-database
and delete-database
are helper functions that are part of the namespace under test.
(defn database-reset-fixture\n \"Setup: drop all tables, creates new tables\n Teardown: drop all tables\n SQL schema code has if clauses to avoid errors running SQL code.\n Arguments:\n test-function - a function to run a specific test\"\n [test-function]\n (SUT/create-database)\n (test-function)\n (SUT/delete-database))\n
The fixture should be used for each unit test (deftest
) that is defined in the namespace the database-reset-fixture
is defined in.
(use-fixtures :each database-reset-fixture)\n
"},{"location":"testing/unit-testing/fixtures/#references","title":"References","text":" use-fixtures
- Clojuredocs.org - Kaocha - focusing and skipping
- Clojure Test Fixtures - astrocaribe
"},{"location":"testing/unit-testing/test-selectors/","title":"Test Selectors","text":"As a project grows in scope its important that tests continue to run quickly. Test runs which take a noticeable time to complete diminish the motivation to run tests frequently.
Divide tests into categories to run selective tests, continuing to provide fast feedback. Longer running tests can be run less often without loosing quality in the feedback from tests.
Test runners use test selectors to run a specific categories, or exclude test selectors so all tests except that category runs.
kaocha focus and skipping
kaocha can group tests into categories in the tests.edn
configuration, providing a way to focus or exclude different types of tests (e.g. :unit
and :spec
)
"},{"location":"testing/unit-testing/test-selectors/#test-category-metadata","title":"Test category metadata","text":"Add metadata to deftest
functions to provide categories of tests, e.g. integration
, persistence
, etc.
(deftest ^:integration register-customer\n (is ,,,))\n
Example from Banking on Clojure
(deftest ^:persistence new-customer-test\n (testing \"New customer generative testing\")\n (is (spec/valid?\n :customer/id\n (:customer/id (SUT/new-customer\n (spec-gen/generate (spec/gen :customer/unregistered)))))))\n
"},{"location":"testing/unit-testing/test-selectors/#test-selectors_1","title":"Test Selectors","text":"Start a test selective category of tests running by specifying test selectors to include or exclude.
KaochaSpacemacsCiderCognitect kaocha supports meta data on deftest
expressions and has its own metadata tag for skipping tests, ^:koacha/skip
Examples of tests with and without test selectors
(deftest simple-test\n (is (= 1 1)))\n\n(deftest ^:integration system-update-test\n (is (spec/valid? :system/update (long-running-function))))\n\n(deftest ^:kaocha/skip under-development-test\n (is (= 3 21/7)))\n
Tests with test selector metadata can be skipped using a tests.edn
configuration
#kaocha/v1\n{:tests [{:kaocha.filter/skip-meta [:integration]}]}\n
Running kaocha will only run the simple-test
, skipping the other two tests.
Specifying --skip-meta
on the command line gives the same results
bin/kaocha --skip-meta :metadata-name\n
Running tests with the universal argument will prompt for test selector filters and only Run those tests that match the selector inclusions/exclusions.
SPC u , t a
runs all tests, prompting for tests selector names to include (space separated)
Then prompting for the test selectors to exclude. A warning displays if CIDER does not find the test selector name.
Invoke the CIDER test runner commands with the universal argument and CIDER will prompt for test selector filters, running only those tests that match the selector inclusions/exclusions.
C-c C-t p
runs all the tests in a project.
C-u C-c C-t p
prompts for test selectors and runs the matching tests in a project.
C-c C-t l
runs all tests currently evaluated in the REPL.
C-u C-c C-t l
prompts for test selectors and runs the matching tests currently evaluated in the REPL.
CIDER first prompts for the test selectors to include:
Then prompts for the test selectors to exclude. A warning displays if CIDER does not find the test selector name.
The Cognitect Labs test runner uses command line options to specify test selectors, --include
and --exclude
.
Practicalli Clojure CLI Config configuration provides the :test/congnitect
alias.
clojure -M:test/cognitect --include :database
only runs tests with the ^:database
test selector
clojure -M:test/cognitect --exclude :integration
runs all tests except those with the ^:integration
test selector
"},{"location":"testing/unit-testing/test-selectors/#references","title":"References","text":" - Kaocha - Focus and skipping tests with test selectors
- Convoluted Magic of Leiningen Test Selectors
- How to use Leiningen test selectors to filter by test name
- Stack overflow - Lein test with Selectors - how to specify a test for multiple conditions
"},{"location":"testing/unit-testing/writing-unit-tests/","title":"Writing Unit Tests with clojure.test","text":"Unit tests are centered on assertions, testing if something returns a true or false value.
is
function is the simplest assertion and the most common. It checks to see if an expression given is true and if so then the assertion passes. If the value is false then that assertion fails.
as
provides a way to run the same assertion with different values, testing the same function with a collection of arguments. This provides a clean way to test a function without lots of repetition.
testing
is a macro to group multiple assertions together, providing a string in which to describe the context the assertions are testing. The well worded context string is invaluable for narrowing down on which assertions are failing.
deftest
is a collection of assertions, with or without testing
expressions. The name of the deftest should be the name of the function it is testing with -test
as a postfix. For example, the function practicalli.playground/calculator
would have a deftest
called practicalli.playground-test/calculator-test
"},{"location":"testing/unit-testing/writing-unit-tests/#requiring-namespaces","title":"Requiring Namespaces","text":"A test namespace has a singular purpose to test a matching src namespace.
The idiomatic approach is to :refer
specific functions from clojure.test
as those functions are used.
The namespace to be tested is referred using a meaningful alias. The alias highlight the exact functions being tested in the body of the code. This provides a visual way to separate functions under test with other test functions, especially if there are helper functions or vars used for test data.
REPLproject (require '[clojure.test :refer [are deftest is testing]])\n
The namespace under test should be referred using the alias so they are readily identified within the test code. (require '[practicalli.gameboard.spec :as gameboard-spec])\n
Add clojure.test
to the namespace definition along with the namespace under test.
(ns practicalli.app-namespace-test\n (:require '[clojure.test :refer [are deftest is testing]]\n [practicalli.gameboard.spec :as gameboard-spec]))\n
"},{"location":"testing/unit-testing/writing-unit-tests/#simple-example","title":"Simple Example","text":"(deftest public-function-in-namespace-test\n (testing \"A description of the test\"\n (is (= 1 (public-function arg)))\n (is (predicate-function? arg))))\n
"},{"location":"testing/unit-testing/writing-unit-tests/#assertion-data-set","title":"Assertion data set","text":"The are
macro can also be used to define assertions, especially when there would otherwise be multiple assertions that only differ by their test data.
An are
assertion defines the arguments to the test, the logic of the test and a series of test data.
(are [x y] (= x y)\n 2 (+ 1 1)\n 4 (* 2 2))\n
This is equivalent to writing
(do (is (= 2 (+ 1 1)))\n (is (= 4 (* 2 2))))\n
Refactor test assertion to use data set
Assertions in the test take the same shape of values, so are candidates to refactor to the are
macro.
(deftest encoder-test\n (testing \"Tens to number words\"\n (is (= '(\"zero\" \"ten\")\n (character-sequence->word-sequence dictionary/digit->word '(\\0 \\1 \\0))))\n (is (= '(\"zero\" \"eleven\")\n (character-sequence->word-sequence dictionary/digit->word '(\\0 \\1 \\1))))\n (is (= '(\"zero\" \"twenty\" \"zero\")\n (character-sequence->word-sequence dictionary/digit->word '(\\0 \\2 \\0))))\n (is (= '(\"zero\" \"twenty\"\"one\")\n (character-sequence->word-sequence dictionary/digit->word '(\\0 \\2 \\1))))\n (is (= '(\"zero\" \"forty\" \"two\")\n (character-sequence->word-sequence dictionary/digit->word '(\\0 \\4 \\2))))))\n
Refactor the assertions using are simplifies the code, making it simpler to change further and extend with more data. (deftest encoder-test\n (testing \"Tens to number words\"\n (are [words numbers]\n (= words (character-sequence->word-sequence dictionary/digit->word numbers))\n '(\"zero\" \"ten\") '(\\0 \\1 \\0)\n '(\"zero\" \"eleven\") '(\\0 \\1 \\1)\n '(\"zero\" \"twenty\" \"zero\") '(\\0 \\2 \\0)\n '(\"zero\" \"twenty\"\"one\") '(\\0 \\2 \\1)\n '(\"zero\" \"forty\" \"two\") '(\\0 \\4 \\2)))\n
Generative Testing provides a wide range of values
Generating test data from Clojure Specs provides an extensive set of values that provide an effective way to test functions.
"},{"location":"testing/unit-testing/writing-unit-tests/#reference","title":"Reference","text":"clojure.test API
"},{"location":"testing/unit-testing/writing-unit-tests/#code-challenges-with-tests","title":"Code challenges with tests","text":"TDD Kata: Recent Song-list TDD Kata: Numbers in words Codewars: Rock Paper Scissors (lizard spock) solution practicalli/codewars-guides practicalli/exercism-clojure-guides
"},{"location":"thinking-functionally/","title":"Thinking Functionally","text":"In this section I cover some simple examples of Clojure code to help you think about the concepts involved in functional programming.
An overview of thinking functionally is also covered in the presentation entitled Getting into Functional Programming with Clojure on slideshare and its accompanying youtube video
Get into Functional Programming with Clojure from John Stevenson Get a free Clojurians slack community account
"},{"location":"thinking-functionally/arity/","title":"Arity","text":"Fixme work in progress
"},{"location":"thinking-functionally/example-hitchhikers-guide/","title":"Example: Hitchhikers Guide","text":"This is an example of using the threading macros and a REPL to give fast feedback as you are developing code.
Suggest you use the assumed perfectly legal copy of the Hitch-hickers book text using the slurp
function
Approximate algorithm
- Use a regular expression to create a collection of individual words - eg. #\"[a-zA-Z0-9|']+\"
- Convert all the words to lower case so they match with common words source -
clojure.string/lower-case
Remove
the common English words used in the book, leaving more context specific words - Calculate the
frequencies
of the remaining words, returning a map of word & word count pairs Sort-by
word count values in the map Reverse
the collection so the most commonly used word is the first element in the map
(def book (slurp \"http://clearwhitelight.org/hitch/hhgttg.txt\"))\n\n(def common-english-words\n (-> (slurp \"https://www.textfixer.com/tutorials/common-english-words.txt\")\n (clojure.string/split #\",\")\n set))\n\n;; using a function to pull in any book\n(defn get-book [book-url]\n (slurp book-url))\n\n\n(defn -main [book-url]\n (->> (get-book book-url)\n (re-seq #\"[a-zA-Z0-9|']+\")\n (map #(clojure.string/lower-case %))\n (remove common-english-words)\n frequencies\n (sort-by val)\n reverse))\n\n;; Call the program\n\n(-main \"http://clearwhitelight.org/hitch/hhgttg.txt\")\n
"},{"location":"thinking-functionally/example-hitchhikers-guide/#note","title":"NOTE","text":"Write functions that will give a list of the most used words used in a book, excluding the common English words like \"the, and, it, I\". Join those functions with a threading macro.
"},{"location":"thinking-functionally/example-hitchhikers-guide/#deconstructing-the-code-in-the-repl","title":"Deconstructing the code in the repl","text":"To understand what each of the functions do in the -main
function then you can simply comment out one or more expressions using in front of the expression #_
(defn -main [book-url]\n (->> (get-book book-url)\n #_(re-seq #\"[a-zA-Z0-9|']+\")\n #_(map #(clojure.string/lower-case %))\n #_(remove common-english-words)\n #_frequencies\n #_(sort-by val)\n #_reverse))\n
Now the -main
function will only return the result of the (get-book book-url)
function. To see what each of the other lines do, simply remove the #_ character from the front of an expression and re-evaluate the -main
function in the repl
Hint In Spacemacs / Emacs, the keybinding C-c C-p show the output in a separate buffer. Very useful when the function returns a large results set.
"},{"location":"thinking-functionally/example-hitchhikers-guide/#off-line-sources-of-hitch-hickers-book-and-common-english-words","title":"Off-line sources of Hitch-hickers book and common English words","text":"(def book (slurp \"./hhgttg.txt\"))\n\n(def common-english-words\n (-> (slurp \"common-english-words.txt\")\n (clojure.string/split #\",\")\n set))\n
Original concept from Misophistful: Understanding thread macros in clojure
Hint The slurp
function holds the contents of the whole file in memory, so it may not be appropriate for very large files. If you are dealing with a large file, consider wrapping slurp in a lazy evaluation or use Java IO (eg. java.io.BufferedReader
, java.io.FileReader.
). See the Clojure I/O cookbook and The Ins & Outs of Clojure for examples.
"},{"location":"thinking-functionally/first-class-functions/","title":"First Class functions","text":"Idempotent - given the same input you get the same output
(+ 1 2 3 4 5 6 7 8 9 10)\n
The range
function generates a sequence of numbers and when given arguments it does so from a specific range. The second number is exclusive, so for 1 to 10 the second argument should be 11.
(range 1 11)\n
Unfortunately we cant just add the result of a range, because it returns a lazy sequence So (range)
by itself will create an error
(+ 1 (range 1 11))\n
Using a function called reduce
we can calculate a single total value from all the numbers in the collection.
The reduce function take 2 arguments, the first is the function to apply to a data structure, the second is the data structure.
(reduce + (range 1 11))\n\n(reduce + (1 2 3 4 5 6 7 8 9 10))\n
"},{"location":"thinking-functionally/first-class-functions/#note","title":"Note::","text":"Write an expression to add up the numbers from 1 to 10 and return the overall total.
"},{"location":"thinking-functionally/first-class-functions/#note_1","title":"Note::","text":"Create an expression to do the same calculation, but without having to write all the numbers. Hint: consider the functions called range and reduce.
"},{"location":"thinking-functionally/function-composition/","title":"Function Composition","text":"We have discussed how functional programs are essentially a number of functions that work together, this is called composition (functional composition).
(let [calculated-value (* 10 (reduce + (map inc (range 5))))]\n calculated-value)\n
This expression is common in the Lisp & Clojure languages. Occasionally the created expressions can becomes challenging to read. To overcome this parsing complexity, developers often break down a more complex expression into its parts, extracting code into its own function.
Note Brake down the above example into each expression that gives a value
(range 5)\n\n(map inc (range 5))\n\n(reduce + (map inc (range 5)))\n\n(* 10 (reduce + (map inc (range 5))))\n\n\n;; Additional examples\n\n;; Use a let expression for code that is used more than once in a function\n\n(let [calculated-value (* 10 (reduce + (map inc (range 5))))]\n calculated-value)\n\n;; Use defn to define a function for code that multiple functions will call\n;; and generalise the function with arguments\n\n(defn common-data-calculation\n [certainty-factor scope]\n (* certainty-factor (reduce + (map inc (range scope)))))\n
"},{"location":"thinking-functionally/functors/","title":"Functors","text":"Fixme work in progress
Put simply, a function that takes a value and a function as its arguments, eg map
. The argument pass as a value is most commonly a collection type (vector, map, string, list).
From Wikipedia
In mathematics, a functor is a type of mapping between categories which is applied in category theory. Functors can be thought of as homomorphisms between categories. In the category of small categories, functors can be thought of more generally as morphisms.
A functor applies the given function to each element in the the collection by unpacking and each element from the collection and passing it to the function as an argument. The result from each application of the function from the element of the collection is put into a new collection. This new collection is returned once all elements of the original collection have been processed.
The function, eg. + is applied in turn to each value and returns a structured value as a result, eg. a list or vector
(map inc [1 2 3 4 5])\n\n(inc 1 )\n
"},{"location":"thinking-functionally/higher-order-functions/","title":"Higher Order functions","text":"Functions can be used as an arguments to other functions as we have seen in function composition. This is possible because a function always evaluates to a value. This is the basis of function composition.
Higher Order functions can also return a function definition, as when that function definition is evaluated it to will return a value.
You could have a function that returns a function definition which in turn returns a function definition, but at some point this will get very confusing for the developers (yes, that means you).
(filter\n even?\n (range 1 10))\n
(defn twice [f]\n ,,,)\n
;; Our higher order function\n\n(defn twice [function x]\n (function (function x)))\n\n(twice\n (fn [arg]\n (* 3.14 arg))\n 21)\n;; => 207.0516\n\n;; using the short syntax for a function definition\n\n(twice #(+ 3.14 %) 21)\n;; => 207.0516\n
(defn calculation [f]\n ,,,)\n
(defn calculation [f]\n (fn [& args]\n (reduce f args)))\n\n((calculation +) 1 1 2 3 5 8 13)\n\n;; The result of `(calculation +)` is also in a list,\n;; so it will be called as a function, with the arguments 1 1 2 3 5 8 13\n
"},{"location":"thinking-functionally/higher-order-functions/#notereturn-the-even-numbers-from-1-to-10","title":"Note::Return the even numbers from 1 to 10","text":"Generate a range of numbers from 1 to 10
Use a function that checks if a number is even and filter the range of numbers to return only the numbers that match
"},{"location":"thinking-functionally/higher-order-functions/#notecreate-a-named-function-as-a-higher-order-function-called-twice","title":"Note::Create a named function as a higher order function called twice
","text":"The function twice which takes a function and value as arguments.
The twice function should call the function passed as an argument on the value passed as an argument.
The result should be then used as an argument to calling the function passed as an argument again.
Call the twice function with an inline function which takes a number as an argument and adds it to Pi, 3.14
.
"},{"location":"thinking-functionally/higher-order-functions/#notedefine-a-function-that-returns-a-function","title":"Note::Define a function that returns a function","text":"The function should take a clojure.core function for a mathematical calculation, i.e. +
, -
, *
, /
The returning function should take one or more arguments [& args]
and use the function originally passed as an argument to reduce
the data to a single value.
"},{"location":"thinking-functionally/higher-order-functions/#references","title":"References","text":" - Writing Elegant Clojure code using Higher Order functions
"},{"location":"thinking-functionally/homoiconicity/","title":"Homoiconicity","text":"Clojure is a homoiconic language, which is a term describing the fact that Clojure programs are represented by Clojure data structures.
In Clojure you write your business logic as functions. A function is defined using a list structure. A function is called using a list structure, as the first element of a list is evaluated as a function call.
Hint Everything in Clojure is a List (or vector, map, set).
This is a very important difference between Clojure (and Common Lisp) and most other programming languages - Clojure is defined in terms of the evaluation of data structures and not in terms of the syntax of character streams/files.
It is quite easy for Clojure programs to manipulate, transform and produce other Clojure programs. This is essentially what macros do in Clojure, they re-write Clojure for you.
Hint If you were going to create Skynet, it would be so much easier to do in Clojure
"},{"location":"thinking-functionally/homoiconicity/#an-example","title":"An example","text":"Consider the following expression:
(let [x 1] \n (inc x))\n
Evaluating the above code in the REPL returns 2
because the repl compiles and executes any code entered into it. But [x 1]
is also a literal vector data structure when it appears in a different context.
All Clojure code can be interpreted as data in this way. In fact, Clojure is a superset of EDN \u2013 Extensible Data Notation, a data transfer format similar to JSON. EDN supports numbers, strings, lists (1 2 3), vectors [1 2 3], maps {\"key\" \"value\"}.
If this sounds and looks a lot like Clojure syntax, it\u2019s because it is. The relationship between Clojure and EDN is similar to that of Javascript and JSON, but much more powerful.
In Clojure, unlike JavaScript, all code is written in this data format. We can look at our let statement not as Clojure code, but an EDN data structure. Let\u2019s take a closer look:
(let [x 1] \n (inc x))\n
In this data structure, there are four different types of data.
- 1 is a literal integer.
- let, x, and inc are symbols. A symbol is an object representing a name \u2013 think a string, but as an atomic object and not a sequence of characters.
- [x 1] is a vector containing two elements: symbol, x, and an integer, 1. Square brackets always signify vectors when talking about EDN data structures.
- (inc x) is a list (a linked list data structure) containing two symbols, inc and x.
When thinking about a piece of Clojure code as a data structure, we say we are talking about the form. Clojure programmers don\u2019t normally talk about EDN, there are just two ways to think about any bit of Clojure: 1) as code that will execute or 2) as a form, a data structure composed of numbers, symbols, keywords, strings, vectors, lists, maps, etc.
Symbols are particularly important. They are first class names. In Clojure, we distinguish between a variable and the name of that variable. When our code is executing, x refers to the variable established by our let binding. But when we deal with that code as a form, x is just a piece of data, it\u2019s a name, which in Clojure is called a symbol.
This is why Clojure is homoiconic. Code forms are data structures and data structures can be thought of as forms and executed as code. This transformation is quite literal, and two core operations, quote and eval are key ingredients to this potion.
"},{"location":"thinking-functionally/homoiconicity/#references","title":"References","text":" - The Reader - Clojure. org
- Homoiconicity - Wikipedia
- Is Clojure Homoiconic - muhuk.com
- Understanding Homoiconicity in Clojure - Drew Colthorp
"},{"location":"thinking-functionally/immutability/","title":"Immutability","text":"There is a strong emphasis on immutability in Clojure. Rather than create variables that change, Clojure uses values that do not change.
Values in Clojure include numbers, characters, strings.
When functions act on values, a new value is created and returned, rather than modifying the existing value.
TODO include a diagram to visualise this...
"},{"location":"thinking-functionally/immutability/#immutabile-data-structures","title":"Immutabile data structures","text":"List, Map, Vector and Set are all immutable data structures in Clojure.
So when you use these data structures with a function, a new data structure is returned.
Hint When a new data structure is created from an existing data structure, then under the covers the two data structures actually share memory use for any elements that are common. This keeps copies very cheap to create in terms of memory used.
See the section on data structures for more details.
"},{"location":"thinking-functionally/immutable-collections/","title":"Immutable collections","text":"As we have discussed, immutable data structures cannot be changed. So when you run a function over a collection a copy of that collection is returned. Lets see this by running some code in the REPL.
Note Define a data structure called numbers
using a vector. Then write a function that uses the map
and inc
function to increment all the numbers in a vector.
Then check the current value of the numbers
data structure by evaluating its name.
;; define the data structure \n(defn numbers [1 2 3 4 5])\n\n;; increment the numbers\n(map inc numbers)\n\n;; see the current value of numbers\nnumbers\n
Note Use the conj
function to first add the number 5
to the numbers
vector from the previous exercise and check the value of numbers
. Then add the number 6
to the numbers
vector and check the value of numbers
.
Finally, use the conj
function to add both 5
and 6
to the numbers
vector and check the value of numbers
(def numbers [1 2 3 4])\n\n;; add 5 to the numbers vector\n(conj numbers 5)\n\n;; check the value of numbers\nnumbers\n;; => [1 2 3 4]\n\n;; add 6 to the numbers vector\n(conj numbers 6)\n\n;; check the value of numbers\nnumbers\n;; => [1 2 3 4]\n\n;; add 5 and 6 to the numbers vector\n(conj numbers 5 6)\n\n;; Alternatively, you can use the threading macro to chain two conj function calls\n(-> numbers\n (conj 5)\n (conj 6))\n\n;; check the value of numbers\nnumbers\n;; => [1 2 3 4]\n
So even though we have applied several functions on the numbers
data structure it still has the same value.
"},{"location":"thinking-functionally/immutable-local-bindings/","title":"Immutable Local Bindings","text":"Names can be bound to values & and data structures with either the def
or let
function. The def
binding is global to the namespace, however the let
function is local to its use.
Hint The let
function is typically used to define names within a function definition, or in snippets of code created during repl driven development.
(let [five 5]\n (str \"Within the let expression the value is \" five))\n;; => Within the let expression the value is 5\n\n;; evaluating the name five outside the let expression returns an error\nfive\n;; => Unable to resolve symbol: five in this context\n
Note Create a local binding called number that represents the value 5 using the let
function. Increment the number, then print out the value of number.
(let [number 5]\n (inc number)\n (str \"The number is still \" number))\n
So the value that any local binding points to is immutable too.
"},{"location":"thinking-functionally/immutable-values/","title":"Immutable values","text":"Fixme work in progress
Values in Clojure include numbers, characters and strings. When you use functions on these values they do not change, instead a new value is returned.
Lets look at a simple example with a number:
(def two-little-ducks 22)\n\n(inc two-little-ducks)\n;; => 23\n\ntwo-little-ducks\n;; => 22\n
Another example with a string:
(def message \"Strings are immutable\")\n\n(str message \",\" \" \" \"you cant change them\")\n;; => \"Strings are immutable, you cant change them\"\n\nmessage\n;; => \"Strings are immutable\"\n
Fixme Add an exercise
"},{"location":"thinking-functionally/impure-functions/","title":"Impure functions","text":"We have seen some simple examples of pure functions, so lets see impure functions as a comparison.
(def global-value '(5 4 3 2 1))\n\n(defn impure-increment-numbers [number-collection]\n (map inc global-value))\n\n(impure-increment-numbers '(1 2 3 4 5))\n
The above function is using a global value rather than the argument passed makes this function deterministic
"},{"location":"thinking-functionally/impure-functions/#side-effect-printing-to-the-console-log","title":"Side Effect: Printing to the console log","text":"Although the following example is probably quite harmless, it is a simple example of a function effecting the state of something outside. These side effects should be avoided where possible to keep your code simpler to reason about.
(defn print-to-console [value-to-print]\n (println \"The value is:\" value-to-print))\n\n(print-to-console \"a side effect\")\n
"},{"location":"thinking-functionally/impure-functions/#side-causes-calling-libraries","title":"Side Causes: Calling libraries","text":"To demonstrate a side causes form of impure functions, lets create a task-comple function that marks a current task complete using the current timestamp.
(defn task-complete [task-name]\n (str \"Setting task \" task-name \" completed on \" (js/Date)))\n\n(task-complete \"hack clojure\")\n
(:import java.util.Date)\n\n(defn task-complete [task-name]\n (str \"Setting task \" task-name \" completed on \" (java.util.Date.)))\n\n(task-complete \"hack clojure\")\n
In this example we have called to the outside world to generate a value for us. The above example is fairly simple, however by calling the outside world rather than passing in a date it makes the functions purpose far less clear.
"},{"location":"thinking-functionally/impure-functions/#hint-the-function-javautildate-is-actually-a-call-to-instantiate-a-javautildate-object-the-full-stop-character-at-the-end-of-the-name-makes-it-a-function-call-and-is-the-short-form-of-new-javautildate","title":"Hint:: The function (java.util.Date.)
is actually a call to instantiate a java.util.Date object. The full-stop character at the end of the name makes it a function call and is the short form of (new java.util.Date)
","text":""},{"location":"thinking-functionally/impure-functions/#re-write-as-a-pure-function","title":"Re-write as a pure function","text":"Change the task-complete function definition and function call to take both the task-name and completed-date as arguments.
(defn task-complete [task-name completed-date]\n (str \"Setting task \" task-name \" completed on \" completed-date))\n\n(task-complete \"hack clojure\" (js/Date))\n
Required values should be generated outside a function where possible. In this case in the (js/Date)
function is first evaluated and replaced by its value, then that date value is passed to the function as an argument, keeping the function pure.
The pure version of the function in Clojure, using the java.util.Date function.
(:import java.util.Date)\n\n(defn task-complete [task-name completed-date]\n (str \"Setting task \" task-name \" completed on \" completed-date))\n\n(task-complete \"hack clojure\" (java.util.Date.))\n
"},{"location":"thinking-functionally/iterate-over-values/","title":"iterate Over Values","text":"This
- loop recur
- reducing functions
- map apply reduce
- partition group-by sort-by
"},{"location":"thinking-functionally/iterate-over-values/#hintwork-in-progress","title":"Hint::Work in progress","text":""},{"location":"thinking-functionally/iterate-over-values/#loop-recur","title":"loop recur","text":"loop recur is a very detailed way of defining a way to iterate over values. map, reduce and apply are commonly used abstractions for iterating over values. They simplify the code (once you are comfortable with them)
Functions that iterate over values usually treat a string as a sequence of characters.
"},{"location":"thinking-functionally/lazy-evaluation/","title":"Lazy Evaluation","text":"Fixme work in progress
In the most basic way possible, laziness is the ability to evaluate an expression only when it's actually needed. Taken further, laziness is also evaluating an expression only to the extent required.
"},{"location":"thinking-functionally/lazy-evaluation/#laziness-in-definition","title":"Laziness in definition","text":""},{"location":"thinking-functionally/lazy-evaluation/#laziness-in-evaluation","title":"Laziness in evaluation","text":""},{"location":"thinking-functionally/lazy-evaluation/#laziness-in-partial-evaluation","title":"Laziness in partial evaluation","text":"Clojure is not entirely lazy, only the majority of sequence operations like map, reduce, filter or repeatedly are lazy evaluated.
The most common use of laziness are infinite lists or streams. For example, we could define a list of all prime numbers. In case you didn't know, that's a lot of prime numbers (infinitely many).
If we would define such list in a language like C++ or Python then the language would try to calculate all prime numbers immediately, which would run literally forever.
If we define such list in Haskell or Clojure, then nothing is calculated just yet. As a matter of fact we could happily print out the first 1000 prime numbers from that list without running into a problem. Again, because lazy evaluation only calculates what is really needed, and nothing more.
"},{"location":"thinking-functionally/lazy-evaluation/#laziness-in-number-calculation-ratio-type","title":"Laziness in number calculation - Ratio type","text":"Dividing an integer value by another results in a Ratio type if the result would otherwise result in a decimal number. Clojure only partially evaluates this expression.
(/ 22 7)\n
We can also just express a value as a ratio. This works because of the prefix notation of Clojure
22/7\n
The laziness can be overridden by specifying a precision, eg coercing the result into a specific type
(/ 22 7.0)\n(double (/ 22 7))\n(float (/ 22 7))\n
"},{"location":"thinking-functionally/lazy-evaluation/#making-something-lazy","title":"Making something lazy","text":"The range
function returns a sequence of numbers limited by any arguments given when calling the range function.
Calling the range function without arguments will force an infinite sequence of numbers to be generated, quickly resulting in an out of memory error in the heap.
Instead, we can either pass arguments to the range function that limit the sequence size or wrap the range function in another function
(take 7 (range))\n
The take
function defines how much of a sequence that range
should generate. So we can call range without arguments and it will generate only those numbers in the sequence as specified by take
.
"},{"location":"thinking-functionally/lazy-evaluation/#references","title":"References","text":" - Being lazy in Clojure - lazily generating monsters
"},{"location":"thinking-functionally/list-comprehension/","title":"List Comprehension","text":"In general terms, list comprehensions should:
- be distinct from (nested) for loops and the use of map & filter functions within the syntax of the language.
- return either a list or an iterator (an iterating being something that returns successive members of a collection, in order),
In Clojure, list comprehension is via the for
function. This is different to the for in other languages as you will see.
(for [number [1 2 3]] (* number 2))\n
The for
function should be read as follows:
\"for each number in the collection [1 2 3], apply the function (* number 2)\"
Couldn't we just do this with map? Yes, we could.
(map #(* % 2) [1 2 3])\n
So why do we need for
function? It really shows its value when you are working with multiple collections
(for [number [1 2 3]\n letter [:a :b :c]]\n (str number letter))\n
Again we could use map
function for this as follows
(mapcat (fn [number] (map (fn [letter] (str number letter)))))\n
So with the for
function we can do the same calculation with much easier code to reason about.
"},{"location":"thinking-functionally/list-comprehension/#filtering-results-with-predicates","title":"Filtering results with predicates","text":"With the for
function we can add a filter on the results by using a predicate, to test if a condition is true or false. Any values that meet the condition as true are returned, values that are false are omitted.
(for [x (range 10) :when (odd? x)] x)\n\n(for [x (range 10) :while (even? x)] x)\n
To do this kind of filtering with maps would be possible, however the code would be harder for humans to parse and understand.
Note Create a 3-tumbler combination padlock, with each tumbler having a range of 0 to 9. Count the number of possible combinations. Then add a predicate that filters out some of the combinations
Lets just model all the possible combinations
(for [tumbler-1 (range 10)\n tumbler-2 (range 10)\n tumbler-3 (range 10)]\n [tumbler-1 tumbler-2 tumbler-3])\n
Now lets count the combinations
(count (for [tumbler-1 (range 10)\n tumbler-2 (range 10)\n tumbler-3 (range 10)]\n [tumbler-1 tumbler-2 tumbler-3]))\n
Now add a predicate using :when
to filter out the combinations that do not match.
(count (for [tumbler-1 (range 10)\n tumbler-2 (range 10)\n tumbler-3 (range 10)\n :when (or (= tumbler-1 tumbler-2)\n (= tumbler-2 tumbler-3)\n (= tumbler-3 tumbler-1))]\n [tumbler-1 tumbler-2 tumbler-3]))\n
Note Create a 2 character prefix for tickets, using capital letters from the English alphabet. However, exclude I and O as they can be mistaken for numbers
Lets just model all the possible combinations
(for [letter-1 capital-letters\n letter-2 capital-letters\n :when (and (not (blacklisted letter-1))\n (not (blacklisted letter-2)))]\n (str letter-1 letter-2))\n
"},{"location":"thinking-functionally/managing-state-changes/","title":"Managing state changes","text":""},{"location":"thinking-functionally/map-with-partial/","title":"map with partial","text":"Lets look at different ways we can map functions over collections with partial
We can map over a collection of words and increment them by writing an anonymous function.
(map (fn [animal] (str animal \"s\")) [\"pig\" \"cow\" \"goat\" \"cat\" \"dog\" \"rabbit\"])\n
The anonymous function has a terse form, that removes the boiler plate function definition (fn [])
, allowing definition of only the body of a function.
%
represents a single argument passed to the function. The %
syntax also supports numbers where there are multiple arguments, e.g. %1
, %2
for the first and second arguments. %&
represents all other arguments and is the same as (fn [& args])
or (fn [arg1 & args])
.
The #
character tells the Clojure reader that this is the macro form of a function definition and expands the code to the full form before executing.
(map #(str % \"s\") [\"pig\" \"cow\" \"goat\" \"cat\" \"dog\" \"rabbit\"])\n
"},{"location":"thinking-functionally/map-with-partial/#hintwhen-to-use-the-terse-form-of-anonymous-function","title":"Hint::When to use the terse form of anonymous function","text":"The terse form is often used with higher order functions, as an argument to a function. If the body of the function is simple to comprehend, then the terse form of anonymous function definition is appropriate. When the body of a function is more complex, then consider using a separate defn
function definition.
"},{"location":"thinking-functionally/map-with-partial/#returning-a-vector-instead-of-a-sequence","title":"Returning a Vector instead of a sequence","text":"The map
function returns a lazy sequence. This is very useful for large data sets.
mapv
is an eager version of map that returns the result as a vector. This is useful when you require random access lookup in real time. mapv
can also be used to return an eager result if laziness is not required.
(mapv #(str % \"s\") [\"pig\" \"cow\" \"goat\" \"cat\" \"dog\" \"rabbit\"])\n
"},{"location":"thinking-functionally/map-with-partial/#hintlists-and-vectors-does-it-matter","title":"Hint::Lists and vectors - does it matter?","text":"Some functions in clojure.core
will return a sequence using the list syntax, even if the arguments given are vectors. Most of the time this is not important, as Clojure considers values rather than constructs for most of its functions. For example, (= (\"pig\" \"cow\" \"goat\" \"cat\" \"dog\" \"rabbit\") [\"pig\" \"cow\" \"goat\" \"cat\" \"dog\" \"rabbit\"])
is true as the values are compared rather than the type of container (list, vector)
"},{"location":"thinking-functionally/map-with-partial/#using-conditionals","title":"Using conditionals","text":"Adding sheep as an element raises a problem, as the plural of sheep is sheep.
Using a conditional, a test can be added to determine if a name should be made plural
First lets abstract out the anonymous function to a shared function using defn
(defn pluralise\n \"Pluralise a given string value\"\n [animal]\n (str string \"s\"))\n
def
will bind a name to our collection of animals
(def animals [\"pig\" \"cow\" \"goat\" \"cat\" \"dog\" \"rabbit\"])\n\n(map pluralise animals)\n
The if
function included a conditional test. If that test is true the next expression is evaluated. If the test is false, the second expression is evaluated.
(defn pluralise\n \"Pluralise a given string value\"\n [animal]\n (if (= animal \"sheep\")\n animal\n (str animal \"s\")))\n\n(map pluralise animals)\n
There are several animals that do not have a plural form. Rather than make a complicated test, a collection of animals that are not plural can be defined.
(def non-plural-words [\"deer\" \"sheep\" \"shrimp\" ])\n\n(defn pluralise\n \"Pluralise a given string value\"\n [animal]\n (if (some #{animal} non-plural-words)\n animal\n (str animal \"s\")))\n\n(def animals [\"pig\" \"cow\" \"goat\" \"cat\" \"dog\" \"rabbit\" \"sheep\" \"shrimp\" \"deer\"])\n\n(map pluralise animals)\n
To keep the function pure, we should pass the non-plural-words as an argument
(defn pluralise\n \"Pluralise a given string value\"\n [animal non-plural-words]\n (if (some #{animal} non-plural-words)\n animal\n (str animal \"s\")))\n
Using the terse form of the anonymous function, #()
, call the pluralise function with two arguments. map
will replace the %
character with an element from the animals collection for each element in the collection.
(map #(pluralise % non-plural-words) animals)\n
The partial
function can be used instead of creating an anonymous function, removing the need for more custom code. The order of the arguments must be swapped for partial
to call pluralise
correctly
(defn pluralise\n \"Pluralise a given string value\"\n [non-plural-words animal]\n (if (some #{animal} non-plural-words)\n animal\n (str animal \"s\")))\n
Now we can call pluralise by wrapping it as a partial function.
The argument that is the non-plural-words is constant, its the individual elements of animals I want to get out via map. So when map runs it gets an element from the animals collection and adds it to the call to pluralise, along with non-plural-words
(map (partial pluralise non-plural-words) animals)\n
Using partial here is like calling (pluralise non-plural-words ,,,)
but each time including an element from animals where the ,,,
is.
"},{"location":"thinking-functionally/map-with-partial/#learning-at-the-repl","title":"Learning at the REPL","text":"At first I was getting incorrect output, [\"deer\" \"sheep\" \"shrimp\"]
, then I realised that it was returning the non-plural-words instead of pluralised animals. The arguments from the partial function were being sent in the wrong order. So I simply changed the order in the pluralise function and it worked.
I checked this by adding some old-fashioned print statement.
(defn pluralise-wrong-argument-order\n \"Pluralise a given string value\"\n [animal non-plural-words ]\n (if (some #{animal} non-plural-words)\n (do\n (println (str animal \" its true\"))\n animal)\n (do\n (println (str animal \" its false\"))\n (str animal \"s\"))))\n
"},{"location":"thinking-functionally/partial-functions/","title":"Currying & Partial Functions","text":"Clojure does not support automatic currying, (+3) would result in applying + to 3, resulting with number 3 instead of a function that adds 3 as in Haskell. Therefore, in Clojure we use partial that enables the equivalent behavior.
(defn sum\n \"Sum two numbers together\"\n [number1 number2]\n (+ number1 number2))\n\n(sum 1 2)\n;; => 3\n
If you try and evaluate sum
with a single value then you get an arity exception
(sum 1)\n;; => clojure.lang.ArityException\n;; => Wrong number of args (1) passed to: functional-concepts/sum\n
If we did need to call sum with fewer than the required arguments, for example if we are mapping sum over a vector, then we can use partial to help us call the sum function with the right number of arguments.
Lets add the value 2 to each element in our collection
(map (partial sum 2) [1 3 5 7 9])\n
"},{"location":"thinking-functionally/partial-functions/#using-functions-on-more-arguments-than-they-can-normally-take","title":"Using functions on more arguments than they can normally take","text":"The reduce
function can only work on a single collection as an argument (or a value and a collection), so an error occurs if you wish to reduce over multiple collections.
(reduce + [1 2 3 4])\n;; => 10\n\n(reduce + [1 2 3 4] [5 6 7 8])\n;; returns an error due to invalid arguments\n
However, by using partial we can take one collection at once and return the result of reduce on each of those collections.
(map (partial reduce +) [[1 2 3 4] [5 6 7 8]])\n
In the above example we map the partial reduce function over each element of the vector, each element being a collection.
"},{"location":"thinking-functionally/partial-functions/#using-partial-to-set-a-default-value","title":"Using partial to set a default value","text":"We can use the partial function to create a default message that can be just given just the custom part. For example, if we want to have a default welcome message but include a custom part to the message at the end.
First we would define a function that combines parts of the message together.
(defn join-strings\n \"join one or more strings\"\n [& args]\n (apply str args))\n
The [& args] argument string says take all the arguments passed and refer to them by the name args. Its the & character that has the semantic meaning, so any name after the & can be used, although args is common if there is no domain specific context involved.
We can simply call this function with all the words of the message.
(join-strings \"Hello\" \" \" \"Clojure\" \" \" \"world\")\n;; \u21d2 \"Hello Clojure world\"\n
Now we define a name called wrap-message
that can be used to wrap the start of our message. This name binds to a partial function call to join-strings
which send that function the default message and any custom message you add when evaluate wrap-message
(def wrap-message (partial join-strings \"Hello Clojurians in \"))\n\n(wrap-message)\n;; \u21d2 \"Hello Clojurians in \"\n\n(wrap-message \"London\")\n ;; => \"Hello Clojurians in London\"\n
"},{"location":"thinking-functionally/partial-functions/#currying-in-clojure","title":"Currying in clojure","text":"Currying is the process of taking some function that accepts multiple arguments, and turning it into a sequence of functions, each accepting a single argument. Or put another way, to transform a function with multiple arguments into a chain of single-argument functions.
Currying relies on having fixed argument sizes, whereas Clojure gets a lot of flexibility from variable argument lengths (variable arity).
Clojure therefore has the partial function gives results similar to currying, however the partial
function also works with variable functions.
partial
refers to supplying some number of arguments to a function, and getting back a new function that takes the rest of the arguments and returns the final result
One advantage of partial
is to avoid having to write your own anonymous functions
"},{"location":"thinking-functionally/partial-functions/#useful-references","title":"Useful references","text":" - Partial function applications for humans
"},{"location":"thinking-functionally/pattern-matching/","title":"Pattern matching","text":"Fixme work in progress
"},{"location":"thinking-functionally/pattern-matching/#regular-expression","title":"Regular Expression","text":""},{"location":"thinking-functionally/pattern-matching/#destructuring","title":"Destructuring","text":""},{"location":"thinking-functionally/persistent-data-structures/","title":"Persistent data structures","text":""},{"location":"thinking-functionally/polymorphism/","title":"Polymorphic function definitions","text":"Polymorphic means many forms.
The simplest example of polymorphism in Clojure is a function definition that acts differently based on the number of arguments passed.
Usually you define a function with one set of arguments, either none []
, one [one]
or many [any number of args]
, using the basic syntax
(defn name\n\"I am the doc string to describe the function\"\n [args]\n (str \"define your behaviour here\"))\n
Instead of writing multiple functions with the same name that each take different numbers of arguments, you can use the following polymorphic syntax in Clojure
(defn name\n \"I am the doc string to describe the function\"\n ([]\n (str \"behaviour with no args\"))\n ([one]\n (str \"behaviour with one arg\"))\n ([one two & args]\n (str \"behaviour with multiple args\")))\n
Note Write a simple function called i-am-polly
that returns a default message when given no arguments and a custom message when given a custom string as an argument
(defn i-am-polly\n ([] (i-am-polly \"My name is polly\"))\n ([message] (str message)))\n\n(i-am-polly)\n(i-am-polly \"I call different behaviour depending on arguments sent\")\n
"},{"location":"thinking-functionally/pure-functions/","title":"Pure functions","text":"A function is considered pure if does not side effects or is affected by side causes. A pure function does not change any other part of the system and is not affected by any other part of the system.
When you pass arguments to a function and that function returns a value without interacting with any other part of the system, then that function is considered pure.
Should something from outside a function be allowed to affect the result of evaluating a function, or if that function be allowed to affect the outside world, then its an impure function.
So lets look at a simple code example
(defn add-numbers [number1 number2]\n (+ number1 number2))\n\n(add-numbers 1 2)\n
Lets look at each line of this suggested answer
;; function takes 2 arguments\n;; function uses both arguments for result\n(defn add-numbers [number1 number2]\n (+ number1 number2))\n\n;; specific values are passed as arguments\n(add-numbers 1 2)\n
"},{"location":"thinking-functionally/pure-functions/#notewrite-a-pure-function-that-adds-two-numbers-together","title":"Note::Write a pure function that adds two numbers together ?","text":""},{"location":"thinking-functionally/pure-functions/#an-example-with-map","title":"An example with map","text":"Note Define a collection called numbers and write a named function that increments each number of the numbers collection. Is your function pure or impure ?
(def numbers '(5 4 3 2 1))\n\n(defn increment-numbers []\n (map inc numbers))\n\n(increment-numbers)\n
The function takes no arguments and is pulling in a value from outside the function. This is a trivial example, but if all your code is like this it would be more complex. If the value pointed to by numbers
is mutable and changes before the increment-numbers
function is called then you will get different results.
Here is a Pure function example
(def numbers '(5 4 3 2 1))\n\n(defn increment-numbers [number-collection]\n (map inc number-collection))\n\n(increment-numbers numbers)\n
In this example we are explicitly passing the numbers
collection to the function. The function works on passed value and returns a predictable result.
"},{"location":"thinking-functionally/recursion-polymorphism/","title":"Recursion & Polymorphism","text":"Fixme work in progress
The following sum
function will calculate the value of adding all the elements in a collection. You can alter the results by adding a starting value to the calculation as a second argument when calling sum
(defn sum\n ([vals] (sum vals 0))\n ([vals accumulating-total]\n (if (empty? vals)\n accumulating-total\n (sum (rest vals) (+ (first vals) accumulating-total)))))\n\n(sum [2 7 9 11 13])\n(sum [1])\n(sum [2 7 9 11 13] 9)\n
Rather than duplicate the calculation, the behaviour of calling sum
with just a collection simply calls sum
again, this time passing a starting value of zero.
"},{"location":"thinking-functionally/recursion/","title":"Recursion","text":"Fixme work in progress
Recursion is used greatly in Clojure to iterate through data and as anything can be treated as data in Clojure you can understand why.
The constructs available in Clojure for recursion include
loop
and recur
- Named function that calls itself
map
, reduce
, filter
, remove
, etc. for
"},{"location":"thinking-functionally/recursion/#recursively-calling-the-same-function","title":"Recursively calling the same function","text":"Lets iterate though a collection using recursion by writing a function that calls itself
(defn recursively-use-a-collection [collection]\n (println (first collection))\n (if (empty? collection)\n (print-str \"no more values to process\")\n (recursively-use-a-collection (rest collection))))\n\n(recursively-use-a-collection [1 2 3])\n
Lets take this recursive approach to create a function that can tell us the length of a collection (list or vector)
We define a function that takes a collection of an argument. The collection is tested to see if it is empty and if so a zero value is returned. If the collection is not empty, then we
(defn length [collection]\n (if (empty? collection)\n 0\n (+ 1 (length (rest collection)))))\n;; => #'clojure-through-code.01-basics/length\n
If we call the length
function with an empty collection, then the empty?
condition will return true and the if
expression will evaluate the first expression, 0, returning 0.
(length [])\n;; => 0\n
If we call the length
function with a collection containing 3 values, then the empty?
function will return false
and the if
function will evaluate the second expression.
The second expression starts with a simple counter, using the +
function and the value one
(length [0 1 2])\n;; => 3\n
(+ 1 (length [1 2]))\n(+ 1 (+ 1 (length [2])))\n(+ 1 (+ 1 (+ 1 (length []))))\n(+ 1 (+ 1 (+ 1 0)))\n\n(length (range 24))\n;; => 24\n
(defn length [collection] (kk))
"},{"location":"thinking-functionally/recursion/#further-recursion-examples","title":"Further recursion examples","text":"Other functions to consider
- every
- accumulating / accumulative
- keep
"},{"location":"thinking-functionally/sequence-abstractions/","title":"Sequence abstraction","text":"Fixme work in progress
(first '(1 2 3 4 5))\n(rest '(1 2 3 4 5))\n(last '(1 2 3 4 5))\n
(defn nth [items n]\n (if (= n 0)\n (first items)\n (recur (rest items) (- n 1))))\n\n(define squares '(0 1 4 9 16 25))\n\n(nth squares 3)\n
"},{"location":"thinking-functionally/sequences/","title":"Sequences","text":"Fixme work in progress
Data structures can be built by combining functions
(cons 1 (cons 2 (cons 3 (cons 4 nil))))\n
(->>\n nil\n (cons 4)\n (cons 3)\n (cons 2)\n (cons 1))\n
"},{"location":"thinking-functionally/side-effects/","title":"Side effects","text":"A side effect is something that creates a change outside of the current code scope, or something external that affects the behaviour or result of executing the code in the current scope.
"},{"location":"thinking-functionally/side-effects/#nondeterministic-the-complexity-iceberg","title":"Nondeterministic - the complexity iceberg","text":"When you have side effects, you cannot reason accurately about a piece of the code.
In order to understand a piece of code you must look at all possible side effects created in all lines of code to ensure you fully understand the result of executing your code.
With side effects in your system, complexity is hidden, causing a far greater risk of a dangerous situation.
"},{"location":"thinking-functionally/side-effects/#side-causes-side-effects","title":"Side causes - side effects","text":"You can think about these effects is in two specific areas, Side Causes and Side Effects
"},{"location":"thinking-functionally/side-effects/#hintside-causes-term","title":"Hint::Side Causes term","text":"The term of side causes was coined by Kris Jenkins in the superb article What is Functional Programming?
"},{"location":"thinking-functionally/tail-recursion/","title":"Tail recursion","text":"Fixme work in progress
If we generate a very large collection we run the risk of blowing our heap space. For example we could use range to generate a very large collection, say a vector containing 10 billion values
Don't try this example below
(vec (range 0 9999999999))\n;; this will crash after a short while as it will use up all your heap space\n
Using tail call optimisation (tail recursion) allows us to reuse a memory location when we call a function recursively. This tail recursion is not part of the underlying Java Virtual Machine (JVM), so instead Clojure has a specific function called recur
The recur
function allows the processing of a very large data set without blowing the heap space because the memory space will be reused.
The recur
function must be the last expression in order to work.
(defn sum\n ([vals] (sum vals 0))\n ([vals accumulating-total]\n (if (empty? vals)\n accumulating-total\n (recur (rest vals) (+ (first vals) accumulating-total)))))\n\n(sum (vec (range 0 9999999)))\n
"},{"location":"thinking-functionally/threading-macros/","title":"Threading macros","text":"The thread-first ->
and thread-last ->>
macros allow Clojure code to be written in a more sequential style and with a more terse syntax. This can sometimes make code easier to understand by humans.
Using the thread-first macro, ->
, the result of the first evaluation is passed as the first argument to the next function and so on.
```clojure (-> (clojure.string/lower-case \"HELLO\") (str \", Clojure world\"))
The value hello is converted to lower case and that result is passed as the first argument to the next function. The string function is then evaluated with this new argument and the final \"hello, Clojure world\" string is returned as the result.\n\n The thread-last macro `->>` passes the result of the first evaluation as the **last argument** to the next expression.\n\n```clojure\n(->> \" this\"\n (str \" is\")\n (str \" backwards\"))\n
"},{"location":"thinking-functionally/threading-macros/#hintparens-optional","title":"Hint::Parens optional","text":"function calls that only take one argument, the one passed by earlier expressions, can be included in the threading macro code without the surrounding ()
parens.
"},{"location":"thinking-functionally/threading-macros/#reading-clojure-code","title":"Reading Clojure code","text":"To read Clojure it is common to start from the inside out, as this is how the Clojure reader also works. This style is inherited from Lisp of which Clojure is an implementation.
The following code is written in classic Lisp style.
(reverse\n (sort-by val (frequencies\n (remove common-english-words\n (map #(clojure.string/lower-case %)\n (re-seq #\"[a-zA-Z0-9|']+\"\n (slurp book.txt)))))))\n
Reading inside out:
- slurp in the contents of the book.txt file, converting it to a string.
- use a regular expression to create a new sequence where the book is a sequence of individual strings for each word.
- convert each string in the sequence by mapping the lower-case function over each element of the sequence.
- remove common english words such as the and and from the sequence.
- count how many times each word occurs and pair the string with its frequency in the book.
- reverse the order of the sequence by the value of frequency, so the most used word is at the start of the sequence.
This function uses the var common-english-words
which is defined as:
(def (set\n (clojure.string/split (slurp \"common-english-words.txt\") #\",\" )))\n
This takes a comma separated file of words and splits them. The words are put into a set so only one instance of each word is included.
"},{"location":"thinking-functionally/threading-macros/#rewriting-the-code-with-threading-macros","title":"Rewriting the code with threading macros","text":"(->> (slurp book.txt)\n (re-seq #\"[a-zA-Z0-9|']+\" ,,,)\n (map #(clojure.string/lower-case %))\n (remove common-english-words)\n frequencies\n (sort-by val)\n reverse)\n
frequencies
and reverse
only take one argument, so they do not require surrounding ()
inside the threading macro.
The common-english-words var is fairly easy to read, so probably doesn't need to be written using a threading macro, however, for completeness here is a thread-first example.
(def common-english-words\n (-> (slurp \"common-english-words.txt\")\n (clojure.string/split #\",\")\n set))\n
"},{"location":"thinking-functionally/threading-macros/#hintmacroexpand","title":"Hint::Macroexpand","text":"use macroexpand-1
around the threading macro code to show resulting lisp style Clojure code. Clojure editors also provide evaluation functions that will macroexpand.
"},{"location":"thinking-functionally/transducers/","title":"Transducers","text":"Transducers provide an efficient way to transform values from a collection or stream of data, eg. core.async channel, java.jdbc database query (0.7.0 upward) or a network stream etc.
Transformations are applied directly to a stream of data without first creating a collection.
Multiple transducers are composed into a single transforming function, walking the data elements only once and without the need for intermediate collections.
One Transducer benefit is to allow the design to switch from a seq-based implementation to a core.async implementation using channels
Transducers: Clojure.org Transducer use cases
Basics Transducer walkthrough
Simple examples of Clojure Transducers
"},{"location":"thinking-functionally/transducers/#evolving-design","title":"Evolving design","text":"Each element of the data collection is acted upon by a composition of each transducer in an expression, making them more efficient than a applying one expression after another (e.g. as with a thread macro or composed list expressions).
Transducers provide benefits like lazy evaluation and alternative performance trade-offs.
Nested calls
(reduce + (filter odd? (map #(+ 2 %) (range 0 10))))\n
Functional composition
(def xform\n (comp\n (partial filter odd?)\n (partial map #(+ 2 %))))\n (reduce + (xform (range 0 10)))\n
Thread macro
(defn xform [xs]\n (->> xs\n (map #(+ 2 %))\n (filter odd?)))\n (reduce + (xform (range 0 10)))\n
Transducer
(def xform\n (comp\n (map #(+ 2 %))\n (filter odd?)))\n(transduce xform + (range 0 10))\n
The order of combinators in a transducer is the same as a thread macro (natural order).
"},{"location":"thinking-functionally/transducers/#coreasync","title":"core.async","text":"Transducers were introduced into the Clojure language to provide a common abstraction to avoid re-implmentation of clojure.core transformation functions specific to core.async.
in a large part to support processing data to and from a core.async channel.
The Clojure maintainers discovered they were re-implementing filter, map, partition, etc. on core.async and realized it would be better to have an abstraction for the transforms independent of the source and destination of data.
The xform can be used with an core.async channel
Transducer with core.async
(chan 1 xform)\n
an optional arg when creating a channel, causing a transform to be applied to all data that goes through the channel
"},{"location":"thinking-functionally/transducers/#database-queries","title":"Database queries","text":"Typical database access involves passing in functions to transform rows and process the transformed result set
java.jdbc
0.7.0-beta1 onwards can also apply transducers to \u201creducible queries\u201d (and reducible result sets).
Create a reducible query which is passed to transforming function, e.g. reducers, transducers, plain ol\u2019 reduce
and into
etc
"},{"location":"thinking-functionally/transducers/#simplified-description","title":"Simplified description","text":"Transducers are recipes what to do with a sequence of data without knowledge what the underlying sequence is (how to do it). It can be any seq, async channel or maybe observable.
They are composable and polymorphic.
The benefit is, you don't have to implement all standard combinators every time new data source is added. Again and again. As resulting effect you as user are able to reuse those recipes on different data sources.
https://stackoverflow.com/questions/26317325/can-someone-explain-clojure-transducers-to-me-in-simple-terms
Work In Progress, sorry A very messy brain dump of ideas to tidy up. Pull requests are welcome
Clojure.core functions such as map and filter are lazy, however they also generate containers for lazy values when chained together.
Without holding onto the head of the collection, large lazy sequences aren't created but intermediate abstractions are still created for each lazy element.
"},{"location":"thinking-functionally/transducers/#reducing-functions","title":"Reducing functions","text":"Reducing functions are those that take two arguments:
- a result so far
- an input.
The reducing function returns a new result (so far).
For example +: With two arguments, you can think of the first as the result so far and the second as the input.
A transducer could now take the + function and make it a twice-plus function (doubles every input before adding it). This is how that transducer would look like (in most basic terms):
Reducing function
(defn double\n [rfn]\n (fn [r i]\n (rfn r (* 2 i))))\n
For illustration substitute rfn with + to see how + is transformed into twice-plus:
Reducing function - twice plus
(def twice-plus ;; result of (double +)\n (fn [r i]\n (+ r (* 2 i))))\n\n(twice-plus 1 2) ;-> 5\n(= (twice-plus 1 2) ((double +) 1 2)) ;-> true\n
Calling the reducing function
(reduce (double +) 0 [1 2 3])\n\n;; => 12\n
Reducing functions returned by transducers are independent of how the result is accumulated because they accumulate with the reducing function passed to them.
conj
takes a collection and a value and returns a new collection with that value appended.
(reduce (double conj) [] [1 2 3])\n;; => [2 4 6]\n
Transducers are independent of the kind of data is passed.
optimisation goes beyond eliminating intermediate streams; it is possible to perform operations in parallel.
"},{"location":"thinking-functionally/transducers/#pmap","title":"pmap","text":"Use pmap
when mapping an expensive function over a sequence, making the operation parallel. No other changes to the code are required.
Transducers will still be faster if the pmap function creates intermedicate data structures.
A transducer clear definition is here:
Transducers are a powerful and composable way to build algorithmic transformations that you can reuse in many contexts.
"},{"location":"thinking-functionally/transducers/#reducing-functions_1","title":"Reducing functions","text":"Population a local Village
The
(def village\n [{:home :north :family \"smith\" :name \"sue\" :age 37 :sex :f :role :parent}\n {:home :north :family \"smith\" :name \"stan\" :age 35 :sex :m :role :parent}\n {:home :north :family \"smith\" :name \"simon\" :age 7 :sex :m :role :child}\n {:home :north :family \"smith\" :name \"sadie\" :age 5 :sex :f :role :child}\n\n {:home :south :family \"jones\" :name \"jill\" :age 45 :sex :f :role :parent}\n {:home :south :family \"jones\" :name \"jeff\" :age 45 :sex :m :role :parent}\n {:home :south :family \"jones\" :name \"jackie\" :age 19 :sex :f :role :child}\n {:home :south :family \"jones\" :name \"jason\" :age 16 :sex :f :role :child}\n {:home :south :family \"jones\" :name \"june\" :age 14 :sex :f :role :child}\n\n {:home :west :family \"brown\" :name \"billie\" :age 55 :sex :f :role :parent}\n {:home :west :family \"brown\" :name \"brian\" :age 23 :sex :m :role :child}\n {:home :west :family \"brown\" :name \"bettie\" :age 29 :sex :f :role :child}\n\n {:home :east :family \"williams\" :name \"walter\" :age 23 :sex :m :role :parent}\n {:home :east :family \"williams\" :name \"wanda\" :age 3 :sex :f :role :child}])\n
Define a reducing expression to return the number of children in the village.
(def children \n (r/map #(if (= :child (:role %)) 1 0)))\n
Call the expression
(r/reduce + 0 (children village))\n;;=> 8\n
Using a transducer to add up all the mapped values
create the transducers using the new arity for map that takes the function, no collection
(def child-numbers (map #(if (= :child (:role %)) 1 0)))\n
Use transduce (c.f r/reduce) with the transducer to get the answer
(transduce child-numbers + 0 village)\n;;=> 8\n
It is really powerful when taking subgroups in account, e.g to know how many children in the Brown Family
Reducer to count children in Brown family
Create the reducer to select members of the Brown family
(def brown-family-children\n (r/filter #(= \"brown\" (string/lower-case (:family %)))))\n
compose a composite function to select the Brown family and map children to 1
(def count-brown-family-children \n (comp ex1a-map-children-to-value-1 brown-family-children))\n
reduce to add up all the Brown children
(r/reduce + 0 (ex2a-count-brown-family-children village))\n;;=> 2\n
"},{"location":"thinking-functionally/transducers/#references","title":"References","text":"Transducers - Clojure next big idea
"},{"location":"using-data-structures/","title":"Using data structures","text":"Data structures in Clojure are used to model information and data, within a particular namespace. Functions are used to run behaviour over the data structures.
Lets look at some of the common functions that are used in Clojure with data structures
fixme the below content is work in progress, sorry.
"},{"location":"using-data-structures/#managing-return-values","title":"Managing Return values","text":"If you run a function over a data structure, you may not always get back the type of value you want. It easy to wrap a function around to give you the desired value type.
Note Use the str
function to get a string from person, rather than a set of characters
(first person)\n(rest person)\n\n(str (first person))\n\n;; How do we return the rest of the string as a string ?\n(str (rest person))\n(map str (rest person))\n(str (map str (rest person)))\n(apply str (rest person))\n
You can get the value of this map
(def luke {:name \"Luke Skywalker\" :skill \"Targeting Swamp Rats\"})\n(def darth {:name \"Darth Vader\" :skill \"Crank phone calls\"})\n(def jarjar {:name \"JarJar Binks\" :skill \"Upsetting a generation of fans\"})\n\n(get luke :skill)\n
"},{"location":"using-data-structures/#immutability","title":"Immutability","text":"When you use functions on data structures, although they can return a new value they do not change the original data structure.
Lets define a name for a data structure
(def name1 [1 2 3 4])\n
when we evaluate that name we get the original data we set
name1\n
Now we use a function called conj to adds (conjoin) another number to our data structure
(conj name1 5)\n
This returns a new value without changing the original data structure
name1\n
We cant change the original data structure, it is immutable. Once it is set it cant be changed. However, if we give a name to the result of changing the original data structure, we can refer to that new data structure
(def name2(conj name1 5))\n
Now name2 is the new data structure, but name1 remains unchanged
name2\nname1\n
So we cannot change the data structure, however we can achieve something that looks like we have changed it. We can re-assign the original name to the result of changing the original data structure
(def name2(conj name1 5))\n
Now name1 and name2 are the same result
name2\nname1\n
An analogy
You have the number 2. If you add 1 to 2, what value is the number 2?
The number 2 is still 2 no mater that you add 1 to it, however, you get the value 3 in return
"},{"location":"using-data-structures/#creating-new-data-structures","title":"Creating new data structures","text":"Use concat to add lists or vectors together
(concat [1 2] '(3 4)) ; => (1 2 3 4)\n
Use filter, map to interact with collections
(map inc [1 2 3]) ; => (2 3 4)\n(filter even? [1 2 3]) ; => (2)\n
Use reduce to reduce them
(reduce + [1 2 3 4])\n; = (+ (+ (+ 1 2) 3) 4)\n; => 10\n
Reduce can take an initial-value argument too
(reduce conj [] '(3 2 1))\n; = (conj (conj (conj [] 3) 2) 1)\n; => [3 2 1]\n
Use cons to add an item to the beginning of a list or vector
(cons 4 [1 2 3]) ; => (4 1 2 3)\n(cons 4 '(1 2 3)) ; => (4 1 2 3)\n
Use conj to add an item to the beginning of a list, or the end of a vector
(conj [1 2 3] 4) ; => [1 2 3 4]\n(conj '(1 2 3) 4) ; => (4 1 2 3)\n
"},{"location":"using-data-structures/applying-functions/","title":"Applying functions to data structures","text":"Applying a functions behaviour to the elements of a data structure
"},{"location":"using-data-structures/destructuring/","title":"Destructuring","text":"Destructuring is a form of pattern matching that is common in Clojure. Destructuring allow you to pull out the specific elements from a collection.
Destructuring is commonly used with the let
method for creating local bindings (locally scoped names).
(let [[a b c & d :as e] [1 2 3 4 5 6 7]]\n [a b c d e])\n\n(let [[[x1 y1][x2 y2]] [[1 2] [3 4]]]\n [x1 y1 x2 y2])\n\n;; with strings\n(let [[a b & c :as str] \"asdjhhfdas\"]\n [a b c str])\n\n;; with maps\n(let [{a :a, b :b, c :c, :as m :or {a 2 b 3}} {:a 5 :c 6}]\n [a b c m])\n
It is often the case that you will want to bind same-named symbols to the map keys. The :keys directive allows you to avoid the redundancy:
(let [{fred :fred ethel :ethel lucy :lucy} m] )\n
This can be written in a shorter form as follows:
(let [{:keys [fred ethel lucy]} m] )\n
As of Clojure 1.6, you can also use prefixed map keys in the map destructuring form:
(let [m {:x/a 1, :y/b 2}\n {:keys [x/a y/b]} m]\n (+ a b))\n
As shown above, in the case of using prefixed keys, the bound symbol name will be the same as the right-hand side of the prefixed key. You can also use auto-resolved keyword forms in the :keys directive:
(let [m {::x 42}\n {:keys [::x]} m]\n x)\n
"},{"location":"using-data-structures/lazy-sequences/","title":"Lazy Sequences","text":"Sequences are an interface for logical lists, which can be lazy. \"Lazy\" means that a sequence can define an infinite series, like so:
(range 4)\n
If you evaluate (range)
just by itself it will return an infinite number of integers, well at least until your computers memory space fills up.
So we dont blow up our memory and just get the values we want we can use range
in conjunction with other functions that define how many numbers we actually want.
For example, if we just wanted the first four numbers from the infinite sequence of range
we could specify that with the take
function
(take 4 (range)) ; (0 1 2 3)\n
Here the range function is being lazy, because it will only generate the first 4 numbers in its sequence.
Clojure (and Lisps in general) often evaluate at the last possible moment, usually when they have been given more specific content.
"},{"location":"using-data-structures/mapping-data-structures/","title":"Mapping functions over data structures","text":"Map allows you to work over one or more data sets, applying the function to each element of each of the data structures.
When the data structures are of equal size, then the same sized data structure is returned.
(map + [1 2 3] [1 2 3])\n;; => (2 4 6)\n
If one data structure is smaller, then the function is only applied up to the last element of the smallest data structure.
(map + [1 2 3] [1 2])\n;; => (2 4)\n\n(map + [1 2 3] [1])\n;; => (2)\n\n(map + [1 2 3] [])\n;; => ()\n\n(map + [1 2 3])\n;; => (1 2 3)\n
Lets look at another example. Here we have a pre-defined Fibonacci sequence up to the first 12 values.
(def fibonacci-sequence [1 2 3 5 8 13 21 34 55 89 144 278])\n
If we just want the first 10 values of the sequence, we can use the take
function.
(take 10 fibonacci-sequence)\n;; => (1 2 3 5 8 13 21 34 55 89)\n
If we want a calculation using the values of the fibonacci-sequence then we can use map
with a function. In this case we are going to generate a range of Integer numbers from 0-9 using the function range
. That range of numbers is then multiplied element by element with the corresponding element in the fibonacci-sequence.
(map * (range 10) fibonacci-sequence)\n;; => (0 2 6 15 32 65 126 238 440 801)\n
So,
- 0 times 1 is 0,
- 1, times 2 is 2,
- 2 times 3 is 6, etc.
If we evaluate the previous expression part by part, its easier to see what is going on. First lets evaluate the fibonacci-sequence
(map * (range 10) [1 2 3 5 8 13 21 34 55 89 144 278])\n;; => (0 2 6 15 32 65 126 238 440 801)\n
Now lets evaluate the (range 10)
function call
(map * (0 1 2 3 4 5 6 7 8 9) [1 2 3 5 8 13 21 34 55 89 144 278])\n;; => (0 2 6 15 32 65 126 238 440 801)\n
We can see the answer is the same, however by evaluating each part of the expression we get an exact view of what is going to happen with the map
function.
"},{"location":"using-data-structures/sequences/","title":"Sequence abstractions","text":"There are functions that work on all the built in data-structures in Clojure.
first
second
rest
cons
"},{"location":"using-data-structures/sequences/#practising-with-lists","title":"Practising with lists","text":"Create a simple collection of developer events. First use a list of strings, then try a map with keywords. For each data structure, pull out some of the event details
(def developer-events-strings '(\"Devoxx UK\" \"Devoxx France\" \"Devoxx\" \"Hack the Tower\"))\n\n(def developer-events-strings2 (list \"Devoxx UK\" \"Devoxx France\" \"Devoxx\" \"Hack the Tower\"))\n\ndeveloper-events-strings\n\n(first developer-events-strings)\n\n(def developer-events-vector\n [:devoxxuk :devoxxfr :devoxx :hackthetower] )\n
Using a Clojure Vector data structure is a more Clojure approach, especially when the vector contains keywords. Think of a Vector as an Array, although in Clojure it is again immutable in the same way a list is.
Create a slightly more involved data structure, holding more data around each developer events. Suggest using a map, with each key being the unique name of the developer event.
The details of each event (the value to go with the event name key) is itself a map as there are several pieces of data associated with each event name.
(def dev-event-details\n {:devoxxuk {:URL \"http://jaxlondon.co.uk\"\n :event-type \"Conference\"\n :number-of-attendees 700\n :call-for-papers true}\n :hackthetower {:URL \"http://hackthetower.co.uk\"\n :event-type \"hackday\"\n :number-of-attendees 60\n :call-for-papers false}})\n
Lets call the data structure and see what it evaluates too, it should not be a surprise
dev-event-details\n
We can ask for the value of a specific key, and just that value is returned
(dev-event-details :devoxxuk)\n
In our example, the value returned from the :devoxxuk key is also a map, so we can ask for a specific part of that map value by again using its key
(:URL (dev-event-details :devoxxuk))\n
Define a simple data structure for stocks data using a vector of maps, as there will be one or more company stocks to track.
Each map represents the stock information for a company.
Get the value of the whole data structure by referring to it by name, ask for a specific element by its position in the array using the nth
function.
Then try some of the common functions that work on collections.
(def portfolio [ { :ticker \"CRM\" :lastTrade 233.12 :open 230.66}\n { :ticker \"AAPL\" :lastTrade 203.25 :open 204.50}\n { :ticker \"MSFT\" :lastTrade 29.12 :open 29.08 }\n { :ticker \"ORCL\" :lastTrade 21.90 :open 21.83 }])\n\nportfolio\n\n(nth portfolio 0)\n\n(nth portfolio 3)\n\n(first portfolio)\n(rest portfolio)\n(last portfolio)\n
First and next are termed as sequence functions in Clojure, unlike other lisps, you can use first and next on other data structures too
"}]}
\ No newline at end of file
+{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Practicalli Clojure","text":"Practicalli Clojure is a hands-on guide to using Clojure throughout all the software development stages. Live coding videos demonstrate the Clojure REPL workflow in action, showing how to get the most out of the unique approach the language provides.
Discover how to make the most of Clojure CLI and community tools, drawing from commercial experiences and community discussions.
Practical code examples are supported by discussions of the concepts behind Clojure, including functional programming, \"pure\" functions and a stateless approach with persistent data structures, changing state safely, Java interoperability and tooling around Clojure.
John Stevenson, Practical.li
Clojure - an elegant language for a more civilised development experience
"},{"location":"#clojure-repl-driven-development","title":"Clojure REPL Driven Development","text":"The Clojure REPL is interactive environment used to run Clojure code in both development and production. The REPL workflow provides an instant feedback loop so individual pieces of code (expressions) can be evaluatived, quickly growing confidence with Clojure and rapidly evolving effective designs.
"},{"location":"#clojure-language","title":"Clojure Language","text":"Clojure programming language has a strong dynamic type system and a simple syntax that is a joy to work with. Immutable values and a pragmatic approach to pure functional programming makes it easier to create simple and highly maintainable systems. A specification library ensures values are of the correct shape, especially valuable when receiving data from outside of Clojure.
Clojure has an open source license and a large number of open source libraries and tools. Simple host interoperability allows a even more libraries to be leveraged.
Adrian Cockcroft - formally Cloud Architect, Netflix
The most productive programmers I know are writing everything in Clojure ... producing ridiculously sophisticated things in a very short time. And that programmer productivity matters.
Clojure REPL Workflow overview Clojure REPL
"},{"location":"#navigate-the-book","title":"Navigate the book","text":"Use the mouse or built-in key bindings to navigate the pages of the book
- P , , : go to previous page
- N , . : go to next page
Use the search box to quickly find a specific topic
- F , S , / : open search dialog
- Down , Up : select next / previous result
- Esc , Tab : close search dialog
- Enter : follow selected result
"},{"location":"#resources","title":"Resources","text":" Practicalli Clojure CLI Config - additional tools via aliases Clojure Aware Editors Practicalli YouTube channel
"},{"location":"#sponsor-practicalli","title":"Sponsor Practicalli","text":"All sponsorship funds are used to support the continued development of Practicalli series of books and videos, although most work is done at personal cost and time.
Thanks to Cognitect, Nubank and a wide range of other sponsors from the Clojure community for your continued support
"},{"location":"#creative-commons-license","title":"Creative commons license","text":"This work is licensed under a Creative Commons Attribution 4.0 ShareAlike License (including images & stylesheets)."},{"location":"core-async/","title":"Core.async","text":""},{"location":"core-async/#discussion-from-the-clojurians-clojure-uk-slack-channel","title":"Discussion from the clojurians clojure-uk slack channel","text":"recommendation / intro tutorial
https://github.com/clojure/core.async/blob/master/examples/walk-through.clj
EuroClojure talk about clojure otp library that built on top of core.async because they felt it was too low level
At the REPL
- if you don\u2019t create channels with non-zero sized buffers, or don\u2019t perform operations in separate threads or go-blocks, then you can end up blocked quite easily
In production be sure to catch exceptions, or at least be logging them. It can be too easy to loose errors (and the processes that threw them)
if you make a note of the nrepl port when you start a repl, you can always connect a second repl to the same process to recover from accidental blocking
"},{"location":"core-async/#coreasync-and-transducers","title":"core.async and transducers","text":"core.async + transducers
"},{"location":"core-async/#manifold-alternative-to-coreasync","title":"Manifold - alternative to core.async","text":"another approach is to use manifold
it\u2019s use of deferred values makes it harder to block the REPL - but obviously the buffering still has to happen somewhere!
manifold
for general async stuff more than core.async
use core.async as a way of chaining together bits of data processing. manifold would be good for that too
put multiple calls to an external API on a channel and have them \u201cdo their thing\u201d in their own time in the background. This seems to be a good use for core.async\u2026 Is Manifold is as good / better?
If processing streams of data then core.async
is fine (as are manifold Stream
s) If calls are better suited to promises then consider manifold Deferred
If you are wanting streams of promises then manifold is a more complete solution (because the manifold stream
and deferred
work easily together
API calls are generally more promise-like than stream-like (unless your result is an SSE stream or websocket etc)
there are promise-chan
s in core.async too though
Manifold could be used to collect a number of remote resources together in a let
which isn't very stream like
manifold has two distinct core abstractions - the deferred
, which is a promise with callbacks and additional machinery, and the stream
which is a umm stream of values, and supports buffering, backpressure etc
First call to the API I will get a total back as part of the result and as there is no paging functionality \u201cbuilt in\u201d on the API in question I will need to take the total and figure out how many more calls I need to make to get the \u201crest\u201d of the data. I was going to do this by throwing the calls onto a channel using core.async\u2026 I am sensing that their promise-y nature would, you feel, be better suited to Manifold Deferred..? a stream or a channel works quite well for that sort of query in my data access lib we actually use a promise of a stream for that sort of access which is an improvement over just a plain stream because it allows easy mixing with other calls which return promises of a value
I was intending to stack up API operations in a channel as a queue so that a) I don\u2019t block execution and b) so that I don\u2019t have to use an actual queue (SQS / rabbitMQ etc) I am starting to think I may not have understood the implications of what I want to do\u2026
i'm not sure - are you just trying to get a stream of records from a paginated api ?
what are you trying to not block the execution of?
The API is not paginated, I need to figure out how many pages there are and stack up the calls.
can you pass an offset or something ?
What I am looking for is a way to stack work up asynchronously in the background so that my call(s) to the external API don\u2019t lock up the whole app / program for minutes at a time.
yes, but there is no \u201cnext\u201d call - the offset and page-size are arbitrary params every call, there is no way to ask for \u201cpage 2 of my last query to the API\u201d
a channel of results in core.async is perfectly reasonable, as is a manifold stream of deferred responses
so:
- Make call for first page
- Process first page (hopefully async)
- Use total to work out how many more ops I need to make
- Fill up channel with calls
- Consume channel \u201celsewhere\u201d.
is my thesis - does that make sense..?
what is the main thread in this context? the app that \u201cruns\u201d which in turn is a hybrid API / webapp
core.async itself uses a threadpool, so you might not need to funnel them all down one channel unless you wanted to limit the number of concurrent api calls I would like to do as much concurrently as possible, but it\u2019s not a deal-breaker, serial is fine as long as the work can be \u201ckicked off\u201d and left going. order of results being processed is not important, so concurrency (particularly if it makes the whole thing faster) would be great.
I need to make one call to find out how many results match the request, the vendor / curator of the AI in question is not prepared to produce a simplified response to figure out the size of results sets, so I am stuck with that.
I am assuming that I need to \u201cdef\u201d the channel(s) and then have a form that is in an evaluated namespace that is \u201cwaiting\u201d for the channel(s) to have something on them..?
a common idiom is to return channels/streams/promises as the result of a query fn
OK, but how would I consume them without tying up the main thread?
many options
- put the consuming code inside a core.async
go
block - create a new channel with a transducer and pipe your first channel to that
- have your api fn take the channel you want responses put on and pass in a channel with a transducer
similarly in manifold,
- chain a step onto a deferred https://github.com/ztellman/manifold/blob/master/docs/deferred.md#composing-with-deferreds
- map a fn over a stream https://github.com/ztellman/manifold/blob/master/docs/stream.md#stream-operators
need some code in an evaluated namespace that was effectively \u201clistening\u201d for there to be \u201cthings\u201d on the channel, just a form at the end of my namespace containing a go block that was consuming a named channel onto which my API function would place \u201cthings\u201d
most web or UI frameworks will already have an event loop to do that i\u2019d have expected?
order of ops:
- Make first query to API
- Process result, including calculation of how many more ops required
- Load up a channel with the other calls
It\u2019s an app that will periodically (every hour / day not sure yet) make calls to an API, stash the returned data in a database and an ElasticSearch cluster, and then do it all again the next time.
might want to add [4] concatenate results from each of the page queries into a single record stream
but what will consume the eventual record stream ?
This makes the API into a smaller, custom dataset that can be interrogated via Kibana
I am not saying I don\u2019t want to add \u201c[4] concatenate results from each of the page queries into a single record stream\u201d, but I can\u2019t think of why I would do that, and that is probably me being ignorant of the benefits etc. Please could you explain to me why I would add this step - I really am asking, not being a prick, I promise
do you want to expose your downstream consumers to an additional level of structure (pages) which is an implementation feature of the upstream API ?
No
I want to take each of the 100 / 1000 / 10000 results and store them as individual documents in ES and as JSONB fields in Postgres
The API I am \u201charvesting\u201d has a 90 day sliding window, so over time the queries I make will have different results. I don\u2019t want to keep track of the last article I harvested, nor do I want to have to \u201cfind\u201d it in the results to then get all the newer ones. It\u2019s easier to just \u201ceat\u201d the whole response every time and reply on ES refusing to re-import a document with an existing id (into the same index) and on postgres\u2019s ability to enforce a \u201cunique\u201d index on the id field.
but I can\u2019t \u201cget\u201d all of the results in one query, the API limits \u201cpages\u201d to 1000 results, so I need to be able to stack up calls and execute them in an async, non-blocking manner.
yep, so you can concatenate the pages into a single record-stream, and process each of the records individually
OK, I like the sound of this in principle, and I think I am sort of doing that already with the synchronous, manual approach, as I get 100 articles back and then I do a doseq over the vector of maps to do INSERT queries into postgres and PUT calls to ES
What do you mean by a \u201crecord-stream\u201d?
by \"record stream\" i mean a conceptual sequence of individual records... could be on a core.async chan
or a manifold stream
the benefit is just simplicity - a sequence of individual records is a simpler thing than a sequence of pages of individual records... but there are tradeoffs - sometimes you want to deal with pages of records
Oh I see! Right, yeah, I was just going to consume the channel of returned promises with the doseq I already have, so concatenating them together into one HUGE vector first seemed like a redundant step. i.e. one of the queries I am going to do returns (currently) a little over 13,000 records - I was expecting to grab the results of 14 promises off the channel and \u201cdoseq\u201d each one until the channel was empty
I suppose I could consume them off the channel into one big vector, or indeed another channel and then have another consumer running what currently runs inside the doseq on each map / JSON blob that comes out of the channel\u2026 Is that what you mean?
so:
channel of promises consumer turns vector of maps into another channel of individual maps consumer2 puts maps into DB and ES off second channel
i meant another channel, yes
possibly even:
consumer2 puts maps into DB and onto another channel consumer3 puts maps on third channel into ES
(as an aside @maleghast , doing any long-running or blocking processing in a vanilla core.async go
block isn't a good idea - there is a fixed-size core.async threadpool which you can exhaust, causing blocking - so you can use https://clojure.github.io/core.async/index.html#clojure.core.async/thread )
So if I use thread inside a go block, or instead of a go block..?
here is an example
(async/go (let [v (async/<! (async/thread (do-blocking-stuff)))] (do-non-blocking-stuff v))
something like that
"},{"location":"core-async/#long-running-processes","title":"Long running processes","text":"also, beware long-running processes in core.async that expand items with eg. mapcat
operations. You can break back pressure that way. (ie. pages on a channel being expanded into multiple events)
ooo i haven't come across that problem what happens ?
requires a very specific use case to be a problem, but it\u2019s caught a few people out: https://stackoverflow.com/questions/37953401/where-is-the-memory-leak-when-mapcat-breaks-backpressure-in-core-async
Where is the memory leak when mapcat breaks backpressure in core.async? I wrote some core.async code in Clojure and when I ran it it consumed all available memory and failed with an error. It appears that using mapcat in a core.async pipeline breaks back pressure. (Whi...
you\u2019re not likely to hit it unless you are using a lot of transforms on your channels, and then its easily worked around, but it can work fine in test, and then blow up in prod with more data/longer running processing
"},{"location":"explaining-macros/","title":"Explaining Macros","text":"The macro system allows you to extend the design of the Clojure language, without waiting for the language designers.
Expose as much of your API as possible as functions so that they can be stored in hash-maps, mapped over sequences of widgets, negated with complement, juxtaposed with juxt, and so on.
Only define your own macros when functions are insufficient or there is a very common abstraction that creates a simpler system.
"},{"location":"explaining-macros/#hintclojure-macros-are-quite-unique","title":"Hint::Clojure macros are quite unique","text":"Many languages have macros, although most are more akin to a template systems.
Clojure macros are a language within the Clojure language that generate Clojure code when the Clojure Reader parses a macro. In fact the Clojure Reader will pass the macro to the macro reader which does the expansion.
"},{"location":"explaining-macros/#hintevery-macro-adds-maintenance-cost","title":"Hint::Every Macro adds maintenance cost","text":"Adding custom macros adds a higher maintenance cost to a codebase and increased the amount of on-boarding of developers onto a project.
Custom macros are additional abstractions to learn when working with a project that are not common across projects in the same way as clojure.core or common libraries.
"},{"location":"explaining-macros/#functional-composition-and-macros","title":"Functional composition and Macros","text":"Macros do not compose functionally
Macros in Clojure aren't values. They can't be passed as arguments to other functions or macros, can't be returned as the result of computations, and can't be stored in data structures.
If macros were values and used as arguments to functions then the compile cycle of Clojure would need to change, otherwise the results of macro expansion wouldn't be known until runtime and could even vary between function calls.
"},{"location":"explaining-macros/#wrapping-macros-in-functions-for-composition","title":"Wrapping Macros in functions for composition","text":"It is possible to wrap a macro in a function and then that macro can be used as part of a functionally composed expression.
(reduce and [true true false true])\n;;=> RuntimeException\n\n(reduce #(and %1 %2) [true true false true])\n;;=> false\n
If you are doing this, then its more probably that a simple function would be a better approach.
"},{"location":"io/","title":"IO in Clojure","text":"From http://blog.isaachodes.io/p/clojure-io-p1/
The Ins and Outs of Clojure: Part I November 13, 2010
(Written about Clojure 1.2.)
It is a truth universally acknowledged, that a programmer using Clojure will want to perform IO. Let me help you out (put).
I\u2019ll go over some of the basics of IO, focusing on what you can use Clojure to do directly. I\u2019ll move on after the basic introduction, to some of the more interesting and generally useful classes that Java offers, giving a little context for each. In
Reading files in is generally one of the first things I want to do when playing with a new language, so I\u2019ll start there. Before I get started though, I should mentioned that in Clojure, strings are always encoded using UTF-16. Generally this saves time and worry, but it\u2019s something to keep in mind should you run into problems on the encoding front. slurp
Clojure comes with a handy little function called slurp that takes in a string representing a filename (or, really, pretty much anything; a File, a stream, a byte array, URL, etc) and returns a string containing the contents of your file. It\u2019s pretty handy if you just need to get some information from a file that\u2019s relatively small, and you\u2019ll be parsing it yourself.
(slurp \"/home/user/file.txt\") => \"A little bit\\nof information here.\"
A nice thing about slurp is that you can easily build up file paths with str. For example, say you want to output to a file based on information you find at runtime:
(slurp (str \"/home/\" username \"/projects/\" filename))
But slurp is pretty basic, and once your files get large enough, totally impractical. Nonetheless, it\u2019s a handy function to know about.
As a useful and comical aside, the function spit is the counterpart to slurp, except that instead of reading input, spit does output. More on this in a future article, though. line-seq
One of my favorite IO functions has got to be line-seq; line-seq takes a reader object (which must implement BufferedReader) and returns a lazy sequence of the lines of the text the reader supplies. This is handy when you\u2019re dealing with files (if this offends you, taking a Unix approach here for now and say that everything is a file) that are too big to merely slurp, but that are \\newline delimited (or CR/LF delimited, if you\u2019re of the Windows persuasion).
(use '[clojure.java.io '(reader)]) (take 2 (line-seq (reader \"bobby.txt\"))) => (\"Bobby was a good boy,\" \"and didn't complain too much\")
Notice how we take 2 from the sequence we get from using line-seq. We can take as much or as little as we need; we won\u2019t be reading much (Clojure will read a bit more than you tell it to in order to get more IO performance, but let\u2019s not worry about that) more than we specify. We can do anything we want with the resulting seq; that\u2019s the beauty of line-seq and the ubiquitous sequence abstraction.
Back in the day, Clojurists had to sink a little lower than the clojure.java.io namespace to use line-seq; two Java classes were needed. One of these Java classes is the most wondrous and amazing thing just below the surface of the more elegant and beautiful Clojure code; BufferedReader. Here\u2019s how we used to do it;
(import '(java.io FileReader BufferedReader)) (take 2 (line-seq (BufferedReader. (FileReader. \"bobby.txt\"))) => (\"Bobby was a good boy,\" \"and didn't complain too much\")
This might give you a better sense of what\u2019s going on when you use reader, though in reality reader is far more complicated than just that: you can trust it to handle a variety of \u201creadable things\u201d and return to you a BufferedReader if possible.
FileReader will return a Reader on a file, and BufferedReader takes and buffers a Reader, as you might have extrapolated from the name. Readers are basically just objects upon which a few methods (like read, skip and close) may be enacted and expected to return reasonable results. line-seq essentially reads up until a line-delimiter and returns the read chunk as an element in the sequence it is generating.
While on the subject of files, I should probably mentioned the file function, from clojure.java.io. file takes in an arbitrary number of string arguments, and pieces them together into a file hierarchy, returning a File instance. This can come in handy. Rivers? inputStreams? Brooks?
Streams are an especially useful class of readers. Oftentimes you\u2019re reading in text; that\u2019s what Readers do. But often you need to read in a stream of bytes; that\u2019s where you need to use clojure.java.io\u2019s input-stream.
(use '[clojure.java.io '(reader)]) (def g (input-stream \"t.txt\")) (.read g) => 105 (.read g) => 115 (char (.read g)) => \\space
As you can see, instead of getting characters from this file (like we get when we use a reader), we\u2019re getting integer byte values. This can be useful when reading, for example, a media file.
In general, strings are always UTF-16, which are 16-bit pieces of data, whereas byte-streams are 8-bit pieces of data. It bears repeating that the stream operators should be used when you\u2019re not dealing with strings: they are not trivially interchangeable, as they might be in other languages where strings are syntactic sugar for byte arrays. RandomAccessFile
Finally, let me introduce to you a spectacularly useful Java class. RandomAccessFile is a class which allows you to quickly jump around in a large file, and read bytes from it.
(import '(java.io RandomAccessFile)) (def f (RandomAccessFile. \"stuff.txt\" \"r\"))
Note the second argument of the constructor, \u201cr\u201d; this indicates that we\u2019re opening the file just for reading. Now that we have f, we can use it to navigate and read the file:
(.read f) => 105 (.length f) => 2015 ;; this is the number of bytes this file is in length (.skipBytes f 20) (.getFilePointer h) => 21 ;; the position we're at in the file (.read f) => 89
As you can see, you can jump around (quickly!) through a file, and read from the parts you want, and skip the parts you do not want. The key methods/functions here (among many others that can also be useful; be sure to check the documentation) are read, length, skipBytes, seek and getFilePointer. Closing
Every file that is opened should be closed, and what we\u2019ve been doing is a little unsafe. In order to close an open reader/file, we should use the close method on it; in the above example, when you\u2019re done with f, simply execute (.close f) to tell the file system that you\u2019re done with the file. Alternatively, and more idiomatically, you can open your files with the handy with-open binder:
(with-open [f (RandomAccessFile. \"stuff.txt\" \"r\")] (.read f))
When you\u2019re done with f, Clojure will close it, and you won\u2019t have to worry one iota about it. Digging Deeper
Should slurp and line-seq not be enough for your reading needs (and chances are that, should you code enough in Clojure, they won\u2019t always been), you might want to explore clojure.java.io some more, as well as some of the Java classes (namely, those stemming from Reader and BufferedReader, as well as InputStream and BufferedInputStream) mentioned above. See my previous article on using Java if you\u2019re unfamiliar with using Java.
Next up is an introduction to the \u201couts\u201d of Clojure and Java. Stay tuned!
I owe a big thank you to Phil Hagelberg for reading over this essay and offering advice. If you don\u2019t already, you should be using his Leiningen for both dependency management and a stress-free development environment.
The Ins and Outs of Clojure: Part I November 13, 2010
(Written about Clojure 1.2.)
It is a truth universally acknowledged, that a programmer using Clojure will want to perform IO. Let me help you out (put).
I\u2019ll go over some of the basics of IO, focusing on what you can use Clojure to do directly. I\u2019ll move on after the basic introduction, to some of the more interesting and generally useful classes that Java offers, giving a little context for each. In
Reading files in is generally one of the first things I want to do when playing with a new language, so I\u2019ll start there. Before I get started though, I should mentioned that in Clojure, strings are always encoded using UTF-16. Generally this saves time and worry, but it\u2019s something to keep in mind should you run into problems on the encoding front. slurp
Clojure comes with a handy little function called slurp that takes in a string representing a filename (or, really, pretty much anything; a File, a stream, a byte array, URL, etc) and returns a string containing the contents of your file. It\u2019s pretty handy if you just need to get some information from a file that\u2019s relatively small, and you\u2019ll be parsing it yourself.
(slurp \"/home/account/projects/config.txt\") => \"A little bit\\nof information here.\"
A nice thing about slurp is that you can easily build up file paths with str. For example, say you want to output to a file based on information you find at runtime:
(slurp (str \"/home/\" username \"/projects/\" filename))
But slurp is pretty basic, and once your files get large enough, totally impractical. Nonetheless, it\u2019s a handy function to know about.
As a useful and comical aside, the function spit is the counterpart to slurp, except that instead of reading input, spit does output. More on this in a future article, though. line-seq
One of my favorite IO functions has got to be line-seq; line-seq takes a reader object (which must implement BufferedReader) and returns a lazy sequence of the lines of the text the reader supplies. This is handy when you\u2019re dealing with files (if this offends you, taking a Unix approach here for now and say that everything is a file) that are too big to merely slurp, but that are \\newline delimited (or CR/LF delimited, if you\u2019re of the Windows persuasion).
(use '[clojure.java.io '(reader)]) (take 2 (line-seq (reader \"bobby.txt\"))) => (\"Bobby was a good boy,\" \"and didn't complain too much\")
Notice how we take 2 from the sequence we get from using line-seq. We can take as much or as little as we need; we won\u2019t be reading much (Clojure will read a bit more than you tell it to in order to get more IO performance, but let\u2019s not worry about that) more than we specify. We can do anything we want with the resulting seq; that\u2019s the beauty of line-seq and the ubiquitous sequence abstraction.
Back in the day, Clojurists had to sink a little lower than the clojure.java.io namespace to use line-seq; two Java classes were needed. One of these Java classes is the most wondrous and amazing thing just below the surface of the more elegant and beautiful Clojure code; BufferedReader. Here\u2019s how we used to do it;
(import '(java.io FileReader BufferedReader)) (take 2 (line-seq (BufferedReader. (FileReader. \"bobby.txt\"))) => (\"Bobby was a good boy,\" \"and didn't complain too much\")
This might give you a better sense of what\u2019s going on when you use reader, though in reality reader is far more complicated than just that: you can trust it to handle a variety of \u201creadable things\u201d and return to you a BufferedReader if possible.
FileReader will return a Reader on a file, and BufferedReader takes and buffers a Reader, as you might have extrapolated from the name. Readers are basically just objects upon which a few methods (like read, skip and close) may be enacted and expected to return reasonable results. line-seq essentially reads up until a line-delimiter and returns the read chunk as an element in the sequence it is generating.
While on the subject of files, I should probably mentioned the file function, from clojure.java.io. file takes in an arbitrary number of string arguments, and pieces them together into a file hierarchy, returning a File instance. This can come in handy. Rivers? inputStreams? Brooks?
Streams are an especially useful class of readers. Oftentimes you\u2019re reading in text; that\u2019s what Readers do. But often you need to read in a stream of bytes; that\u2019s where you need to use clojure.java.io\u2019s input-stream.
(use '[clojure.java.io '(reader)]) (def g (input-stream \"t.txt\")) (.read g) => 105 (.read g) => 115 (char (.read g)) => \\space
As you can see, instead of getting characters from this file (like we get when we use a reader), we\u2019re getting integer byte values. This can be useful when reading, for example, a media file.
In general, strings are always UTF-16, which are 16-bit pieces of data, whereas byte-streams are 8-bit pieces of data. It bears repeating that the stream operators should be used when you\u2019re not dealing with strings: they are not trivially interchangeable, as they might be in other languages where strings are syntactic sugar for byte arrays. RandomAccessFile
Finally, let me introduce to you a spectacularly useful Java class. RandomAccessFile is a class which allows you to quickly jump around in a large file, and read bytes from it.
(import '(java.io RandomAccessFile)) (def f (RandomAccessFile. \"stuff.txt\" \"r\"))
Note the second argument of the constructor, \u201cr\u201d; this indicates that we\u2019re opening the file just for reading. Now that we have f, we can use it to navigate and read the file:
(.read f) => 105 (.length f) => 2015 ;; this is the number of bytes this file is in length (.skipBytes f 20) (.getFilePointer h) => 21 ;; the position we're at in the file (.read f) => 89
As you can see, you can jump around (quickly!) through a file, and read from the parts you want, and skip the parts you do not want. The key methods/functions here (among many others that can also be useful; be sure to check the documentation) are read, length, skipBytes, seek and getFilePointer. Closing
Every file that is opened should be closed, and what we\u2019ve been doing is a little unsafe. In order to close an open reader/file, we should use the close method on it; in the above example, when you\u2019re done with f, simply execute (.close f) to tell the file system that you\u2019re done with the file. Alternatively, and more idiomatically, you can open your files with the handy with-open binder:
(with-open [f (RandomAccessFile. \"stuff.txt\" \"r\")] (.read f))
When you\u2019re done with f, Clojure will close it, and you won\u2019t have to worry one iota about it. Digging Deeper
Should slurp and line-seq not be enough for your reading needs (and chances are that, should you code enough in Clojure, they won\u2019t always been), you might want to explore clojure.java.io some more, as well as some of the Java classes (namely, those stemming from Reader and BufferedReader, as well as InputStream and BufferedInputStream) mentioned above. See my previous article on using Java if you\u2019re unfamiliar with using Java.
Next up is an introduction to the \u201couts\u201d of Clojure and Java. Stay tuned!
I owe a big thank you to Phil Hagelberg for reading over this essay and offering advice. If you don\u2019t already, you should be using his Leiningen for both dependency management and a stress-free development environment.
"},{"location":"lazy-evaluation/","title":"Lazy evaluation","text":"repeat
"},{"location":"testing-in-clojure/","title":"Testing in clojure","text":"My recommended Clojure testing setup
kaocha test runner in watch mode
Occasionally, either on Stack Overflow or in the Clojurians Slack group, someone will ask what tools they should use to test Clojure code. Below is what I would currently recommend. I\u2019ve come to this recommendation through observing teams using a variety of testing tools and through my own use them.
Use clojure.test with humane-test-output and lein-test-refresh.
Use clojure.test
clojure.test is ubiquitous and not a big departure from other languages' testing libraries. It has its warts but your team will be able to understand it quickly and will be able to write maintainable tests.
Use humane-test-output
You should use clojure.test with humane-test-output. Together they provide a testing library that has minimal additional syntax and good test failure reporting.
Use lein-test-refresh
If you\u2019re not using a tool that reloads and reruns your tests on file changes then you are wasting your time. The delay between changing code and seeing test results is drastically reduced by using a tool like lein-test-refresh. Nearly everyone I know who tries adding lein-test-refresh to their testing toolbox continues to use it. Many of these converts were not newcomers to Clojure either, they had years of experience and had already developed workflows that worked for them.
Use lein-test-refresh\u2019s advanced features
lein-test-refresh makes development better even if you don\u2019t change any of its settings. It gets even better if you use some of its advanced features.
Below is a stripped down version of my ~/.lein/profiles.clj. The :test-refresh key points towards my recommended lein-test-refresh settings.
{:user {:dependencies [[pjstadig/humane-test-output \"0.8.0\"]] :injections [(require 'pjstadig.humane-test-output) (pjstadig.humane-test-output/activate!)] :plugins [[[com.jakemccrary/lein-test-refresh \"0.16.0\"]]] :test-refresh {:notify-command [\"terminal-notifier\" \"-title\" \"Tests\" \"-message\"] :quiet true :changes-only true}}}
These settings turn on notifications when my tests finish running (:notify-command setting), make clojure.test\u2019s output less verbose (:quiet true), and only run tests in namespaces affected by the previous code change (:changes-only true). These three settings give me the quickest feedback possible and free me from having the terminal running lein test-refresh visible.
Quick feedback lets you make changes faster. If you\u2019re going to write tests, and you should write tests, having them run quickly is powerful. After years of writing Clojure, this is my current go-to for testing Clojure code and getting extremely fast feedback.
I use test-refresh every day. Frankly I'm not sure how one can program effectively without it. I especially like being able to control the notifications
"},{"location":"alternative-tools/","title":"Alternative Tools","text":""},{"location":"alternative-tools/clojure-cli/basic-repl/","title":"Basic Terminal REPL UI","text":"The clojure
command will start a REPL by default or if given the --repl
or -r
argument. The basic repl does not provide history of commands.
clj
is a script that wraps the clojure
command and requires rlwrap
, an external readline command, to navigate REPL history via the Up and Down keys.
Use clj
when you want to run a repl (or preferably use rebel readline instead) and clojure
for everything else.
Rebel Rich Terminal UI
rebel readline is a terminal REPL UI that provides interactive help, function autocomplete, signature prompts and many other features to provide a very rich REPL experience.
Practicalli Clojure CLI Config includes the -M:repl/rebel
alias to run rebel readline REPL.
clj
command in a terminal window starts a Clojure REPL and shows the version of Clojure used. The command does not need to be in a directory containing a Clojure project.
clj\n
Type in a Clojure expression at the => user
REPL prompt and press Enter to see the result
Ctrl+d to exit the REPL
"},{"location":"alternative-tools/clojure-cli/compare-with-leiningen/","title":"Clojure Automation Aproaches","text":"Leiningen, Clojure CLI tools and Boot are different approaches to Clojure project configuration. Regardless of the tool used, the Clojure code in the project remains the same.
"},{"location":"alternative-tools/clojure-cli/compare-with-leiningen/#clojure-cli-tools-overview","title":"Clojure CLI tools overview","text":"Clojure CLI tools takes a very simple approach, focusing on running a REPL process, Clojure programs via clojure main and specific functions via clojure exec. With the Clojure exec approach any function can be called as the entry point to running an application or tool written in Clojure.
CLI tools can use both Maven and Git repositories for dependency management. Code from a Git repository dependency can be used without packaging it into a library (Java jar file), simplifying the use of libraries under active development.
Clojure CLI tools provides a comprehensive set of features via community tools. These community tools are provided via aliases in the user level configuration for all projects (e.g. practicalli/clojure-deps-edn) or an alias for a specific project deps.edn
configuration.
"},{"location":"alternative-tools/clojure-cli/compare-with-leiningen/#leiningen-overview","title":"Leiningen overview","text":"Leiningen is a feature rich tool which is simple to get started with a plugin extension to add more functionality. Leiningen does use more resources as it starts a Java Virtual machine to run itself and another to run the application.
"},{"location":"alternative-tools/clojure-cli/compare-with-leiningen/#boot-overview","title":"Boot overview","text":"Boot runs tasks written in Clojure on the command line, providing a flexible way to work with projects. However, this approach does require expertise with Clojure and Clojure scripts to work with projects.
Clojure code is the same which ever approach used
The Clojure code for the project will be the same regardless of which tool is used to configure and manage the Clojure project.
"},{"location":"alternative-tools/clojure-cli/compare-with-leiningen/#configuration","title":"Configuration","text":"Tool Project Config User Config Extension Clojure CLI tools deps.edn hash-map merged with user config $XDG_HOME_CONFIG/clojure/deps.edn
or $HOME/.clojure/deps.edn
aliases in project and user deps.edn Leiningen project.clj and defproject
macro ~/.lein/profiles.clj Leiningen specific plugin Boot build.boot with deftask
, task-options!
, set-env!
Write the tasks required in Clojure Clojure CLILeiningenBoot Clojure CLI tools are configured with an EDN data structure, i.e. a hash-map of key-value pairs. As this is a Clojure data structure its much easier to parse and should be very familiar to Clojure developers.
Leiningen projects are configured with a project.clj
file which contains a defproject
macro with a great many options. The Leiningen tutorial explains the options in detail. A sample project.clj contains examples of using each of this options.
For most projects all the configuration resides in the project.clj file. Exceptions to this include figwheel-main, which also adds it own EDN configuration and EDN build configuration files.
Leiningen also has a user level configuration
build.boot
is a file containing Clojure code that defines the tasks for using your project.
"},{"location":"alternative-tools/clojure-cli/compare-with-leiningen/#extending-the-tools","title":"Extending the tools","text":"Clojure CLILeiningenBoot Clojure CLI tools has been designed for a very specific role, to provide a lightweight wrapper over running Clojure programs and via tools.deps managing dependencies from Maven and Git repositories.
The projects that extend Clojure CLI tools are self-contained libraries and tools, so are not tied to any one particular tool. Any general tools written for Clojure should work with Clojure CLI tools by calling their main function (clojure main) or a specifically named function (clojure exec)
Leiningen plugin extension was the main way to extend the functionality of Leiningen (or getting pull requests accepted to the Leiningen projects).
Although there are several plugins that were widely adopted, some plugins eventually caused more confusion than benefit or were simply trivial and in the main plugins seem to have become less important to the Clojure community.
One of the limitations of Leiningnen plugin mechanism was not being able to exclude any configuration in a users .lein/profiles.clj
file, so there was greater potential for conflict.
The recommended way to extend Leiningen is to not write plugins, but to include aliases that define a qualified function to run when that alias is used with the Leiningen command.
Write clojure to define scripts to run with boot projects.
Babashka for Clojure scripting
Babashka is an approach to writing bash-style scripts using the Clojure language. Babashka bundles additional libraries to support common tasks
"},{"location":"alternative-tools/clojure-cli/evaluate-an-expression/","title":"Evaluating an expression with Clojure CLI tools","text":"An expression is a piece of Clojure code that can be evaluated and return a result
This expression calls the +
function with the arguments 1 2 3 4 5
. As this code works, we get a result.
(+ 1 2 3 4 5)\n
Using the -e
option an expression can be passed to the Clojure CLI tools and a value returned
clojure -e (+ 1 2 3 4 5)\n
"},{"location":"alternative-tools/clojure-cli/evaluate-an-expression/#expressions-returning-nil","title":"Expressions returning nil
","text":"If the expressing used returns a value of nil
, a legal value in Clojure, then no result is printed out.
"},{"location":"alternative-tools/clojure-cli/evaluate-an-expression/#when-to-use-this","title":"When to use this?","text":"clojure -e
is a quick way to see what an expression does without having to set anything up (although starting a REPL is very little extra effort).
Using the -e
option is useful for running simple scripts written in Clojure, especially on servers and remote environments.
The lack of return value for nil is very useful when using Clojure CLI tools to evaluate Clojure code within another script.
"},{"location":"alternative-tools/clojure-cli/set-namespace-on-repl-startup/","title":"Set namespace on REPL startup","text":"The REPL process does not evaluate project code on start-up. If it did and that code had a error, it could prevent the REPL from starting.
The common approach is to require the main namespace for the project, making the functions in that namespace available. This will also make available functions from those namespaces.
Switching to a specific namespace in the REPL allows calling functions by name, without the fully qualified name.
"},{"location":"alternative-tools/clojure-cli/set-namespace-on-repl-startup/#set-namespace-via-the-command-line","title":"Set namespace via the command line","text":"To require and switch to a namespace on startup, use the clojure
or clj
commands with the --eval option to run the specific commands. The --repl option will ensure the repl starts.
clj --eval \"(require 'practicalli.random-clojure-core-function)\" --eval \"(in-ns 'practicalli.random-clojure-core-function)\" --repl\n
-r
or (clojure.main/repl)
are the same as using the --repl
option
clj -e \"(ns foo.bar) (alter-var-root #'*ns* (constantly 'foo.bar))\" -r\nclj -e \"(ns foo.bar) (alter-var-root #'*ns* (constantly 'foo.bar)) (clojure.main/repl)\"\n
"},{"location":"alternative-tools/clojure-cli/set-namespace-on-repl-startup/#set-namespace-with-rebel-readline","title":"Set namespace with Rebel Readline","text":"Set the namespace using Rebel Readline alias from Practicalli Clojure CLI Config
clj -M:lib/rebel -e \"(ns foo.bar) (alter-var-root #'*ns* (constantly (find-ns 'foo.bar)))\" -m rebel-readline.main\n\n#object[clojure.lang.Namespace 0x46cf05f7 \"foo.bar\"]\n[Rebel readline] Type :repl/help for online help info\nfoo.bar=>\n
The :lib/rebel
alias adds the rebel library as a dependency without calling clojure main on the rebel namespace. alter-var-root
sets the namespace. The -m
flag defines the namespace which Clojure main will run the -main
function from, starting the rebel UI on the command line.
The --eval
approach will be blocked if used with aliases that set the main namespace, such as :repl/rebel
.
"},{"location":"alternative-tools/clojure-cli/set-namespace-on-repl-startup/#set-namespace-using-an-editor","title":"Set namespace using an editor","text":"It is not necessary to set the namespace when evaluating code in a Clojure aware editor. Expressions are evaluated within the scope of the namespace in which they are defined.
Using an editor to evaluate Clojure is much simpler and quicker than using a command line REPL, especially when working with Clojure projects with more than one namespace.
"},{"location":"alternative-tools/data-inspector/reveal/","title":"Reveal REPL with visual data browser","text":"Reveal provides a REPL with a connected visual data explorer. Reveal can also be used as a tap>
target, visualizing data added to a tap>
(an elegant approach compared to print statements).
Reveal describes itself as a read evaluate visualize loop tool proving extra tools to explore data in Clojure visually. The extensive documentation shows the many ways to use Reveal.
Use Reveal with a terminal REPL or a Clojure editor that uses nrepl such as Emacs Cider and Spacemacs. Reveal can be added as a tap source to any running REPL, eg. using Reveal with Rebel Readline.
REPL Terminal UInrepl Editors Practicalli Clojure CLI Config contains the :inspect/reveal
alias to run Reveal REPL in a terminal with the Reveal UI along-side.
Open a terminal and run the command:
clojure -M:inspect/reveal\n
Use the clj
command if the rlwrap
binary is available
Write Clojure code as normal in the REPL and result are also sent to the Reveal data browser window.
The extensive documentation shows the many ways to use Reveal.
Start reveal with an nREPL server to allow connection from a Clojure aware editor
Practicalli Clojure CLI ConfigAlias Definition :inspect/reveal-nrepl
alias provided by Practicalli Clojure CLI Config runs a Reveal repl with data browser and nrepl server, allowing connections from Clojure aware editors such as Emacs CIDER and VSCode Calva.
:inspect/reveal-light-nrepl
does the same and uses a light them with Ubuntu Mono 32 point font (good for demos, HiDPI screens)
Add the following aliases to your user level ~/.clojure/deps.edn
configuration to make reveal available to all projects.
Plain nrepl server with Reveal.
:inspect/reveal-nrepl\n {:extra-deps {vlaaad/reveal {:mvn/version \"1.1.159\"}\n nrepl/nrepl {:mvn/version \"0.8.3\"}}\n :main-opts [\"-m\" \"nrepl.cmdline\"\n \"--middleware\" \"[vlaaad.reveal.nrepl/middleware]\"]}\n
CIDER specific nrepl connection with the Cider middleware :inspect/reveal-nrepl-cider\n {:extra-deps {vlaaad/reveal {:mvn/version \"1.1.159\"}\n nrepl/nrepl {:mvn/version \"0.8.3\"}\n cider/cider-nrepl {:mvn/version \"0.25.4\"}\n refactor-nrepl/refactor-nrepl {:mvn/version \"2.5.0\"}}\n :main-opts [\"-m\" \"nrepl.cmdline\"\n \"--middleware\" \"[vlaaad.reveal.nrepl/middleware,refactor-nrepl.middleware/wrap-refactor,cider.nrepl/cider-middleware]\"]}\n
Start a Reveal REPL with nrepl server
clojure -M:inspect/reveal-nrepl\n
Connect to the Reveal repl from a Clojure aware editor, e.g cider-connect
"},{"location":"alternative-tools/data-inspector/reveal/#using-reveal-with-cider-jack-in","title":"Using Reveal with Cider Jack-in","text":"C-u cider-jack-in-clj
in CIDER to start a reveal REPL (SPC u , '
in Spacemacs)
Edit the jack-in command by deleting the all the configuration after the clojure
command and add the alias
clojure -M:inspect/reveal-nrepl-cider\n
:inspect/reveal-nrepl-cider
is a light version of the above.
"},{"location":"alternative-tools/data-inspector/reveal/#cider-jack-in-with-reveal-using-a-dir-localsel","title":"Cider jack-in with reveal using a .dir-locals.el","text":"Add a .dir-locals.el
file to the root of the Clojure project. The .dir-locals.el
configuration adds the :inspect/reveal-nrepl-cider
via cider-clojure-cli-aliases
and all other automatically injected configuration is disabled to prevent those dependencies over-riding the alias.
((clojure-mode . ((cider-preferred-build-tool . clojure-cli)\n (cider-clojure-cli-aliases . \":inspect/reveal-nrepl-cider\")\n (cider-jack-in-dependencies . nil)\n (cider-jack-in-nrepl-middlewares . nil)\n (cider-jack-in-lein-plugins . nil)\n (cider-clojure-cli-parameters . \"\"))))\n
Ensure the .dir-locals.el
file is loaded by using revert-buffer
on any open Clojure file from the project.
cider-jack-in-clj
should now start the Reveal REPL and send evaluations from Cider to the Reveal visualization UI.
"},{"location":"alternative-tools/data-inspector/reveal/#reveal-with-rebel-and-tap","title":"Reveal with Rebel and tap>
","text":"Reveal can be used as a tap>
target with the Rebel REPL, launching the Reveal data browser when added as a tap> target.
Start Rebel REPL with Reveal library as a dependency
clojure -M:repl-reveal:repl/rebel\n
Add reveal as a tap target that will receive all data from tap>
function calls
(add-tap ((requiring-resolve 'vlaaad.reveal/ui)))\n
A reveal window opens and receives all tap>
values while the REPL is running.
(tap> [1 2 3 4 5])\n(tap> {:name \"Jenny\" :skill \"Clojure\"})\n(tap> (zipmap [:a :b :c] [1 2 3 4]))\n
"},{"location":"alternative-tools/data-inspector/reveal/#atom-and-tapped-value","title":"Atom and Tapped value","text":"Create an atom for debugging purposes
(def debug-state (atom nil))\n(add-tap #(reset! debug-state %))\n
Use tap>
to capture intermediate values in the middle of code that requires debugging, inspecting or watching the atom value (deref the atom and watch:latest or watch:all). This approach complements an editor debugging process
"},{"location":"alternative-tools/data-inspector/reveal/#tracking-state-with-an-atom","title":"Tracking state with an atom","text":"Define the state as an atom, the state being a simple value in this case
(def state (atom 24))\n
Select the reference created for the state in the Revel browser.
SPACE
or ENTER
to open the menu and select Deref
. A tab opens with the value of the atom.
SPACE
or ENTER
with the value selected and select Watch:all
.
In the REPL, update the value of the state atom.
(swap! state * 12)\n
The new value of the state atom is shown in the Reveal data browser. Each Clojure expressions evaluated that affects the state atom will be displayed in the Reveal browser.
"},{"location":"alternative-tools/data-inspector/reveal/#running-different-types-of-repl","title":"Running different types of repl","text":"Using Clojure exec -X
flag, the default repl function can be over-ridden on the command line, supplying the io-prepl
or remote-prepl
functions.
clojure -X:inspect/reveal io-prepl :title '\"I am a prepl repl\"
clojure -X:inspect/reveal remote-prepl :title '\"I am a remote prepl repl\"'
"},{"location":"alternative-tools/data-inspector/reveal/#configure-theme-font","title":"Configure theme & font","text":"Add a custom theme and font via the -J
command line option or create an alias using :inspect/reveal-light
as an example.
clojure -M:inspect/reveal -J-Dvlaaad.reveal.prefs='{:theme :light :font-family \"Ubuntu Mono\" :font-size 32}'\n
"},{"location":"alternative-tools/data-inspector/reveal/#rebel-readline-reveal-add-reveal-as-tap-source","title":"Rebel Readline & Reveal: Add Reveal as tap> source","text":"Evaluate (add-tap ((requiring-resolve 'vlaaad.reveal/ui)))
when using Rebel Readline to add Reveal as a tap source, showing (tap> ,,,)
expressions in the reveal window, eg. (tap> (map inc [1 2 3 4 5]))
.
"},{"location":"alternative-tools/leiningen/","title":"Leiningen build automation tool","text":"Leiningen will help you create, build and deploy your Clojure projects.
Practicalli recommends using Clojure CLI
For new project, Clojure CLI is recommended as it requires fewer resources and enables a more customisable approach to configuring project and running tools to support Clojure development.
"},{"location":"alternative-tools/leiningen/#install-leiningen","title":"Install Leiningen","text":"Install the Leiningen tool using the specific instructions for your Operating System
{% tabs first=\"Linux\", second=\"Homebrew\", third=\"GitBash\", forth=\"Chocolatey\", fifth=\"Windows Manual\" %}
LinuxHomebrewGitBashChocolateyWindows Download the lein script to your local bin
directory. Then make the lein
script executable and run lein
to download the full version.
mkdir ~/bin\ncurl https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein > ~/bin/lein\nchmod u+x ~/bin/lein\nlein\n
If the command lein
is not found, run source ~/.bashrc
to ensure your bin
directory is on the path. If you have Homebrew installed, run the following command in a terminal window.
brew install leiningen\n
GitBash allows you to use the Linux lein
script, which may have less issues when installing.
Create a directory called C:\\Users\\your-user-name\\AppData\\Local\\Programs\\Leiningen
Download the lein
file and save it to the above directory
Open Environment variables for your account
and add the directory to your path
Open a command window and run the command: lein
The full version of Leiningen will be downloaded and Leiningen is ready to use.
If you have Chocolatey installed, add the Leiningen package by running the following command in a terminal window.
choco install lein\n
Create a directory called C:\\Users\\your-user-name\\AppData\\Local\\Programs\\Leiningen
Download the lein.bat
file and save it to the above directory
Open Environment variables for your account
and add the directory to your path
Open a command window and run the command: lein.bat
The full version of Leiningen will be downloaded and Leiningen is ready to use.
"},{"location":"alternative-tools/leiningen/#run-leiningen","title":"Run Leiningen","text":"Check Leiningen is working by running lein
command in a terminal
lein help\n
If a list of Leiningen commands is shown then it is working correctly.
"},{"location":"alternative-tools/leiningen/#create-project","title":"Create project","text":"Create a new Clojure project with Leiningen using the new
task
lein new template-name domain/project-name\n
Built-in templates include app
and lib
Create a new project called playground
Open a terminal window and in a directory where you usually keep your projects, run the following command
lein new app practicalli/playground\n
A new directory will be created called playground
with a src/practicalli/playground.clj
"},{"location":"alternative-tools/leiningen/#unit-testing","title":"Unit Testing","text":"Leiningen automatically includes the test
directory when running, so no additional configuration is required if all tests reside inside the test
directory.
Run all the tests saved to file:
lein test\n
Run just the unit tests in a specific namepsace.
lein test :only domain.namespace-test\n
"},{"location":"alternative-tools/leiningen/#test-plugins","title":"Test Plugins","text":"The following Leiningen plugins watch the file system and will run tests when a file change is detected in the project files.
- lein-test-refresh
- lein-auto
"},{"location":"alternative-tools/leiningen/#configure-test-paths","title":"Configure test paths","text":":test-paths
added as a top level key to the defproject
configuration in the project.clj
file will configure specific paths for tests
For example, if the tests are defined under project-name/clj/tests
then the project.clj file would look as follows:
(defproject my-project \"0.5.0-SNAPSHOT\"\n :description \"A project for doing things.\"\n :license \"Creative Commons Zero\"\n :url \"http://github.com/practicalli/my-project\"\n\n :dependencies [[org.clojure/clojure \"1.10.1\"]]\n :test-paths [\"clj/test\" \"src/test/clojure\"]\n :plugins [[lein-auto \"0.1.3\"]])\n
:source-paths
can also be used to define the location of the source code files in the same manner.
"},{"location":"assets/images/social/","title":"Social Cards","text":"Social Cards are visual previews of the website that are included when sending links via social media platforms.
Material for MkDocs is configured to generate beautiful social cards automatically, using the colors, fonts and logos defined in mkdocs.yml
Generated images are stored in this directory.
"},{"location":"automation/","title":"Automation","text":"Automation tools can provide a consistent command line interface across a wide range of projects.
Whilst the Clojure CLI is a very extensible tool that flexibility can also add some complexity to its command line interface.
Automation tools abstract the command line to provide a consistent and simple user experience whilst keeping underlying flexibility.
Practicalli recommends make
A Makefile is not reliant on programming language knowledge so has no barrier to those who are unfamiliar with the Clojure language.
Make is useful when working with mixed language teams to create a unified tool and command line across a wide range of projects.
"},{"location":"automation/#automation-tooling","title":"Automation tooling","text":" - Make - ubiquitous task automation tool, programming language agnostic
- Shell Scripts
- Babashka - create task automation tool with Clojure
"},{"location":"automation/#make","title":"Make","text":"Make is very simple to use and has a long history as a build tool and wider task automation tool.
Task are defined in a Makefile
and task can depend on each other. Any commands or combination of commands that run on the command line can be used as make tasks.
make provides tab completion of tasks defined in the Makefile without additional configuration.
make
is available for all operating systems.
Practicalli Project Templates include Makefile
Creating new projects with :project/create
and Practicalli Project Templates provide a Makefile with a wide range of common tasks for Clojure development.
"},{"location":"automation/#shell-scripts","title":"Shell Scripts","text":"Shell scripts provide a very common way to create a relatively ubiquitous approach to running tools, even across multiple Shell implementations (Bash, Zsh, fish, etc.) and operating systems.
Shell scripting language is very powerful especially for manipulation of the operating system, although scripts require development and maintenance.
"},{"location":"automation/#babashka","title":"Babashka","text":"Write automation scripts with Clojure code using the Babashka task runner
Babashka can use a wide range of Clojure functions and libraries, although as a general script tool then additional coding and maintenance may be reqiured compared to a dedicated tool.
Babashka task runner
"},{"location":"automation/make/","title":"Make","text":" practicalli/dotfiles Makefile
GNU Make provide a simple and consistent way to run any development task for Clojure & ClojureScript projects (or any other languages).
Wrap any combination of tools (building, linting, formatting, testing) with Make targets for a simple command line interface.
Make supports tab completion making tasks discoverable.
All that is required is a Makefile
in the root of the project
"},{"location":"automation/make/#gnu-make-overview","title":"GNU Make overview","text":"GNU Make is a language agnostic build automation tool which has been an integral part of building Linux/Unix operating system code and applications for decades, providing a consistent way to configure, compile and deploy code for all projects.
A Makefile
defines targets called via the make
command. Each target can run one or more commands. Targets can be dependent on other targets, e.g the dist
target that builds a project can be dependent on deps
& test
targets.
GNU Make is available on all Linux/Unix based operating systems (and Windows via chocolatey or nmake).
Practicalli also uses make
to configure and build the latest versions of Emacs and other Linux open source software
"},{"location":"automation/make/#defining-tasks","title":"Defining tasks","text":"Create a Makefile
in the root of a project and define a target by typing a suitable name followed by a :
character, e.g. test:
Insert a tab on the next line and type a command to be called. Further commands can be added on new lines so long as each line is tab indented.
The repl
target prints out an information message and then uses the Clojure CLI with aliases from practicalli/clojure-deps-edn to run a Clojure REPL process with a rich terminal UI (Rebel Readline)
repl: ## Run Clojure REPL with rich terminal UI (Rebel Readline)\n $(info --------- Run Rebel REPL ---------)\n clojure -M:dev/env:test/env:repl/rebel\n
"},{"location":"automation/make/#common-target-naming","title":"Common target naming","text":"Targets used across Practicalli projects follow the make standard targets for users
all
, test-ci
, deps
and dist
targets are recommended for use with a CI deployment pipeline and builder stage when using Docker.
all
calling all targets to prepare the application to be run. e.g. all: deps test-ci dist clean deps
download library dependencies (depend on deps.edn
file) dist
create a distribution tar file for this program or zip deployment package for AWS Lambda lint
run lint tools to check code quality - e.g MegaLinter which provides a wide range of tools format-check
report format and style issues for a specific programming language format-fix
update source code files if there are format and style issues for a specific programming language pre-commit
run unit tests and code quality targets before considering a Git commit repl
run an interactive run-time environment for the programming language test-unit
run all unit tests test-ci
test running in CI build (optionally focus on integration testing) clean
remove files created by any of the commands from other targets (i.e. ensure a clean build each time)
practicalli/dotfiles/Makefile also defines docker targets to build and compose images locally, inspect images and prune containers and images.
"},{"location":"automation/make/#target-dependencies","title":"Target dependencies","text":"A Makefile
target can depend on either a file name or another target in the Makefile
.
The all target typically depends on several Makefile
targets to test, compile and package a service. Add the names of the targets this target depends upon
all: deps test-ci dist clean\n
Add the filename of a file after the name of the target, to depend on if that file has changed. If the file has not changed since make was last run then the task will not run again.
Clojure CLI Example: If the deps
target depends on deps.edn
and the file has not changed since last run, the deps target will not run again.
"},{"location":"automation/make/#deps-target-depend-on-a-file","title":"deps target - depend on a file","text":"The deps target would use Clojure CLI or Leiningen to download dependencies.
Configuring the deps
target to depend on deps.edn
or project.clj
file, then if the file has not changed the deps will not run again.
A Clojure CLI example depends on the deps.edn
file that defines all the library dependencies for the project, tools for testing and packaging the Clojure service. The -P
flag is the prepare option, a dry run that only downloads the dependencies for the given tasks.
deps: deps.edn ## Prepare dependencies for test and dist targets\n $(info --------- Download libraries to test and build the service ---------)\n clojure -P -X:test/env:package/uberjar\n
:test/env
adds libraries to run Kaocha and libraries used to run unit tests. :package/uberjar
runs a tool that creates an uberjar.
"},{"location":"automation/make/#clean-target-hiding-command-errors","title":"Clean target - hiding command errors","text":"The clean target should remove files and directories created by the build (compile) process, to ensure a consistent approach to building each time.
On Linux / Unix systems files can be deleted with the rm
command using -r
for recursively deleting a directory and its contents. -f
forces the deleting of files and directories, otherwise a prompt for confirmation of the delete may be shown.
-
before a command instructs make
to ignore an error code, useful if the files to be deleted did not exist (i.e. the build failed part way through and not all files were created).
# `-` before the command ignores any errors returned\nclean:\n $(info --------- Clean Clojure classpath cache ---------)\n - rm -rf ./.cpcache\n
"},{"location":"automation/make/#megalinter-target-simplifying-a-command","title":"MegaLinter target - simplifying a command","text":"The lint
target is an example of how the Makefile
simplifies the command line interface.
lint:
target is used to call the MegaLinter runner, avoiding the need to remember the common options passed when calling MegaLinter command line tool, mega-linter-runner
The Java flavor of MegaLinter is less than 2Gb image (full MegaLinter image is 8Gb) and contains all the tools for a Clojure project. The flavor can only be set via a command line option, so the make file ensures that option is always used and the full MegaLinter docker image is not downloaded by mistake.
When MegaLinter is configured to generate reports (default), lint-clean:
target is used to clean those reports.
# Run MegaLinter with custom configuration\nlint:\n $(info --------- MegaLinter Runner ---------)\n mega-linter-runner --flavor java --env 'MEGALINTER_CONFIG=.github/linters/mega-linter.yml'\n\nlint-clean:\n $(info --------- MegaLinter Clean Reports ---------)\n - rm -rf ./megalinter-reports\n
"},{"location":"automation/make/#enhancing-make-output","title":"Enhancing make output","text":"The info
message is used with each target to enhances the readability of the make output, especially when multiple targets and commands are involved, or if commands are generating excessive output to standard out.
test:\n $(info --------- Runner for unit tests ---------)\n ./bin/test\n
"},{"location":"automation/make/#avoiding-file-name-collisions","title":"Avoiding file name collisions","text":"Although unlikely, if a filename in the root of a project has the same name as a Makefile
target, it can be used instead of running the targets command
.PHONY:
defines the names of targets in the Makefile
to avoid name clashes
.PHONY: all lint deps test test-ci dist clean\n
phony - MakefileTutorial
"},{"location":"automation/make/#halt-on-error","title":"Halt on error","text":".DELETE_ON_ERROR:
halts any further commands if a command returns non-zero exit status. Useful as short-circuit to stop tasks when further work is not valuable, e.g. if tests fail then it may not be valuable to build the Clojure project.
.DELETE_ON_ERROR:\nall: deps test-ci dist clean\n
delete_on_error - MakefileTutorial
"},{"location":"automation/make/#references","title":"References","text":"Makefile Tutorial by Example practicalli/dotfiles Makefile
"},{"location":"automation/make/#summary","title":"Summary","text":"A Makefile
can simplify the command line interface for any task with a Clojure project (or any other language and tooling).
Using the same target names across all projects reduces the cognitive load for driving any project.
"},{"location":"clojure-cli/","title":"Clojure CLI","text":"Learn by doing To learn Clojure CLI by doing, jump to Terminal REPL or Clojure project sections to start using the Clojure CLI
Clojure CLI (command line interface) is the latest approach to working with Clojure projects, libraries and tools. Clojure CLI focuses on:
- running Clojure code (applications and tools)
- starting a REPL process (Read-Eval-Print Loop) for interactive development with a Clojure editor or a command line REPL UI.
- managing dependencies (tools.deps) -downloading from Maven and Git repositories
- building Clojure projects (using tools.build) to create deployable Clojure services
Practicalli Clojure CLI Config extends the feautres of Clojure CLI, defining aliases that add community libraries and tools.
Video commands dated but concepts remain valid"},{"location":"clojure-cli/#common-tasks-for-clojure-development","title":"Common tasks for Clojure development","text":"The Clojure CLI has several built-in tasks. Additional tasks are provided via aliases that include libraries and tools from the Clojure community, e.g. Practicalli Clojure CLI Config
REPL Reloaded
clojure -M:repl/reloaded
runs a rich terminal UI REPL prompt that includes portal data inspector, namespace reloading and library hotload tools. test
path is included to support editor test runners and dev
path for custom user namespace for custom REPL startup
Task Command Defined In Basic terminal UI REPL clojure
(or clj
if rlwrap
binary installed) Clojure CLI Enhanced terminal UI REPL (Rebel & nREPL) clojure -M:repl/rebel
or clojure -M:repl/reloaded
Practicalli Create project clojure -T:project/new :template app :name domain/appname :args '[\"+h2\"]'
Practicalli Run unit tests / watch for changes clojure -X:test/run
or clojure -X:test/watch
Practicalli Run the project (clojure.main) clojure -M -m domain.main-namespace
No Alias Find libraries (maven & git) clojure -M:search/library library-name
Practicalli Find library versions (maven) clojure -X:deps find-versions domain/library-name
CLojure CLI Download dependencies clojure -P
(plus optional execution flags with aliases) CLojure CLI Check for new dependency versions clojure -T:search/outdated
Practicalli Package library clojure -X:build/jar
and clojure -X:build/uberjar
Practicalli Deploy library locally clojure -X:deps mvn-install
Clojure CLI Check code for unused vars clojure -X:search/unused
Practicalli tools.build is recommended for packaging projects
Package with tools.build is the recommended approach to create jar and Uberjar packages of a Clojure project.
"},{"location":"clojure-cli/#execution-option-flags","title":"Execution option flags","text":"The execution option flags for the clojure
command define how to run Clojure code. The most commonly used options are:
-M
uses clojure.main and calls the -main
function of the given namespace, passing positional string arguments. -X
uses clojure.exec to call a fully qualified function which has a map argument, passing key/value pair arguments. -T
is the same as -X
execpt setting the classpath to .
, ignoring project dependencies not defined in a given alias.
Flag Purpose -A
Pass alias to built-in terminal UI REPL (clojure
or clj
) -M
Run Clojure with clojure.main -P
Prepare / dry run (Build scripts, CI servers, Containers) -X
Execute a fully qualified function, optional default arguments -T
Run a tool independently from a project configuration -J
Java Virtual Machine specific options (heap size, etc) Examples of execution option flags
Execution option page expands on flag usage with numerous examples
"},{"location":"clojure-cli/#configure-clojure-cli","title":"Configure Clojure CLI","text":"A deps.edn
file configures the Clojure CLI, using extensible data notation (EDN), the underlying syntax of Clojure itself.
Configuration is defined using a hash-map with the following top-level keys:
:deps
- library dependencies :paths
- directories to search for code and resources (Java classpath) :aliases
- named configuration defining extra paths, extra deps and configuration to run Clojure :mvn/repos
- library dependency sources, remote and local (e.g. Clojars, Maven, Artifactory, etc).
:aliases
configuration is only included when using the alias name with the Clojure CLI, e.g. :repl/rebel
alias in Practicalli Clojure CLI Config adds library dependencies only used during development to run a rich terminal UI REPL.
clojure -M:repl/rebel\n
Add a wide range of aliases by installing Practicalli Clojure CLI Config Practicalli Clojure CLI Config provides aliases for a wide range of tools for use with Clojure CLI to support Clojure software development.
"},{"location":"clojure-cli/#precedence-order","title":"Precedence Order","text":"Clojure CLI Configuration can be used from several different sources.
Configuration Description Command line arguments string or edn (key value) arguments passed to the clojure
command project deps.edn
Project specific configuration: paths, dependencies, aliases $XDG_CONFIG_HOME/clojure/deps.edn
/ $HOME/.clojure/deps.edn
User level configuration for use with all projects Clojure CLI install Includes Clojure standard library, src
path and built-in :deps
aliases Command line arguments take preceedence over the other configurations. When running the clojure
command the configurations are merged, with key/values being added or replaces following the precedence order.
"},{"location":"clojure-cli/#install-configuration","title":"Install configuration","text":"Clojure CLI install has a built-in configuration:
org.clojure/clojure
library dependency, setting the default version of Clojure for the Clojure CLI src
set as the default path
Clojure CLI Install deps.edn The Clojure CLI install includes a deps.edn
configuration, e.g. /usr/local/lib/clojure/deps.edn
{\n :paths [\"src\"]\n\n :deps {\n org.clojure/clojure {:mvn/version \"1.11.1\"}\n }\n\n :aliases {\n :deps {:replace-paths []\n :replace-deps {org.clojure/tools.deps.cli {:mvn/version \"0.9.10\"}}\n :ns-default clojure.tools.deps.cli.api\n :ns-aliases {help clojure.tools.deps.cli.help}}\n :test {:extra-paths [\"test\"]}\n }\n\n :mvn/repos {\n \"central\" {:url \"https://repo1.maven.org/maven2/\"}\n \"clojars\" {:url \"https://repo.clojars.org/\"}\n }\n}\n
Check version of Clojure Evaluate *clojure-version*
in a REPL shows which version of the Clojure language is currently being used.
Including org.clojure/clojure
as a dependency the project deps.edn
file specifies a version of the Clojure language. The Clojure CLI version is used if no other dependency is specified.
"},{"location":"clojure-cli/#user-configuration","title":"User configuration","text":"A Clojure CLI user configuration is available to all projects by the operating system user account. Practicalli Clojure CLI Config is a user configuration that contains a set of well-formed aliases that add common tools for all Clojure project.
Clojure CLI User Configuration Location Clojure CLI tools creates a configuration directory called .clojure
, which by default is placed in the root of the operating system user account directory, e.g. $HOME/.clojure
.
XDG_CONFIG_HOME
may be set by your operating system and over-rides the default location, e.g. $HOME/.config/.clojure
CLJ_CONFIG
can be used to over-ride all other location settings
Run clojure -Sdescribe
in a terminal and checking the :config-user
value to see the location of your Clojure configuration directory
A basic example of a user configuration for Clojure CLI
{\n :aliases {\n :test/env {:extra-paths [\"test\"]}\n\n :project/new\n {:extra-deps {seancorfield/clj-new {:mvn/version \"1.0.199\"}}\n :main-opts [\"-m\" \"clj-new.create\"]}\n }\n\n :mvn/repos {\n \"central\" {:url \"https://repo1.maven.org/maven2/\"}\n \"clojars\" {:url \"https://repo.clojars.org/\"}\n }\n}\n
Clojure Tools install sets Clojure version
A default version of Clojure is set by the Clojure tools install, enabling the clojure
command to know what version of Clojure library to use. This version will be over-ridden by the user or project specific deps.edn configuration files if set.
"},{"location":"clojure-cli/#references","title":"References","text":"tools.deps and cli guide clojure.main API Reference tools.deps.alpha API Reference
"},{"location":"clojure-cli/built-in-commands/","title":"Clojure CLI Built-in commands","text":"clojure
without any other arguments will run a REPL with a basic terminal prompt.
clj
is a wrapper script for the clojure
command (rlwrap
required) to add command history to the basic REPL prompt.
clojure -T:deps
to run one of several built-in commands to help work with Clojure CLI projects and libraries.
clojure --help
list the available commands.
"},{"location":"clojure-cli/built-in-commands/#deps-alias","title":":deps alias","text":"The :deps
aliases provides tools for managing library dependencies.
The Clojure CLI -X
flag is used to call these tools via clojure.exec
and take key value pairs as arguments
aliases Description clojure -X:deps list
List full transitive deps set and licenses clojure -X:deps tree
download dependencies & print dependency tree, indenting libraries that are dependencies of dependencies clojure -X:deps find-versions
Find available versions of a given library (domain/name) clojure -X:deps prep
Prepare all unprepped libs in the dep tree clojure -X:deps mvn-pom
Generate or update pom.xml with deps and paths clojure -X:deps mvn-install :jar '\"/path/to.jar\"'
install a given jar file into the local maven repository, eg. ~/.m2/repository
clojure -X:deps git-resolve-tags
update deps.edn
git based dependencies that used tags with the equivalent SHA commit values tools.deps.alpha API Reference
Libraries are downloaded if they are not in the local Maven cache, e.g. $HOME/.m2/repository
, so on first run a command may take a little time.
Use Clojure CLI -P flag to download libraries clojure -P
is the recommended way to download library dependencies as it does not run any other commands
The -P
flag can be used with aliases to download libraries for building and testing a Clojure project, especially useful for a cached environment like Docker.
clojure -P -M:dev/reloaded:project/build\n
"},{"location":"clojure-cli/built-in-commands/#list-library-dependencies","title":"List Library dependencies","text":"List library depenencies of a project, including the library version and software licence for each library.
The list includes transitive dependencies, library dependencies of the project library dependencies.
The :aliases '[:alias/name(s)]'
argument will also list library dependencies from a given alias, either project alias or user alias.
clojure -X:deps list\n
Dependency list from Practicalli Service project template \u276f clojure -X:deps list\naero/aero 1.1.6 (MIT)\namalloy/ring-buffer 1.3.1 (EPL-1.0)\naysylu/loom 1.0.2 (EPL-1.0)\nborkdude/dynaload 0.2.2 (EPL-1.0)\nborkdude/edamame 0.0.18 (EPL-1.0)\ncom.bhauman/spell-spec 0.1.2 (EPL-1.0)\ncom.brunobonacci/mulog 0.9.0 (Apache-2.0)\ncom.brunobonacci/mulog-adv-console 0.9.0 (Apache-2.0)\ncom.brunobonacci/mulog-json 0.9.0 (Apache-2.0)\ncom.cnuernber/charred 1.010\ncom.cognitect/transit-clj 1.0.324 (Apache-2.0)\ncom.cognitect/transit-java 1.0.343 (Apache-2.0)\ncom.fasterxml.jackson.core/jackson-annotations 2.12.1 (Apache-2.0)\ncom.fasterxml.jackson.core/jackson-core 2.12.1 (Apache-2.0)\ncom.fasterxml.jackson.core/jackson-databind 2.12.1 (Apache-2.0)\ncom.fasterxml.jackson.datatype/jackson-datatype-jsr310 2.12.0 (Apache-2.0)\ncom.google.javascript/closure-compiler v20151015 (Apache-2.0)\ncom.googlecode.json-simple/json-simple 1.1.1 (Apache-2.0)\ncommons-codec/commons-codec 1.15 (Apache-2.0)\ncommons-fileupload/commons-fileupload 1.4 (Apache-2.0)\ncommons-io/commons-io 2.6 (Apache-2.0)\ncrypto-equality/crypto-equality 1.0.0 (EPL-1.0)\ncrypto-random/crypto-random 1.2.0 (EPL-1.0)\nexpound/expound 0.9.0 (EPL-1.0)\nfipp/fipp 0.6.25 (EPL-1.0)\nhttp-kit/http-kit 2.6.0 (Apache-2.0)\njavax.xml.bind/jaxb-api 2.3.0 (CDDL 1.1)\nlambdaisland/deep-diff 0.0-47 (EPL-1.0)\nmeta-merge/meta-merge 1.0.0 (EPL-1.0)\nmetosin/jsonista 0.3.1 (EPL-1.0)\nmetosin/malli 0.7.5 (EPL-2.0)\nmetosin/muuntaja 0.6.8 (EPL-1.0)\nmetosin/reitit 0.5.13 (EPL-1.0)\nmetosin/reitit-core 0.5.18 (EPL-1.0)\nmetosin/reitit-dev 0.5.18 (EPL-1.0)\nmetosin/reitit-frontend 0.5.13 (EPL-1.0)\nmetosin/reitit-http 0.5.13 (EPL-1.0)\nmetosin/reitit-interceptors 0.5.13 (EPL-1.0)\nmetosin/reitit-malli 0.5.13 (EPL-1.0)\nmetosin/reitit-middleware 0.5.13 (EPL-1.0)\nmetosin/reitit-ring 0.5.13 (EPL-1.0)\nmetosin/reitit-schema 0.5.13 (EPL-1.0)\nmetosin/reitit-sieppari 0.5.13 (EPL-1.0)\nmetosin/reitit-spec 0.5.13 (EPL-1.0)\nmetosin/reitit-swagger 0.5.13 (EPL-1.0)\nmetosin/reitit-swagger-ui 0.5.13 (EPL-1.0)\nmetosin/ring-swagger-ui 3.36.0 (EPL-1.0)\nmetosin/schema-tools 0.12.3 (EPL-1.0)\nmetosin/sieppari 0.0.0-alpha13 (EPL-1.0)\nmetosin/spec-tools 0.10.5 (EPL-1.0)\nmvxcvi/arrangement 2.0.0 (Public Domain)\nmvxcvi/puget 1.1.2 (Public Domain)\norg.clojure/clojure 1.11.1 (EPL-1.0)\norg.clojure/clojurescript 1.7.170 (EPL-1.0)\norg.clojure/core.rrb-vector 0.0.14 (EPL-1.0)\norg.clojure/core.specs.alpha 0.2.62 (EPL-1.0)\norg.clojure/data.json 0.2.6 (EPL-1.0)\norg.clojure/data.priority-map 0.0.5 (EPL-1.0)\norg.clojure/google-closure-library 0.0-20151016-61277aea (Apache-2.0)\norg.clojure/google-closure-library-third-party 0.0-20151016-61277aea (Apache-2.0)\norg.clojure/java.classpath 1.0.0 (EPL-1.0)\norg.clojure/spec.alpha 0.3.218 (EPL-1.0)\norg.clojure/test.check 1.1.1 (EPL-1.0)\norg.clojure/tools.namespace 1.3.0 (EPL-1.0)\norg.clojure/tools.reader 1.3.6 (EPL-1.0)\norg.javassist/javassist 3.18.1-GA (MPL 1.1)\norg.mozilla/rhino 1.7R5 (Mozilla Public License, Version 2.0)\norg.msgpack/msgpack 0.6.12 (Apache-2.0)\nparty.donut/system 0.0.202 \nprismatic/schema 1.1.12 (EPL-1.0)\nring/ring-codec 1.1.3 (MIT)\nring/ring-core 1.9.1 (MIT)\ntailrecursion/cljs-priority-map 1.2.1 (EPL-1.0)\ntech.droit/clj-diff 1.0.1 (EPL-1.0)\n
Use the :aliases '[:alias/name(s)]'
option to also include the dependencies from a project or user alias. Showing the dependencies from an aliase can useful to identify conflicting dependencies when using an alias (unlikely but it could happen).
clojure -X:deps list :aliases '[:dev/reloaded]'\n
Dependency list from project and Practicalli :dev/reloaded alias \u276f clojure -X:deps list :aliases '[:dev/reloaded]'\naero/aero 1.1.6 (MIT)\namalloy/ring-buffer 1.3.1 (EPL-1.0)\nclj-commons/clj-yaml 1.0.27 (EPL-1.0)\ncom.brunobonacci/mulog 0.9.0 (Apache-2.0)\ncom.cognitect/transit-clj 1.0.333 (Apache-2.0)\ncom.cognitect/transit-cljs 0.8.280 (Apache-2.0)\ncom.cognitect/transit-java 1.0.371 (Apache-2.0)\ncom.cognitect/transit-js 0.8.874 (Apache-2.0)\ncom.fasterxml.jackson.core/jackson-core 2.14.2 (Apache-2.0)\ncom.google.code.gson/gson 2.10.1 (Apache-2.0)\ncom.googlecode.json-simple/json-simple 1.1.1 (Apache-2.0)\ncom.nextjournal/beholder 1.0.2 \ncriterium/criterium 0.4.6 (EPL-1.0)\ndjblue/portal 0.49.0 (MIT)\nexpound/expound 0.9.0 (EPL-1.0)\nfipp/fipp 0.6.26 (EPL-1.0)\nhawk/hawk 0.2.11 (EPL-1.0)\nhttp-kit/http-kit 2.7.0 (Apache-2.0)\nio.methvin/directory-watcher 0.17.3 (Apache-2.0)\njavax.activation/javax.activation-api 1.2.0 (CDDL/GPLv2+CE)\njavax.xml.bind/jaxb-api 2.4.0-b180830.0359 (CDDL 1.1)\nlambdaisland/clj-diff 1.4.78 (EPL-1.0)\nlambdaisland/deep-diff2 2.10.211 (EPL-1.0)\nlambdaisland/kaocha 1.87.1366 (EPL-1.0)\nlambdaisland/tools.namespace 0.3.256 (EPL-1.0)\nmeta-merge/meta-merge 1.0.0 (EPL-1.0)\nmvxcvi/arrangement 2.1.0 (Public Domain)\nnet.incongru.watchservice/barbary-watchservice 1.0 (GPLv2 + Classpath Exception)\nnet.java.dev.jna/jna 5.12.1 (LGPL-2.1-or-later)\norg.clojure/clojure 1.11.1 (EPL-1.0)\norg.clojure/core.rrb-vector 0.1.2 (EPL-1.0)\norg.clojure/core.specs.alpha 0.2.62 (EPL-1.0)\norg.clojure/data.json 2.4.0 (EPL-1.0)\norg.clojure/java.classpath 1.0.0 (EPL-1.0)\norg.clojure/spec.alpha 0.3.218 (EPL-1.0)\norg.clojure/test.check 1.1.1 (EPL-1.0)\norg.clojure/tools.cli 1.0.219 (EPL-1.0)\norg.clojure/tools.namespace 1.4.4 (EPL-1.0)\norg.clojure/tools.reader 1.3.6 (EPL-1.0)\norg.clojure/tools.trace 0.7.11 (EPL-1.0)\norg.flatland/ordered 1.15.11 (EPL-1.0)\norg.javassist/javassist 3.18.1-GA (MPL 1.1)\norg.msgpack/msgpack 0.6.12 (Apache-2.0)\norg.slf4j/slf4j-api 2.0.9 (MIT)\norg.slf4j/slf4j-nop 2.0.9 (MIT)\norg.tcrawley/dynapath 1.1.0 (EPL-1.0)\norg.yaml/snakeyaml 2.1 (Apache-2.0)\nprogrock/progrock 0.1.2 (EPL-1.0)\nslingshot/slingshot 0.12.2 (EPL-1.0)\n
The :aliases
option can be used to inspect the dependencies of a user alias, listing only dependencies from the specified aliases when run outside of a Clojure project.
Dependency list from Practicalli :repl/rebel alias only \u276f clojure -X:deps list :aliases '[:repl/rebel]'\ncider/cider-nrepl 0.42.1 (EPL-1.0)\ncider/orchard 0.18.0 (EPL-1.0)\ncljfmt/cljfmt 0.5.7 (EPL-1.0)\ncom.bhauman/rebel-readline 0.1.4 (EPL-1.0)\ncom.google.javascript/closure-compiler v20151216 (Apache-2.0)\ncompliment/compliment 0.3.6 (EPL-1.0)\nmx.cider/logjam 0.1.1 (EPL-1.0)\nnrepl/nrepl 1.1.0 (EPL-1.0)\norg.clojure/clojure 1.11.1 (EPL-1.0)\norg.clojure/clojurescript 1.7.228 (EPL-1.0)\norg.clojure/core.specs.alpha 0.2.62 (EPL-1.0)\norg.clojure/data.json 0.2.6 (EPL-1.0)\norg.clojure/google-closure-library 0.0-20151016-61277aea (Apache-2.0)\norg.clojure/google-closure-library-third-party 0.0-20151016-61277aea (Apache-2.0)\norg.clojure/spec.alpha 0.3.218 (EPL-1.0)\norg.clojure/tools.reader 1.0.0-alpha4 (EPL-1.0)\norg.fusesource.jansi/jansi 1.16 (Apache-2.0)\norg.jline/jline-reader 3.5.1 (The BSD License)\norg.jline/jline-terminal 3.5.1 (The BSD License)\norg.jline/jline-terminal-jansi 3.5.1 (The BSD License)\norg.mozilla/rhino 1.7R5 (Mozilla Public License, Version 2.0)\nrewrite-clj/rewrite-clj 0.5.2 (MIT)\nrewrite-cljs/rewrite-cljs 0.4.3 (MIT)\n
"},{"location":"clojure-cli/built-in-commands/#dependency-tree","title":"Dependency tree","text":"Show a tree hieracthy of project library dependencies, including the library name and version used.
The tree includes transitive dependencies, library dependencies of the project library dependencies.
The :aliases '[:alias/name(s)]'
argument will also list library dependencies from a given alias, either project alias or user alias.
clojure -X:deps tree\n
Dependency list from Practicalli Service project template \u276f clojure -X:deps tree\norg.clojure/clojure 1.11.1\n . org.clojure/spec.alpha 0.3.218\n . org.clojure/core.specs.alpha 0.2.62\nhttp-kit/http-kit 2.6.0\nmetosin/reitit 0.5.13\n X metosin/reitit-core 0.5.13 :superseded\n X meta-merge/meta-merge 1.0.0 :parent-omitted\n X metosin/reitit-dev 0.5.13 :use-top\n . metosin/reitit-spec 0.5.13\n X metosin/reitit-core 0.5.13 :older-version\n . metosin/spec-tools 0.10.5\n X org.clojure/spec.alpha 0.2.187 :older-version\n . metosin/reitit-malli 0.5.13\n X metosin/reitit-core 0.5.13 :older-version\n X metosin/malli 0.3.0 :older-version\n . metosin/reitit-schema 0.5.13\n X metosin/reitit-core 0.5.13 :older-version\n . metosin/schema-tools 0.12.3\n . prismatic/schema 1.1.12\n . metosin/reitit-ring 0.5.13\n X metosin/reitit-core 0.5.13 :older-version\n . ring/ring-core 1.9.1\n . ring/ring-codec 1.1.3\n . commons-codec/commons-codec 1.15\n . commons-io/commons-io 2.6\n . commons-fileupload/commons-fileupload 1.4\n X commons-io/commons-io 2.2 :older-version\n . crypto-random/crypto-random 1.2.0\n X commons-codec/commons-codec 1.6 :older-version\n . crypto-equality/crypto-equality 1.0.0\n . metosin/reitit-middleware 0.5.13\n . metosin/reitit-ring 0.5.13\n . lambdaisland/deep-diff 0.0-47\n . mvxcvi/puget 1.1.2\n X mvxcvi/arrangement 1.2.0 :older-version\n X fipp/fipp 0.6.17 :older-version\n X fipp/fipp 0.6.17 :older-version\n . org.clojure/core.rrb-vector 0.0.14\n . tech.droit/clj-diff 1.0.1\n X mvxcvi/arrangement 1.2.0 :older-version\n . metosin/muuntaja 0.6.8\n . metosin/jsonista 0.3.1\n . com.cognitect/transit-clj 1.0.324\n . com.cognitect/transit-java 1.0.343\n X com.fasterxml.jackson.core/jackson-core 2.8.7 :older-version\n . org.msgpack/msgpack 0.6.12\n . com.googlecode.json-simple/json-simple 1.1.1\n . org.javassist/javassist 3.18.1-GA\n X commons-codec/commons-codec 1.10 :older-version\n . javax.xml.bind/jaxb-api 2.3.0\n . metosin/spec-tools 0.10.5\n . metosin/reitit-http 0.5.13\n X metosin/reitit-core 0.5.13 :older-version\n . metosin/reitit-ring 0.5.13\n . metosin/reitit-interceptors 0.5.13\n . metosin/reitit-ring 0.5.13\n . lambdaisland/deep-diff 0.0-47\n . metosin/muuntaja 0.6.8\n . metosin/reitit-swagger 0.5.13\n X metosin/reitit-core 0.5.13 :older-version\n . metosin/reitit-swagger-ui 0.5.13\n . metosin/reitit-ring 0.5.13\n . metosin/jsonista 0.3.1\n X com.fasterxml.jackson.core/jackson-core 2.12.0 :older-version\n X com.fasterxml.jackson.core/jackson-databind 2.12.0 :older-version\n . com.fasterxml.jackson.datatype/jackson-datatype-jsr310 2.12.0\n X com.fasterxml.jackson.core/jackson-annotations 2.12.0 :older-version\n X com.fasterxml.jackson.core/jackson-core 2.12.0 :older-version\n X com.fasterxml.jackson.core/jackson-databind 2.12.0 :older-version\n . metosin/ring-swagger-ui 3.36.0\n . metosin/reitit-frontend 0.5.13\n X metosin/reitit-core 0.5.13 :older-version\n . metosin/reitit-sieppari 0.5.13\n X metosin/reitit-core 0.5.13 :older-version\n . metosin/sieppari 0.0.0-alpha13\n . com.fasterxml.jackson.core/jackson-core 2.12.1\n . com.fasterxml.jackson.core/jackson-databind 2.12.1\n . com.fasterxml.jackson.core/jackson-annotations 2.12.1\n . com.fasterxml.jackson.core/jackson-core 2.12.1\nmetosin/reitit-dev 0.5.18\n . metosin/reitit-core 0.5.18 :newer-version\n . meta-merge/meta-merge 1.0.0\n . com.bhauman/spell-spec 0.1.2\n . expound/expound 0.9.0\n . fipp/fipp 0.6.25\ncom.brunobonacci/mulog 0.9.0\n . amalloy/ring-buffer 1.3.1\ncom.brunobonacci/mulog-adv-console 0.9.0\n X com.brunobonacci/mulog 0.9.0 :use-top\n . com.brunobonacci/mulog-json 0.9.0\n X com.brunobonacci/mulog 0.9.0 :use-top\n . com.cnuernber/charred 1.010\naero/aero 1.1.6\nparty.donut/system 0.0.202\n . aysylu/loom 1.0.2\n . org.clojure/data.priority-map 0.0.5\n . tailrecursion/cljs-priority-map 1.2.1\n . org.clojure/clojurescript 1.7.170\n . com.google.javascript/closure-compiler v20151015\n . org.clojure/google-closure-library 0.0-20151016-61277aea\n . org.clojure/google-closure-library-third-party 0.0-20151016-61277aea\n . org.clojure/data.json 0.2.6\n . org.mozilla/rhino 1.7R5\n X org.clojure/tools.reader 0.10.0-alpha3 :older-version\n . org.clojure/tools.namespace 1.3.0\n . org.clojure/java.classpath 1.0.0\n . org.clojure/tools.reader 1.3.6\n . metosin/malli 0.7.5\n . borkdude/dynaload 0.2.2\n . borkdude/edamame 0.0.18\n X org.clojure/tools.reader 1.3.4 :older-version\n . org.clojure/test.check 1.1.1\n X fipp/fipp 0.6.24 :older-version\n . mvxcvi/arrangement 2.0.0\n
Use the :aliases
option with the Clojure CLI in the root of a project to show library dependencies for the project and the :dev/reloaded
alias which could be useful if there are library conflicts when using an alias (unlikely but it could happen).
clojure -X:deps tree :aliases '[:dev/reloaded]'\n
The :aliases
option can be used to inspect the dependencies of a user alias, listing only dependencies from the specified aliases when run outside of a Clojure project.
Dependency list from Practicalli :repl/rebel alias only \u276f clojure -X:deps tree :aliases '[:repl/rebel]'\norg.clojure/clojure 1.11.1\n . org.clojure/spec.alpha 0.3.218\n . org.clojure/core.specs.alpha 0.2.62\nnrepl/nrepl 1.1.0\ncider/cider-nrepl 0.42.1\n X nrepl/nrepl 1.0.0 :use-top\n . cider/orchard 0.18.0\n . mx.cider/logjam 0.1.1\ncom.bhauman/rebel-readline 0.1.4\n . org.jline/jline-reader 3.5.1\n . org.jline/jline-terminal 3.5.1\n . org.jline/jline-terminal 3.5.1\n . org.jline/jline-terminal-jansi 3.5.1\n . org.fusesource.jansi/jansi 1.16\n . org.jline/jline-terminal 3.5.1\n . cljfmt/cljfmt 0.5.7\n . org.clojure/tools.reader 1.0.0-alpha4\n . rewrite-clj/rewrite-clj 0.5.2\n X org.clojure/tools.reader 0.10.0 :older-version\n . rewrite-cljs/rewrite-cljs 0.4.3\n . org.clojure/clojurescript 1.7.228\n . com.google.javascript/closure-compiler v20151216\n . org.clojure/google-closure-library 0.0-20151016-61277aea\n . org.clojure/google-closure-library-third-party 0.0-20151016-61277aea\n . org.clojure/data.json 0.2.6\n . org.mozilla/rhino 1.7R5\n X org.clojure/tools.reader 1.0.0-alpha1 :older-version\n X org.clojure/tools.reader 1.0.0-alpha3 :older-version\n . compliment/compliment 0.3.6\n
"},{"location":"clojure-cli/built-in-commands/#local-library-install","title":"Local library install","text":"Add a jar file for a library to the local Maven repository, e.g. ~/.m2/repository
, making that library accessible to all other local projects.
clojure -X:deps mvn-install :jar '\"/path/to.jar\"'`\n
"},{"location":"clojure-cli/built-in-commands/#find-library-versions","title":"Find Library Versions","text":"Find the available versions of a given library in the form domain/library-name (domain is typically the company name or Git Service user or organisation name).
clojure -X:deps find-versions :lib clojure.java-time/clojure.java-time\n
"},{"location":"clojure-cli/built-in-commands/#prepare-source-dependencies","title":"Prepare Source dependencies","text":"Some dependencies will require a preparation step before they can be used on the classpath.
Projects that require preparation would have a configuration of the form:
Example
{:paths [\"src\" \"target/classes\"]\n :deps/prep-lib {:alias :build\n :fn compile\n :ensure \"target/classes\"}}\n
Including the top-level key :deps/prep-lib
tells the tools.deps classpath construction that something extra is needed to prepare this lib and that can be performed by invoking the compile function in the :build alias. Once the prepare step has been done, it should create the path \"target/classes\" and that can be checked for completion.
Add a library dependency as with any other library (git or local/root):
Example
{:deps {practicalli/library-name {:local/root \"../needs-prep\"}\n practicalli/library-name {:git/sha \"../needs-prep\"}}}\n
:deps prep
will built the library of any dependency that requires it
clojure -X:deps prep\n
"},{"location":"clojure-cli/built-in-commands/#pom-file","title":"POM file","text":"Generate or update pom.xml with deps and paths
clojure -X:deps mvn-pom\n
"},{"location":"clojure-cli/built-in-commands/#resolve-git-tags","title":"Resolve Git tags","text":"-X:deps git-resolve-tags
updates git based dependencies in the project deps.edn
file which use :git/tags key to the equivalent SHA commit values in the :git/sha
key
clojure -X:deps git-resolve-tags\n
"},{"location":"clojure-cli/built-in-commands/#references","title":"References","text":"tools.deps and cli guide clojure.main guide clojure.main API Reference tools.deps.alpha API Reference
"},{"location":"clojure-cli/clojure-style/","title":"Clojure Style","text":"Code is easier to read and work with when it is consistent format that follows common rules.
Clojure community style guide provides a common style for Clojure code. While most style recommendations are widely used, others are more contentious. Ultimately the development team for the project should define a workable set of style rules that makes them productions, ideally using much of those rules from the style guide.
A consistent format between editors also minimises version control changes not related to code design. The following format tools for clojure can all be configured to be consistent with each other (although zprint defaults will require more customisation):
- cljfmt - library, also included in Clojure LSP
- cljstyle - binary and library (re-write of cljfmt)
- zprint - binary & library
Tooling that uses the Clojure Style Guide Emacs clojure-mode
and Clojure LSP (via cljfmt) format code following the most common Clojure style guide rules, although cljfmt rules are quite strick so Practicalli disables many of them.
cljstyle default configuration follows the majority of styles and has the same defaults as cljfmt. Practicalli Clojure CLI Config tweaks a few rules to make code more readable and allow for repl design experiments.
"},{"location":"clojure-cli/clojure-style/#cljstyle","title":"cljstyle","text":"Cljstyle is a rewrite of cljfmt, designed to be easier to configure. The default rules implement many of the style rules from the Clojure community style guide and is compatible with cljfmt.
Call with the check
option to report formatting issues, providing a coloured diff view of the format changes
Call with fix
option to automatically update all Clojure files with fixes, indicating which files have changed.
Cljstyle will examine all files in the current directory and any sub-directories.
.cljstyle
configuration file in the root of the project can override the default customisation, including indentation rules.
cljstyle config used by Practicalli
Clojure App template repository contains the .cljstyle
configuration file used for all Practicalli projects
Binary Practicalli Clojure CLI ConfigMakefile Install the latest binary release from the cljstyle GitHub repository onto the operating system path, e.g. $HOME/.local/bin
cljstyle check\n
fix
option automatically updates all source code files that have format issues.
cljstyle fix\n
cljstyle can be used as a library without installing the cljstyle binary. Practicalli Clojure CLI Config defines the :format/cljstyle
alias which should be passed wither the check
or format
option
Check all the Clojure files (.clj .cljc .edn .cljs) in the current project
clojure -M:format/cljstyle\n
clojure -M:format/cljstyle!\n
Clojure Alias for cljstyle
:format/cljstyle\n{:extra-deps\n {mvxcvi/cljstyle {:git/url \"https://github.com/greglook/cljstyle.git\"\n :git/sha \"14c18e5b593c39bc59f10df1b894c31a0020dc49\"}}\n :main-opts [\"-m\" \"cljstyle.main\" \"check\"]}\n\n:format/cljstyle!\n{:extra-deps\n {mvxcvi/cljstyle {:git/url \"https://github.com/greglook/cljstyle.git\"\n :git/sha \"14c18e5b593c39bc59f10df1b894c31a0020dc49\"}}\n :main-opts [\"-m\" \"cljstyle.main\" \"fix\"]}\n
Use a Makefile to run common commands such as checking style, running tests, building uberjars, etc.
Practicalli Clojure App template repository contains an example Makefile that contains common tasks for Clojure development
This example calls the cljstyle binary, but could be changed to call the clojure -M:format/cljstyle check
and clojure -M:format/cljstyle fix
aliases instead.
# ------- Code Quality --------------- #\nformat-check: ## Run cljstyle to check the formatting of Clojure code\n $(info --------- cljstyle Runner ---------)\n cljstyle check\n\nformat-fix: ## Run cljstyle and fix the formatting of Clojure code\n $(info --------- cljstyle Runner ---------)\n cljstyle fix\n# ------------------------------------ #\n
Stage changes before automatically fixing format
Practicalli suggests staging (or committing) changes before running cljstyle fix
to easily undo undesired changes or simply confirm what changes have been made
"},{"location":"clojure-cli/clojure-style/#recommended-configuration","title":"Recommended configuration","text":"Practicalli updated the default cljstyle configuration with the following changes
Configure list indent to one character
.cljstyle :indentation\n {:enabled? true,\n :list-indent 1,\n\n }\n
Do not warn about duplicate var names (def, defn names) - excluded to stop warning about REPL experiments and design journal rich comments that contain alternative designs.
.cljstyle :vars\n {:enabled? false}\n
"},{"location":"clojure-cli/clojure-style/#cljfmt","title":"cljfmt","text":"cljfmt is not available as a separate binary, although it a fixed part of the Clojure LSP server implementation.
whist typing Clojure code, Clojure LSP will format using cljfmt rules
Define a cljfmt configuration via Clojure LSP to define rules and indentation settings for all projects.
.config/clojure-lsp/config.edn :cljfmt-config-path \"cljfmt.edn\"\n
Or specify cljfmt configuration within the Clojure LSP configuration file
.config/clojure-lsp/config.edn :cljfmt {}\n
Practicalli Clojure LSP config - LSP and cljfmt Practicalli Clojure LSP config provides an example config.edn configuration file for Clojure LSP that uses a cljfmt.edn configuration file for a minimum set of Clojure format rules
The default cljfmt rules feel overly strict and Practicalli configuration disables the more draconian rules to make code far more readable
"},{"location":"clojure-cli/clojure-style/#zprint","title":"zprint","text":"zprint is a highly configurable format tool for both Clojure code and Clojure/EDN structures, available as a library and command line tool
zprint has advanced features over cljstyle and cljfmt, although may require some additional configuration work especially to format consistently with these tools.
zprint available styles
No built-in diff option zprint requires an external diff tool to see the format changes made, as zprint only reports on the files changed and not the content of those files that has changed.
zprint can write changes to a new file and a file comparison made. Or files can be staged / committed in a local Git repository before running zprint and a Git client used to see the diff.
Once the desirable styles and configuration are established there is less need for an external diff tool, although its always useful to have a quick way to check what format tools are doing.
Binary Practicalli Clojure CLI ConfigNode.js Download zprint for Linux or MacOSX using the latest binary released on the GitHub repository
Move the binary to the executable path for the operating system, updating the name to zprint
(or use a symbolic link)
mv ~/Downloads/zprintl-1.2.5 ~/.local/bin/zprint\n
Make the binary executable chmod a+x ~/.local/bin/zprint\n
Ensure the zprint binary is working and examine the default configuration for zprint, including all default values and highlighting where non-default values are set zprint --explain-all\n
Using zprint to check the Clojure files in the current directory and list which files require formatting zprint --formatted-check *.clj\n
A more detailed zprint report checking all the Clojure files a project, including files in the route directory and all sub-directories (i.e. **/*.cjl
pattern)
zprint --list-formatted-summary-check **/*.clj **/*.edn\n
Or using short form flags zprint -lfsc **/*.clj **/*.edn\n
Update formatting for all the files in a projects, showing details of the files processed and changed
zprint -lfsw **/*.clj *.edn *.clj\n
zprint can be used as a library without installing the binary. Practicalli Clojure CLI Config defines the :format/zprint
alias which checks the format of a file and reports which files required
clojure -M:format/zprint deps.edn\n
clojure -M:format/zprint filename\n
Clojure Alias for zprint Add :format/zprint
alias to check format and :format/zprint!
to write format changes to a given file or filename pattern User or project deps.edn file
:format/zprint\n{:extra-deps {zprint/zprint {:mvn/version \"1.2.4\"}}\n :main-opts [\"-m\" \"zprint.main\"\n \"{:style :indent-only}\"\n \"--list-formatted-summary-check\"]}\n\n:format/zprint!\n{:extra-deps {zprint/zprint {:mvn/version \"1.2.4\"}}\n :main-opts [\"-m\" \"zprint.main\"\n \"{:style :indent-only}\"\n \"--list-formatted-summary-write\"]}\n
Use the alise zprint is available as an NPM package
sudo --install --global zprint-clj\n
Run zprint-clj over all Clojure files zprint-clj **/*.{clj,cljs,cljc,edn}\n
"},{"location":"clojure-cli/clojure-style/#configure-zprint","title":"Configure zprint","text":"It is assumed that the majority of format needs are met by one of the following style rule sets
{:style :indent-only}
only formats indentation, less likely to change the general style of code {:style :community}
a quite strict adhearence to the Clojure Community Guide (which Practicalli finds a little to strict)
Unless the code is really messy (e.g. not written in a clojure aware editor with live linting) then {:style :indent-only}
is a simple starting point.
If the team have adopted most if not all community styles, then {:style :community}
may be a more appropriate start point. Use --explain-all flag with the zprint command to see all the rules that are applied with a partiular style and modify as appropriate
$HOME/.zprintrc
is used for the configuration applied to all files, although this can be overridden in each project (or even as zprint comments in particular files)
zprint - GitHub repo zprint - clojars zprint - cljdoc
"},{"location":"clojure-cli/defining-aliases/","title":"Defining aliases","text":"Aliases extend the built-in functionality of Clojure CLI via community libraries and tools, either in a project specific or a user deps.edn
configuration file.
Aliases are explicitly added to the clojure
command, e.g. clojure -M:repl/rebel
to start a repl with rebel rich terminal UI.
Aliases are optional configuration that supports the development workflow.
Aliases can be used to :
- add libraries and directories to the class path
- configure how to run community tools and provide default options
Understanding Clojure CLI Execution Options Understand the execution options (exec-opts) on the command line options ensures an effective use of Clojure CLI tools.
"},{"location":"clojure-cli/defining-aliases/#configuration-file","title":"Configuration file","text":"deps.edn
is an EDN configuration file containing a single hash-map with several top-level keywords. All keywords are optional.
:paths
- directories included by default as a vector of directory names, e.g. [\"src\" \"resources\"]
:deps
- library dependencies included by default as a map (practicalli/banking-on-clojure example) :mvn/repos
- a map of repositories to download Maven dependencies, Maven Central and Clojars included by default :mvn/local-repo
to specify an alternative location for the Maven cache :aliases
- a map of optional libraries and tools, the key being the alias name and its value the configuration
The installation of Clojure CLI contains a configuration
- adds
src
and org.clojure/clojure
library - Maven Central & Clojars.org repository sources.
Configuration available to all projects (or stand-alone tools) is defined in a user deps.edn
configuration in either $XDG_CONFIG_HOME/clojure
or $HOME/.clojure
.
Project specific configuration is defined in a deps.edn
file in the root of the Clojure project.
User configuration locations If XDG_CONFIG_HOME
environment variable is set, then the user configuration is $XDG_CONFIG_HOME/clojure/deps.edn
Otherwide the user configuration is $HOME/.clojure/deps.edn
.
The CLJ_CONFIG
environment variable will be used if set.
"},{"location":"clojure-cli/defining-aliases/#alias-keys","title":"Alias keys","text":"An alias name is a keyword in Clojure, e.g. :test/env
, so the :
is an intrinsic part of the alias name.
Keys used to define an alias are:
:extra-paths
- a vector of directory names added to the project class path, e.g. [\"env/dev\" \"env/test\"]
:extra-deps
- a map of additional library dependencies, as a Maven library, Git repository or local directory {domain/library-name {:mvn/version \"1.2.33\"}}
maven library {domain/name {:git/url \"https://github.com/account-name/repository-name\" :git/sha 'ab3de67'}}
{io.github.account/repository-name {:git/tag \"2023-01-10\" :git/sha 'ab3de67'}}
{}
:main-opts
- a vector of command line options passed to clojure.main
:exec-fn
- the fully qualified name of a function to be run by clojure.exec
:exec-args
- default arguments passed to the function, over-ridden by matching argument keys specified on the command line
Keys used when defining an alias for a standalone tool which exclude the paths and dependencies defined in top-level keys.
:replace-paths
- use only the paths specified as the class path :replace-deps
- use only the libraries specified, defined as a Maven library or Git repository
alias :paths and :deps short-cuts Using :paths
and :deps
keys in an alias are short-cuts for their respective replace-paths
and :replace-deps
keywords
Using :paths
and :deps
in an alias can be very confusing and Practialli recommends using the explicit names for greater clarity
Clojure CLI -T option -T
execution option will exclude the top-level :paths
and :deps
keys
-T
sets \".\" as the path, adding only paths and libraries defined in aliases used with the execution flag
"},{"location":"clojure-cli/defining-aliases/#clojuremain-alias","title":"clojure.main alias","text":":main-opts
specifies the options passed to a clojure.main alias, using the clojure -M
execution option flag.
The value is a vector containing individual string values that represent each option, i.e. option flag and value.
-m
is used to define the fully qualified namespace in which clojure.main
should look for the -main
function.
The :main-opts
vector defines arguments that are passed to the -main
function, the same kind of arguments that would be passed via the command line.
The \"--middleware\"
argument adds cider-nrepl middleware to the nREPL server, allowing Cider and other editors complete control over the REPL. The syntax uses values wrapped in a vector.
The \"-interactive\"
argument runs an interactive REPL prompt. A headless process is run without this option.
:repl/cider\n {:extra-deps {nrepl/nrepl {:mvn/version \"0.9.0\"}\n cider/cider-nrepl {:mvn/version \"0.27.4\"}}\n :main-opts [\"-m\" \"nrepl.cmdline\"\n \"--middleware\" \"[cider.nrepl/cider-middleware]\"\n \"-interactive\"]}\n
This alias is called using the command clojure -M:repl/cider
"},{"location":"clojure-cli/defining-aliases/#clojureexec-alias","title":"clojure.exec alias","text":":exec-fn
specifies the fully qualified name of the function, using the clojure -X
execution option flag .
:exec-args
specifies a hash-map of default key/value pairs passed to the :exec-fn
function. The defaults can be overridden on the command line with respective key/value pairs.
Arguments can be nested within the :exec-args
map, especially useful on projects with several component configurations (server, database, logging) and managed by a component system (i.e Integrant)
{:aliases\n {:project/run\n {:exec-fn practicalli.service/start\n :exec-args {:http/server {:port 8080\n :join? fale}\n :log/mulog :elastic-search}}}}\n
To run with the default arguments:
clojure -X:project/run\n
Over-ride the default arguments by passing them on the command line
clojure -X:project/run '[:http/server :port]' 8888 :log/mulog :console :profile :dev\n
In this command the vector defines the path to the :port
key and over-rides the default value. :log/mulog is a top-level key which changes the log publisher type. :profile
is another top-level key that sets the environment to :dev
(e.g. to configure Integrant / Aero).
Arguments in a nested map within the alias can be traversed (as with get-in
and update-in
functions) to override the default values in the alias. So to set a different port value :
Argument keys should either be a top-level key or a vector of keys to refer to a key in a nested hash-map of arguments.
An alias can contain configuration to run both clojure.main
and clojure.exec
(useful if steadily migrating users from -M to -X approach without breaking the user experience)
"},{"location":"clojure-cli/defining-aliases/#examples","title":"Examples","text":"Practicalli Clojure CLI Config provides a wide range of aliases Practicalli Clojure CLI Config is a configuration designed to work across all Clojure projects, containing unique and meaningful alias names for ease of understanding.
"},{"location":"clojure-cli/defining-aliases/#simple-project","title":"Simple Project","text":"A new Clojure project can be made by creating a deps.edn
file and respective src
& test
directory trees.
A project deps.edn
file typically contains :path
, :deps
and :aliases
sections, although deps.edn
could start with a simple {}
empty hash-map.
{:paths [\"src\" \"resources\"]\n\n :deps\n {org.clojure/clojure {:mvn/version \"1.11.1\"}}\n\n :aliases\n {:test/env {:extra-paths [\"test\"]}}}\n
The test
path and associated libraries are added as an alias as they are not required when packaging or running a Clojure application. :path
and :deps
keys are always included by default, :aliases
are optional and only included when specified with the clojure
command, e.g. clojure -M:test/env
"},{"location":"clojure-cli/defining-aliases/#clojuremain-tool","title":"clojure.main tool","text":"The Cognitect Lab test runner included the test
directory in the class path, so test code will be included when run with this alias.
The test runner dependency is pulled from a specific commit shared on GitHub (defined as a Git SHA).
The main namespace is set to that library and the -main
function is called when using this alias.
{:aliases\n\n :test/cognitect\n {:extra-paths [\"test\"]\n :extra-deps {com.cognitect/test-runner\n {:git/url \"https://github.com/cognitect-labs/test-runner.git\"\n :git/sha \"f7ef16dc3b8332b0d77bc0274578ad5270fbfedd\"}}\n :main-opts [\"-m\" \"cognitect.test-runner\"]}\n}\n
"},{"location":"clojure-cli/defining-aliases/#clojure-exec-tool","title":"Clojure Exec tool","text":"With Clojure CLI tools version 1.10.1.697 the -X
flag was introduced using aliases with Clojure exec.
The configuration should define a fully qualified function that runs the tool.
The function should take arguments as key/value pairs as with an Edn hash-map, rather than relying on positional arguments as strings.
In this example, :exec-fn
defines the fully qualified function name that will be called. :exec-args
is a hash-map of the default key values pairs that are passed to the function as an argument.
:project/new\n {:replace-deps {seancorfield/clj-new {:mvn/version \"1.1.23\"}}\n :main-opts [\"-m\" \"clj-new.create\"] ;; deprecated\n :exec-fn clj-new/create\n :exec-args {:template lib :name practicalli/playground}\n }\n
Default arguments can be over-ridden in the command, e.g. clojure -X:project/new :template app :name practicalli/simple-application
uses a different template
Additional arguments can be sent when running the command, e.g. clojure -X:project/new :template figwheel-main :name practicalli/landing-page :args '[\"--reagent\"]'
uses the figwheel-main
template, specifies a name and :args
arguments sent to
:ns-default
can also be used to qualify the function that will be executed in an alias. :ns-default
is especially useful when there are several functions that could be called from the specific namespace.
The command line can over-ride the :exec-fn
function configuration, allowing for a default configuration that can be easily over-ridden.
Example
:project/new\n {:replace-deps {seancorfield/clj-new {:mvn/version \"1.1.226\"}}\n :main-opts [\"-m\" \"clj-new.create\"] ;; deprecated\n :ns-default clj-new\n :exec-fn create\n :exec-args {:template lib :name practicalli/playground}\n }\n
Keyword naming
Alias names are a Clojure keyword, which can be qualified to provide context, e.g. :project/create
.
Aliases are composable (chained together) and their path and deps configurations merged:
clojure -M:task/path:task/deps:build/options\n
When multiple aliases contain a :main-opts
configurations they are not merged, the configuration in the last alias is used
"},{"location":"clojure-cli/defining-aliases/#resources","title":"Resources","text":"clj-exec: insideclojure.org clj-exec update: insideclojure.org Clojure CLI execution options Tips for designing aliases
"},{"location":"clojure-cli/design-journal/","title":"Design Journal","text":"A design journal captures with code and comments the decisions taken for a project, invaluable to anyone trying to get up to speed with a project.
"},{"location":"clojure-cli/design-journal/#using-a-design-journal-namespace","title":"Using a design-journal namespace","text":"A single namespace encourages the journal to flow as the design of the application flows. So onboarding onto a project is essentially reading and evaluating code from top to bottom.
"},{"location":"clojure-cli/design-journal/#using-comment-blocks","title":"Using comment blocks","text":"It is useful to include examples of how you expect key parts of the system to be called and the kind of arguments they receive. Placing these examples in a (comment ,,,) expression ensures they are not accidentally evaluated whilst showing the most important function calls in a particular namespace.
"},{"location":"clojure-cli/execution-options/","title":"Clojure CLI Execution options","text":"Execution options (-A
-M
-P
-T
-X
) define how aliases are used with the Clojure CLI. Aliases are included via one of these execution options and each option can affect how the alias is used.
Clojure CLI design evolution The first documented released used the -A
execution option to include aliases.
The design has evolved to provide specific execution options to run code via clojure.main (-M
) and clojure.exec (-X
).
In July 2021 the ability to run tools (-T
) independent from the Clojure project classpath was introduced.
"},{"location":"clojure-cli/execution-options/#quick-summary","title":"Quick summary","text":"-M
uses clojure.main
to call the -main
function of the specified namespace, passing string-based arguments.
-X
uses clojure.exec
to call a fully qualified function, passing arguments as key and value pairs
-T
runs a tool independent from project dependencies. Only the libraries in the alias are included in the Class Path. The path is defined as \".\"
by default.
-P
downloads library dependencies, including those from specified aliases
-A
in the specific case of running a basic terminal UI REPL with the clojure
command or clj
wrapper.
"},{"location":"clojure-cli/execution-options/#clojuremain","title":"clojure.main","text":"-M
flag instructs Clojure CLI tools to use clojure.main
to run Clojure code.
The --main
or -m
flag is an argument to clojure.main
which specifies the namespace to search for a -main
function.
clojure.main/main
function searches for a -main
function in the given namespace, e.g. --main pracicalli.gameboard.service
If the -main function is not found or the namespace is not specified, then the clojure
command will run a REPL session.
Run a project with the main namespace practicalli.sudoku-solver
, without any additional aliases on the command line
clojure -M -m practicalli.sudoku-solver\n
Add :project/run
alias to the project deps.edn
file to provide a simpler way to run the project on the command line
:project/run {:main-opts [\"--main\" \"practicalli.sudoku-solver\"]}\n
Now the project code can be run using the simple command line form
clojure -M:project/run\n
"},{"location":"clojure-cli/execution-options/#using-clojuremain","title":"Using clojure.main","text":"clojure.main
namespace has been the way Clojure code was run (including a REPL) for most of its history. This is now evolving with the addition of clojure.exec. clojure.main has other features, as covered in the REPL and main entrypoints article) on clojure.org.
"},{"location":"clojure-cli/execution-options/#rebel-rich-terminal-ui","title":"Rebel rich terminal UI","text":"Rebel readline provides a terminal UI REPL, providing auto-completion, function signatures, documentation, etc.
:repl/rebel
is an alias that includes nrepl, cider-nrepl and rebel-readline libraries, with a :main-opts
to run the rebel-readline.main/-main
function via clojure.main
.
:repl/rebel\n{:extra-deps {nrepl/nrepl {:mvn/version \"0.9.0\"}\n cider/cider-nrepl {:mvn/version \"0.28.2\"}\n com.bhauman/rebel-readline {:mvn/version \"0.1.4\"}}\n :main-opts [\"-m\" \"nrepl.cmdline\"\n \"--middleware\" \"[cider.nrepl/cider-middleware]\"\n \"--interactive\"\n \"-f\" \"rebel-readline.main/-main\"]}\n
Use the :repl/rebel
alias with the -M
execution option
clojure -M:repl/rebel\n
Multiple aliases can be specified to include additional paths and libraries. Aliases chained together have their configuration merged
:env/dev
adds \"dev\" as an extra path, with the dev/user.clj
file automatically loading its code into the user
namespace when the REPL starts
:lib/hotload
alias adds the org.clojure/tools.deps.alpha
library to provide hotloading of dependencies into the running REPL
Start a REPL process with this alias
clojure -M:env/dev:lib/hotload:repl/rebel\n
The Rebel REPL UI will start, include the dev directory on the class path and the org.clojure/tools.deps.alpha
library loaded into the REPL
"},{"location":"clojure-cli/execution-options/#chaining-aliases","title":"Chaining aliases","text":"Alises can be used together by chaining their names on the command line
clojure -M:env/dev:lib/hotload:repl/rebel\n
The clojure
command will merge the :extra-paths
and :extra-deps
values from each alias in the chain.
The :main-opts
values from the aliases are not merged. Only the :main-opts
value from the last alias in the chain is used with clojure.main
to run the Clojure code.
If the command line includes the -m
flag with a namespace, then that namespace is passed to clojure.main
, ignoring all :main-opts
values from the aliases. The -i
and -e
flags for clojure.main also replace :main-opts
values.
"},{"location":"clojure-cli/execution-options/#clojureexec","title":"clojure.exec","text":"-X
flag provides the flexibility to call any fully qualified function, so Clojure code is no longer tied to -main
Any function on the class path can be called and is passed a hash-map as an argument. The argument hash-map is either specified in an alias using :exec-args
or assembled into a hash-map from key/value pairs on the command line. Key/values from the command line are merged into the :exec-args
map if it exists, with the command line key/values taking precedence.
"},{"location":"clojure-cli/execution-options/#clojureexec-arguments","title":"clojure.exec arguments","text":"Clojure.exec command takes key value pairs read as EDN values (extensible data notation that is the base syntax of Clojure).
Number values and keywords can be parsed from the command line
Arguments that are vectors and hash maps should be wrapped in single quotes to avoid the command line shell splitting arguments at spaces, e.g. '[:a :b]'
, '{:c 1}'
.
The double quotes in an EDN string must be wrapped by single quotes, along with vectors and hash-maps
'\"strings in double quotes surround by single quotes\"'
'[:vectors :with-single-quotes]'
'{:hash-maps :with-single-quotes}'
"},{"location":"clojure-cli/execution-options/#clojureexec-examples","title":"clojure.exec examples","text":"Call the status
function from the namespace practicalli.service
, which is on the classpath in the practicalli.service project
clojure -X practicalli.service/status\n
Pass arguments to a start
function in the practicalli.service
namespace
clojure -X practicalli.service/start :port 8080 :join? false\n
As the arguments are key/value pairs, it does not matter in which order the pairs are used in the command line.
"},{"location":"clojure-cli/execution-options/#built-in-functions","title":"Built in functions","text":"Clojure CLI tools has some built in tools under the special :deps
alias (not to be confused with the :deps
configuration in a deps.edn
file)
-X:deps mvn-install
- install a maven jar to the local repository cache -X:deps find-versions
- Find available versions of a library -X:deps prep
- prepare source code libraries in the dependency tree
See clojure --help
for an overview or man clojure
for detailed descriptions
"},{"location":"clojure-cli/execution-options/#run-a-tool","title":"Run a Tool","text":"-T
install, run and remove a tool, by the tool name or an alias.
The -T
execution option also uses the clojure.exec
approach, although the :deps
and :path
values from a project deps.edn
file are ignored. This isolates the tool from the dependencies in a Clojure project.
Calling Tools on the command line has the general form:
clojure -Ttool-name function-name :key \"value\" ,,,\n
A tool may provide many functions, so the specific function name is provided when calling the tool.
key/value pairs can be passed as arguments to that function (as with the -X execution option)
-Ttools
is a built-in tool to install
and remove
other tools, with the :as
directive providing a specific name for the tool.
In this example, the antq tool is installed using the name antq
clojure -Ttools install com.github.liquidz/antq '{:git/tag \"1.3.1\"}' :as antq\n
Installing a tool adds an EDN configuration file using the name of the tool in $XDG_HOME/.clojure/tools/
or $HOME/.clojure/tools/
directory.
Once a tool is installed, run by using the name of the tool.
clojure -Tantq outdated\n
Options to the tool are passed as key/value pairs (as the tool is called by clojure.exec)
clojure -Tantq outdated :upgrade true\n
-Ttools remove
will remove the configuration of the tool of the given name
clojure -Ttools remove :tool antq\n
"},{"location":"clojure-cli/execution-options/#tools-install-or-aliases","title":"Tools install or aliases","text":"Tools can also be defined in an alias with :exec-fn
can be run via -T:alias-name
as they are both executed using clojure.exec
.
-X
execution option can emulate -T
behaviour when an alias uses :replace-paths
and :replace-deps
keys, instead of :extra-paths
and :extra-deps
, so project paths and dependencies are not included loaded by the alias.
Using an alias for a tool has the advantage allowing a use to define their preferred default arguments that are passed to the :exec-fn
, using the :exec-args
key.
Default arguments could be included in the deps.edn
of the installed tool itself, although this is controlled by the developer of that tool project.
The :search/outdated
alias defined in the practicalli/clojure-deps-edn
user level configuration is an example of a tool alias with default arguments
:search/outdated\n {:replace-paths [\".\"]\n :replace-deps {com.github.liquidz/antq {:mvn/version \"1.3.1\"}\n org.slf4j/slf4j-nop {:mvn/version \"1.7.32\"}}\n :main-opts [\"-m\" \"antq.core\"]\n :exec-fn antq.tool/outdated\n :exec-args {:directory [\".\"] ; default\n :exclude [\"com.cognitect/rebl\"\n \"org.openjfx/javafx-base\"\n \"org.openjfx/javafx-controls\"\n \"org.openjfx/javafx-fxml\"\n \"org.openjfx/javafx-swing\"\n \"org.openjfx/javafx-web\"]\n ;; :focus [\"com.github.liquidz/antq\"]\n :skip [\"boot\" \"leiningen\"]\n :reporter \"table\" ; json edn format\n :verbose false\n :upgrade false\n :force false}}\n
This alias is called using clojure -T:search/outdated
and is the same as calling clojure -Tantq outdated ,,, ,,,
with a long list of key value options that represent the arguments in the alias.
As the output is a table of results, the command output is typically pushed to a file: clojure -T:search/outdated > outdated-2021-12-24.txt
Example tools include
- liquidz/antq - search dependencies for newer library versions
- seancorfield/deps-new - create new projects using templates
- clojure-nvd - check dependencies against National Vunerability Database
"},{"location":"clojure-cli/execution-options/#prepare-dependencies","title":"Prepare dependencies","text":"-P
flag instructs the clojure
command to download all library dependencies to the local cache and then stop without executing a function call.
The -P
flag is often used with Continuous Integration workflows and to create pre-populated Container images, to avoid repeatedly downloading the same library jar files.
If used with just a project, then the Maven dependencies defined in the project deps.edn
file will be downloaded, if not already in the users local cache (~/.m2/repository/
).
If :git
or :local/root
dependencies are defined, the respective code will be downloaded and added to the classpath.
Prepare flag by itself download dependencies defined in the :deps
section of the deps.edn
file of the current project.
clojure -P\n
Including one or more aliases will preparing all the dependencies from every alias specified
clojure -P -M:env/dev:lib/hotload:repl/cider\n
-P
flag must be used before any subsequent arguments, i.e. before -M
, -X
, -T
As prepare is essentially a dry run, then the clojure
command does not call :main-opts
or :exec-fn
functions, even if they exist in an alias or on the command line.
-P
will warn if a project has dependencies that require building from source (i.e Java code) or resource file manipulation. If so then clojure -X:deps prep
will prepare these source based dependencies.
"},{"location":"clojure-cli/execution-options/#built-in-terminal-ui-repl","title":"Built-in terminal UI REPL","text":"-A
is stated as the official way to include an alias when running a REPL terminal UI clojure
or clj
.
Practicalli recommends using Rebel Readline which uses -M execution option, so -A execution option is rarely used by Practicalli.
The :env/dev
alias adds \"dev\" directory to the class path, typically used to add a user.clj
that will automatically load code from the user
namespace defined in that file.
clojure -A:env/dev\n
The alias definition is :env/dev {:extra-paths [\"dev\"]}
Aliases can be chained together and their configuration will be merged
:lib/hotload
adds a dependency to provide hotloading of other dependencies
:lib/hotload\n{:extra-deps {org.clojure/tools.deps.alpha\n {:git/url \"https://github.com/clojure/tools.deps.alpha\"\n :sha \"d77476f3d5f624249462e275ae62d26da89f320b\"}\n org.slf4j/slf4j-nop {:mvn/version \"1.7.32\"}}}\n
Start a REPL process with this alias
clojure -A:env/dev:lib/hotload\n
Use -M for alias definitions including :main-opts
Using an alias that contains a :main-opts
key with -A
will fail to run a REPL and print a warning to use -M
execution option The :main-opts
configuration for -A
execution option is deprecated (although currently works in 1.10.x). To run Clojure code via clojure.main
the -M
option should be with aliases that includes :main-opts
.
"},{"location":"clojure-cli/execution-options/#summary","title":"Summary","text":"There are many options when it comes to running Clojure CLI tools that are not covered here, however, this guide gives you the most common options used so far.
Practicalli recommends using the -X
execution option where possible, as arguments follow the data approach of Clojure design.
The -J
and :jvm-opts
are useful to configure the Java Virtual machine and deserve an article to themselves as there are many possible options.
The -T
tools is an exciting and evolving approach and it will be interesting to see how the Clojure community adopt this model.
See the Deps and CLI Reference Rationale for more details and description of these options.
"},{"location":"clojure-cli/execution-options/#references","title":"References","text":" - Inside Clojure - clj exec
- Inside Clojure - clj exec update
"},{"location":"clojure-cli/practicalli-config/","title":"Practicalli Clojure CLI Configuration","text":" Practicalli Clojure CLI Config
Practicalli Clojure CLI Config is a user configuration for Clojure CLI tools providing a range of community tools via meaningful aliases, supporting Clojure and ClojureScript development.
Alias names are designed with qualified keywords that provide context for the use of an alias (env
, inspect
, project
, repl
, search
test
). These keywords help with discovery and reduce cognitive load required to remember their purpose.
Commonly used arguments are included in many alias via :main-opts
or :exec-args
which can be overridden on the command line.
Minimum Clojure CLI Version - 1.10.3.1040 Clojure CLI version 1.10.3.1040 is the minimum version, although the latest available version is recommended.
Check the version of Clojure CLI currently installed via clojure --version
or clojure -Sdescribe
Remote Environments or Continuous Integration For remote environments or Continuous Integration services, include Practicalli Clojure CLI Config) in the environment build or copy specific aliases to the Clojure project deps.edn
configuration.
"},{"location":"clojure-cli/practicalli-config/#install","title":"Install","text":"Fork or clone Practicalli Clojure CLI Config GitHub repository, first removing the $XDG_CONFIG_HOME/clojure
or $HOME/.clojure
directory if they exist.
Check Clojure CLI configuration location Check the location of your Clojure configuration directory by running clojure -Sdescribe
and checking the :user-config
value.
Free Desktop XDG CONFIGClassic Config If XDG_CONFIG_HOME
environment variable is set, clone the repository to $XDG_CONFIG_HOME/clojure
git clone https://github.com/practicalli/clojure-deps-edn.git $XDG_CONFIG_HOME/clojure\n
Clojure CLI will look for its configuration in $HOME/.clojure
directory if $XDG_CONFIG_HOME
and CLJ_CONFIG
environment variables not set.
git clone https://github.com/practicalli/clojure-deps-edn.git $HOME/.clojure\n
"},{"location":"clojure-cli/practicalli-config/#community-tools","title":"Community Tools","text":"The Clojure configuration directory contains a deps.edn
file containing a substantial :aliases
section with a long list of aliases. These aliases are described in the README of the project.
All tools are provided via libraries and are only installed on first use. Unused aliases will therefore not install their libraries.
Aliases to start with
Start with the following aliases to keep things simple
clojure -T:project/create :name domain/project-name
to create a new clojure project
clojure -M:repl/reloaded
to run a fully loaded REPL and rich terminal UI (which can be connected to from Clojure editors)
clojure -X:test/watch
to run tests on file save (or :test/run
to manually run tests once)
Use Clojure tools.build to create jar and uberjar packages of the project.
"},{"location":"clojure-cli/practicalli-config/#repl-experience","title":"REPL experience","text":"Rebel REPL terminal UI provides a feature rich REPL prompt experience, far beyond the basic clj
command.
Command Description clojure -M:repl/rebel
Rebel terminal UI clojure -M:env/dev:repl/rebel
Rebel including deps & path from :env/dev
alias to configure REPL start clojure -M:repl/reloaded
Rebel with dev
& test
paths, library hotload, namespace reload, portal data inspector clojure -M:repl/rebel-cljs
Run a ClojureScript REPL using Rebel Readline :repl/help
in the REPL for help and available commands. :repl/quit
to close the REPL.
"},{"location":"clojure-cli/practicalli-config/#clojure-projects","title":"Clojure Projects","text":" - Create Clojure CLI specific projects using deps-new
- Create projects from deps, leiningen and boot templates with clj-new
Command Description clojure -T:project/create
library project called playground clojure -T:project/create :template app :name practialli/service
Clojure CLI project from app template clojure -T:project/new :template luminus :name practicalli/full-stack-app +http-kit +h2
Luminus project with given name and template options"},{"location":"clojure-cli/practicalli-config/#run-projects","title":"Run projects","text":"Run project with or without an alias:
clojure -M:alias -m domain.app-name\nclojure -M -m domain.app-name\n
The -M
flag is required even if an alias is not included in the running of the application. A warning will be displayed if the -M
option is missing.
In the project deps.edn file it could be useful to define an alias to run the project, specifying the main namespace, the function to run and optionally any default arguments that are passed to that function.
:project/run\n{:ns-default domain.main-namespace\n :exec-fn -main\n :exec-args {:port 8888}}\n
Then the project can be run using clojure -X:project/run
and arguments can optionally be included in this command line, to complement or replace any default arguments in exec-args
.
"},{"location":"clojure-cli/practicalli-config/#project-dependencies","title":"Project dependencies","text":"Command Description clojure -M:project/errors
detailed report of compilation errors for a project clojure -M:search/libraries library-name
fuzzy search Maven & Clojars clojure -M:search/libraries -F:merge library-name
fuzzy search Maven & Clojars and save to project deps.edn clojure -M:search/outdated
report newer versions for maven and git dependencies clojure -M:search/unused-vars
search and remove unused vars"},{"location":"clojure-cli/practicalli-config/#project-deployment","title":"Project Deployment","text":"Deploy a project archive file locally or to Clojars.org
Package projects into jars using tools.build
Clojure tools.build is the recommended way to create library jar files and application Uberjar files.
Command Description clojure -X:deps mvn-install project.jar
[NEW] deploy jar file to local maven repository, i.e. ~/.m2/repository
clojure -M:project/clojars project.jar
deploy jar file to Clojars clojure -M:project/clojars-signed project.jar
deploy signed jar file to Clojars Set Clojars username/token in CLOJARS_USERNAME
and CLOJARS_PASSWORD
environment variables.
Set fully qualified artifact-name and version in project pom.xml
file
Path to project.jar can also be set in alias to simplify the Clojure command.
clojure -X:deps mvn-install project.jar
for local deployment of jars is part of the 1.10.1.697 release of the Clojure CLI tools in September 2020.
"},{"location":"clojure-cli/practicalli-config/#java-sources","title":"Java Sources","text":"Include Java source on the classpath to look up Java Class and method definitions, e.g. cider-find-var
in Emacs Requires: Java sources installed locally (e.g. \"/usr/lib/jvm/openjdk-11/lib/src.zip\")
Use the aliases with either -M
or -X
flags on the Clojure command line.
"},{"location":"clojure-cli/practicalli-config/#format-tools","title":"Format tools","text":"Use formatting tools to support a consistent code style across all Clojure projects
Command Description clojure -M:format/cljstyle check / fix
Check or fix code style (cljstyle) clojure -M:format/cljfmt check / fix
Check or fix code style (cljfmt) clojure -M:format/zprint filename
Format file using zprint Include :lib/pprint-sorted
when starting a REPL to pretty print data with sorted keys and set values
"},{"location":"clojure-cli/practicalli-config/#databases-and-drivers","title":"Databases and drivers","text":"Databases and drivers, typically for development time inclusion such as embedded databases
:database/h2
- H2 embedded database library and next.jdbc lib/next.jdbc
- include the next.jdbc library
clojure -M:database/h2
- run a REPL with an embedded H2 database and next.jdbc libraries
https://cljdoc.org/d/seancorfield/next.jdbc/CURRENT/doc/getting-started#create--populate-a-database
Use the aliases with either -M
or -X
flags on the Clojure command line.
"},{"location":"clojure-cli/practicalli-config/#data-science","title":"Data Science","text":" lib/clerk
- Clerk Notebooks
"},{"location":"clojure-cli/practicalli-config/#visualizing-projects","title":"Visualizing projects","text":"Create Graphviz graphs of project and library dependencies
Morpheus creates grahps of project vars and their relationships
:graph/vars
- generate graph of vars in a project as a .dot file :graph/vars-png
- generate graph of vars in a project as a .png file using src
and test
paths :graph/vars-svg
- generate graph of vars in a project as a .svg file using src
and test
paths
Install Graphviz to generate PNG and SVG images. Or use the Edotor website to convert .dot files to PNG or SVG images and select different graph layout engines.
Vizns creates graphs of relationships between library dependencies and project namespaces
:graph/deps
:graph/deps-png
- generate a single deps-graph png image
Other options: - clojure -M:graph/deps navigate
# navigable folder of SVGs - clojure -M:graph/deps single
# deps-graph.dot file - clojure -M:graph/deps single -o deps-graph.png -f png
- clojure -M:graph/deps single -o deps-graph.svg -f svg
- clojure -M:graph/deps single --show
# View graph without saving
"},{"location":"clojure-cli/practicalli-config/#data-inspector","title":"Data Inspector","text":"Portal Navigate data in the form of edn, json and transit
Practicalli Clojure - data browsers section - portal
Command Description clojure -M:inspect/portal-cli
Clojure REPL with Portal dependency clojure -M:inspect/portal-web
ClojureScript web browser REPL with Portal dependency clojure -M:inspect/portal-node
ClojureScript node.js REPL with Portal dependency Using Portal once running
(require '[portal.api :as portal])
once the REPL starts. For inspect/portal-web
use (require '[portal.web :as portal])
instead
(portal/open)
to open the web based inspector window in a browser.
(portal/tap)
to add portal as a tap target (add-tap)
(tap> {:accounts [{:name \"jen\" :email \"jen@jen.com\"} {:name \"sara\" :email \"sara@sara.com\"}]})
to send data to the portal inspector window (or any other data you wish to send)
(portal/clear)
to clear all values from the portal inspector window.
(portal/close)
to close the inspector window.
"},{"location":"clojure-cli/practicalli-config/#clojure-specification","title":"Clojure Specification","text":"Clojure spec, generators and test.check
:lib/spec-test
- generative testing with Clojure test.check :lib/spec2
- experiment with the next version of Clojure spec - alpha: design may change
"},{"location":"clojure-cli/practicalli-config/#unit-testing-frameworks","title":"Unit Testing frameworks","text":"Unit test libraries and configuration. The Clojure standard library includes the clojure.test
namespace, so no alias is required.
:test/env
- add test
directory to classpath :lib/expectations
- clojure.test
with expectations :lib/expectations-classic
- expectations framework
Use expectations in a project clojure -M:test:expectations
or from the command line with a test runner, e.g. clojure -M:lib/expectations:test/runner
"},{"location":"clojure-cli/practicalli-config/#test-runners-and-test-coverage","title":"Test runners and Test Coverage","text":"Tools to run unit tests in a project which are defined under test
path.
Run clojure with the specific test runner alias: clojure -M:test-runner-alias
Command Description clojure -M:test/run
Kaocha test runner for Clojure clojure -M:test/watch
Kaocha: watch for changes clojure -M:test/cljs
Kaocha test runner for ClojureScript"},{"location":"clojure-cli/practicalli-config/#lint-tools","title":"Lint tools","text":"Static analysis tools to help maintain code quality and suggest Clojure idioms.
Command Description clojure -M:lint/clj-kondo
comprehensive and fast static analysis lint tool clojure -M:lint/eastwood
classic lint tool for Clojure clojure -M:lint/idiom
Suggest idiomatic Clojure code"},{"location":"clojure-cli/practicalli-config/#performance-testing","title":"Performance testing","text":":performance/benchmark
alias includes the Criterium library for performance testing of Clojure expressions.
Use the aliases with either -M
or -X
flags on the Clojure command line.
:dev/reloaded
and :repl/reloaded
both include criterium library
Start a REPL using the :repl/reloaded
alias, or by including the :performance/benchmark
in a Clojure command to start a REPL.
Repl Reloaded ```shell clojure -M:repl/reloaded
```
Clojure command ```shell clojure -M:performance/benchmark:repl/basic
```
REPL Require the Criterium quick-bench
function
(require '[criterium.core :refer [quick-bench]])\n
(quick-bench (adhoc-expression))\n
Performance test a project in the REPL
clojure -M:performance/benchmark:repl/rebel\n\n(require '[practicalli/namespace-name]) ; require project code\n(in-ns 'practicalli/namespace-name)\n(quick-bench (project-function args))\n
Use the aliases with either -M
or -X
flags on the Clojure command line.
In the REPL:
(require '[clj-memory-meter.core :as memory-meter])\n (memory-meter/measure (your-expression))\n
"},{"location":"clojure-cli/repl-reloaded/","title":"Practicalli REPL Reloaded Workflow","text":"An effective REPL workflow is central to Clojure development. Practicalli REPL Reloaded workflow provides a rich set of tools and minimises the need to restart the REPL
- custom REPL startup using
dev/user.clj
- continually run unit tests with Kaocha
- event log and publisher with mulog
- visualise & navigate evaluation data and logs with Portal
- hotload libraries without restarting the REPL with
clojure.repl.deps
(Clojure 1.12) - reload changed namespaces to manage large code refactor with
tools.namespace
- performance testing code expressions with time & Criterium
"},{"location":"clojure-cli/repl-reloaded/#start-the-repl","title":"Start the REPL","text":"Start a Clojure REPL with the :repl/reloaded
alias (or include :dev/reloaded
alias in an Editor jack-in command or other REPL startup command).
Aliases are defined in Practicalli Clojure CLI Config
Start a rich terminal UI repl and the REPL Reloaded tools
clojure -M:repl/reloaded\n
A Rebel rich terminal UI REPL prompt provides direct evaluation in the REPL (with autocomplete, documentation, signature hints and multi-line editing)
An nREPL server is started to allow connections from a range of Clojure editors.
Portal Inspector window opens and is connected to all evaluation results and Mulog events that occur.
Rebel REPL Teminal UI
Example Alias Definitions Start a REPL process with an nREPL server to connect Clojure editors. Providing a Rebel rich terminal UI with tools to hotload libraries, reload namespaces and run Portal data inspector. The alias also includes a path for custom REPL startup and a path to access unit test code, along with a test runner.
:repl/reloaded\n{:extra-paths [\"dev\" \"test\"]\n :extra-deps {nrepl/nrepl {:mvn/version \"1.0.0\"}\n cider/cider-nrepl {:mvn/version \"0.30.0\"}\n com.bhauman/rebel-readline {:mvn/version \"0.1.4\"}\n djblue/portal {:mvn/version \"0.35.1\"}\n org.clojure/tools.namespace {:mvn/version \"1.4.1\"}\n org.slf4j/slf4j-nop {:mvn/version \"2.0.6\"}\n com.brunobonacci/mulog {:mvn/version \"0.9.0\"}\n lambdaisland/kaocha {:mvn/version \"1.77.1236\"}\n org.clojure/test.check {:mvn/version \"1.1.1\"}\n ring/ring-mock {:mvn/version \"0.4.0\"}\n criterium/criterium {:mvn/version \"0.4.6\"}}\n :main-opts [\"-m\" \"nrepl.cmdline\"\n \"--middleware\" \"[cider.nrepl/cider-middleware,portal.nrepl/wrap-portal]\"\n \"--interactive\"\n \"-f\" \"rebel-readline.main/-main\"]}\n\n:dev/reloaded\n{:extra-paths [\"dev\" \"test\"]\n :extra-deps {djblue/portal {:mvn/version \"0.35.1\"}\n org.clojure/tools.namespace {:mvn/version \"1.4.1\"}\n org.slf4j/slf4j-nop {:mvn/version \"2.0.6\"}\n com.brunobonacci/mulog {:mvn/version \"0.9.0\"}\n lambdaisland/kaocha {:mvn/version \"1.77.1236\"}\n org.clojure/test.check {:mvn/version \"1.1.1\"}\n ring/ring-mock {:mvn/version \"0.4.0\"}\n criterium/criterium {:mvn/version \"0.4.6\"}}}\n
Include the :dev/reloaded
or :lib/hotload
aliases when starting the REPL with other aliases, using any of the available Clojure CLI execution options (-A
,-M
,-X
,-T
).
Alias example from Practicalli Clojure CLI Config
Clojure 1.11 Hotload Support To support Clojure 1.11.x, add an :lib/hotload
alias for the clojure.tools.deps.alpha.repl
library using the latest SHA commit from the add-libs3 branch of clojure.tools.deps.alpha
library as an extra dependency.
The add-libs
code is on a separate , so requires the SHA from the head of add-libs3 branch
:lib/hotload\n {:extra-deps {org.clojure/tools.deps.alpha\n {:git/url \"https://github.com/clojure/tools.deps.alpha\"\n :git/sha \"e4fb92eef724fa39e29b39cc2b1a850567d490dd\"}}}\n
Include the :dev/reloaded
or :lib/hotload
aliases when starting the REPL with other aliases, using any of the available Clojure CLI execution options (-A
,-M
,-X
,-T
). Alias example from Practicalli Clojure CLI Config
"},{"location":"clojure-cli/repl-reloaded/#custom-repl-startup","title":"Custom REPL startup","text":"A Clojure REPL starts in the user
namespace. When a user.clj
file is on the classpath its code is loaded (evaluated) into the REPL during startup.
Create a dev/user.clj
file with libraries and tools to support development and add the dev
directory to the classpath.
Create a custom REPL Startup with dev/user.clj
"},{"location":"clojure-cli/repl-reloaded/#reload-namespaces","title":"Reload namespaces","text":"As code and design evolves, expressions evaluated in the REPL may become stale especially when the names (symbols, vars) bound to function definitions are renamed or deleted from the source code. Rather than restart the REPL process and loose all the state, one or more namespaces can be refreshed.
Remove function definitions before renaming To minimise the need to reload namespaces, undefine function definitions (unbind their name to the function) before changing the names of the function.
Remove a symbol from the namespace, using *ns*
which is dynamically bound to the current namespace
(ns-unmap *ns* 'function-or-def-name)\n
Remove a specific namespace (any functions defined in the namespace are no longer accessible - illegalStateException - Attempting to call unbound function) (remove-ns 'practicalli.service.utils)\n
Remove an alias for a specific namespace (ns-unalias 'practicalli.service.utils 'utils)\n
Clojure editors may provide commands to undefine a function definition, e.g. Emacs CIDER includes cider-undef
to remove the current symbol via nREPL commands
clojure.tools.namespace.repl contains the refresh
function that compares source code files with the definitions in the REPL, removing and re-evaluating those namespaces containing changes.
refresh will manage loading of namespaces with respect to their dependencies, ensuring each namespace can be loaded without error.
Require the clojure.tools.namespace.repl refresh function
REPLProject (require '[clojure.tools.namespace.repl :refer [refresh]])\n
Use an ns form for the namespace (often added to a custom user
namespace)
(ns user\n (:require [clojure.tools.namespace.repl :refer [refresh]]))\n
Or in a rich comment expression
(comment\n (require '[clojure.tools.namespace.repl :refer [refresh]]))\n
Refresh the namespaces that have saved changes
(refresh)\n
A list of refreshed namespaces are printed. If there are errors in the Clojure code, then a namespace cannot be loaded and error messages are printed. Check the individual code expressions in the namespace to ensure they are correctly formed.
Reload namespaces with dev/user.clj
Handling Errors If an exception is thrown while loading a namespace, refresh stops and prints the namespace that caused the exception. (clojure.repl/pst) prints the rest of the stack trace
*e
is bound to the exeception so will print the exeception when evaluated
tools.namespace refactor - documentation can be misleading refresh
and other functions were moved to the clojure.tools.namespace.repl
namespace. The original clojure.tools.namespace
functions are deprecated, although the new clojure.tools.namespace.repl
namespace is not deprecated.
Clojure tools.namespace API reference Namespaces Reference - Clojure.org
"},{"location":"clojure-cli/repl-reloaded/#hotload-libraries","title":"Hotload Libraries","text":"clojure.repl.deps
provides functions to hotload libraries into a running REPL, avoiding the need to restart the REPL and loose state just to use a new library with the project.
add-lib
finds a library by name and adds it to the REPL add-libs
takes a hash-map of one or more library name and version key/value pairs and adds them to the REPL sync-deps
reads the project deps.edn
file and adds :deps
dependencies to the REPL that are not already loaded
Hotload functions are typically called from a rich comment block in a separate dev/user.clj
file to avoid being automatically loaded.
Once hot-loaded, a library namespace can be required as if the dependency had been added to the project configuration before the REPL started.
practicalli/clojure-webapp-hotload-libraries is an example project that uses REPL driven development and hot loading of libraries to build a very simple web server using http-kit and hiccup.
Hotload requires Clojure 1.12 & latest Clojure CLI Install the latest Clojure CLI version and use Clojure 1.12 onward to use the officially released hotload library.
add-libs
is an unofficial feature for Clojure 1.11.x and available only in the add-libs3 branch of the now deprecated clojure.tools.deps.alpha
library.
Hotload simple web server and build a page (comment\n ;; Require if not automatically loaded by the REPL tooling, ie. Rebel Readline\n #_(require '[clojure..deps.repl :refer [add-lib add-libs sync-deps]])\n\n ;; hotload the libraries required for the server\n (add-libs '{http-kit/http-kit {:mvn/version \"2.5.1\"}})\n\n (require '[org.httpkit.server :as app-server])\n\n ;; Discover which http-kit functions are available\n (ns-publics (find-ns 'org.httpkit.server))\n\n ;; Define an entry point for the application\n (defn welcome-page\n [request]\n {:status 200\n :body \"Welcome to the world of Clojure CLI hotloading\"\n :headers {}})\n\n ;; Start the application server\n (app-server/run-server #'welcome-page {:port (or (System/getenv \"PORT\") 8888)})\n\n ;; Visit http://localhost:8888/ to see the welcome-page\n\n ;; Hotload Hiccup to generate html for the welcome page\n (add-libs '{hiccup/hiccup {:mvn/version \"2.0.0-alpha2\"}})\n\n (require '[hiccup.core :as hiccup])\n (require '[hiccup.page :as hiccup-page])\n\n (defn page-template [content]\n (hiccup-page/html5\n {:lang \"en\"}\n [:head (hiccup-page/include-css \"https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css\")]\n [:body\n [:section {:class \"hero is-info\"}\n [:div {:class \"hero-body\"}\n [:div {:class \"container\"}\n [:h1 {:class \"title\"} (:title content) ]\n [:p {:class \"subtitle\"} (:sub-title content)]]]]]))\n\n ;; Check the page template returns HTML\n (page-template {:title \"Hotload Libraries in the REPL\"\n :sub-title \"REPL driven development enables experimentation with designs\"})\n\n\n ;; redefine the welcome page to call the page template\n (defn welcome-page\n [request]\n {:status 200\n :body (page-template {:title \"Hotload Libraries in the REPL\"\n :sub-title \"REPL driven development enables experimentation with designs\"})\n :headers {}})\n\n ) ; End of rich comment block\n
There are several approaches to hotload libraries, including via a terminal REPL UI or in a project with a Clojure editor:
- Rich terminal UI REPL
- Hotload in a Project
"},{"location":"clojure-cli/repl-reloaded/#unit-test-runner","title":"Unit test runner","text":"Unit tests are written with clojure.test
and reside in a parallel test
directory, creating matching _test.clj
files for each relevant clojure file under src
.
Test runners are highly configurable and can run a very specific set of tests, although Clojure is usually fast enough to run all the tests each time.
Run the tests in a project with the kaocha test runner by issuing the following command in the root of the project.
clojure -X:test/run\n
Or continually watch for changes and run kaocha test runner each time a file is saved (typically in a separate terminal)
clojure -X:test/watch\n
Test run will stop on the first failed test unless :fail-fast? false
is passed as an argument to either command.
Running Unit Tests in an Editor Emacs and Neovim can run the kaocha test runner if one of the :repl/reloaded
, :dev/reloaded
or :lib/kaocha
aliases are used to start the Clojure REPL.
Emacs, Neovim and VS Code Calva also have their own built-in test runners. Test and source code must be evaluated in the REPL for the editor test runners to discover this code. Editor test runners to not read code from the Clojure files in src
or test
directories.
Writing Unit Tests for Clojure Using Test Runners with Projects
"},{"location":"clojure-cli/repl-reloaded/#performance-tests","title":"Performance tests","text":"time
is a quick way to see if an expression is worth further performance investigation.
(time ,,,)
wrapped around an expression will print the duration that expression took to run. This provides a very rough indicator of the performance of code, although as it only runs once then results may vary and are easily affected by the environment (Java Virtual Machine, Operating System, other concurrent processes).
Criterium provides more realistic performance results which are less affected by the environment, providing a better indication of performance to inform design choices.
Criterium tools take a little longer to run in order to return more accurate and consistent performance results.
REPLProject Require the criterium library
(require '[criterium.core :as benchmark])\n
Require the criterium library via the ns expression
(ns user\n (:require [criterium.core :as benchmark]))\n
Or require the criterium library in a rich comment expression (comment\n (require '[criterium.core :as benchmark]))\n
Wrap the quick-bench
function around the expression to run performance testing upon
(benchmark/quick-bench ,,,)\n
The expression being tested will be called multiple times and the duration and average times will be printed.
Criterium automatically adjusts the benchmark run time according to the execution time of the measured expression. If the expression is fast, Criterium will run it plenty of times, but if a single iteration is quite slow, it will be executed fewer times
Use quick-bench rather than bench
The bench macro is claimed to be more accurate than quick-bench, but in practice, it runs for much longer and doesn't yield significantly different results in most cases
Criterium API Documentation Benchmark with Criterium article
"},{"location":"clojure-cli/repl-reloaded/#log-and-publish-events","title":"Log and publish events","text":"mulog is a micro-logging library that logs events and data extremely fast and provides a range of publishers for log analysis.
Use the mulog log
function to create events to capture useful information about the Clojure system operation. Publish the logs locally to a console or to a log analysis service such as zipkin or Grafana
REPLProject Require the mulog library
(require '[com.brunobonacci.mulog :as mulog])\n
Require the mulog library via the namespace form
(ns your-ns\n (:require [com.brunobonacci.mulog :as mulog]))\n
Optionally create an event global context, containing information that will be included in every event created
(mulog/set-global-context! {:service-name \"Practicalli GameBoard\", :version \"1.0.1\", :env \"dev\"})\n
Create events with an identity that contains key/value pairs of data that captures the desired information about the event.
(mulog/log ::system-started :version \"0.1.0\" :init-time 32)\n
Start a publisher to see all the events created. The publisher can be to the console or to log analysis tools like zipkin or Grafana
(mulog/start-publisher! {:type :console})\n
trace
provides accurate data around instrumented operations of a single system or over a distributed system. trace data can be used in Elasticsearch and real-time streaming system sudh as Apache Kafka.
trace will track the rate of a complex operation, including the outcome and latency, within the contextual information of that operation.
Consider a function that calls several external services
(defn product-status [product-id]\n (let [stock (http/get availability-service {:product-id product-id})\n pricing (http/get pricing-service {:product-id product-id})]))\n
Create a trace between the function calls
(mulog/trace ::product-status\n [:product-id product-id]\n (product-status product-id))\n
trace
starts a timer then calls (product-status product-id)
. Once the execution completes a log an event is created using log
and uses the global context. By including the product id in the trace call, information is captured about the specific product involved in the trace log.
;; {:mulog/event-name :practicalli.service/products,\n;; :mulog/timestamp 1587504242983,\n;; :mulog/trace-id #mulog/flake \"4VTF9QBbnef57vxVy-b4uKzh7dG7r7y4\",\n;; :mulog/root-trace #mulog/flake \"4VTF9QBbnef57vxVy-b4uKzh7dG7r7y4\",\n;; :mulog/duration 254402837,\n;; :mulog/namespace \"practicalli.service\",\n;; :mulog/outcome :ok,\n;; :app-name \"Practicalli GameBoard\",\n;; :env \"dev\",\n;; :version \"1.0.1\"}\n
"},{"location":"clojure-cli/repl-reloaded/#trace-function-calls","title":"Trace function calls","text":"clojure.tools.trace can trace values, functions and a whole namespace of functions.
Tracing a value will show how that value flows through the code
Tracing a function shows the arguments passed to the function each time it is called and the results. Tracing will identify forms that are failing and also show the results of the function call, helping spotting unwanted nil
arguments and parts of a function definition that is failing.
trace
values, optionally assigning a tag trace-vars
dynamically trace a given fully qualified function untrace-vars
- remove trace from a given fully qualified function trace-ns
dynamically trace all functions in the given namespace untrace-ns
remove trace from all functions in the given namespace
:repl/reloaded
and :dev/reloaded
include the clojure.tools.trace dependency, i.e. org.clojure/tools.trace {:mvn/version \"0.7.11\"}
tools.trace API Reference
REPLProject Require the clojure.tools.trace
library and refer the trace
and untrace
functions
(require '[clojure.tools.trace :as trace])\n
Require the clojure.tools.trace
library using the alias trace
(ns user\n (:require '[clojure.tools.trace :as trace]))\n
To trace a value returned from an expression, optionally adding a tag
(trace/trace \"increments\" (map inc [1 2 3 4 5]))\n;;=> TRACE increments: (2 3 4 5 6)\n;;=> (2 3 4 5 6)\n
Trace a function call and its return value
(deftrace random-function [namespace] (rand-nth (vals (ns-publics namespace))))\n
Call the function to see the output of trace
(random-function 'clojure.core)\n;;=> TRACE t1002: (random-function 'clojure.core)\n;;=> TRACE t1002: => #'clojure.core/iteration\n;;=> #'clojure.core/iteration\n
Trace functions can identify which form is failing
(trace/trace-vars practicalli.random-function/random-number)\n;;=> #'practicalli.random-function/random-number\n
Call the function that is being traced and see the error
(practicalli.random-function/random-number 42)\n;;=> TRACE t10951: (practicalli.random-function/random-number 42)\n;;=> Execution error (ArithmeticException) at practicalli.random-function/random-number (random_function.clj:17).\n;;=> Divide by zero\n
Dynamically trace all functions in the given name space
(trace-ns domain.namespace)\n
Or remove all function traces in a namespace
(untrace-ns domain.namespace)\n
Dynamically trace a given function
(trace-vars domain.namespace/function-name)\n ```\n\nRemove the trace on a given function\n\n```clojure\n(untrace-vars domain.namespace/function-name)\n
"},{"location":"clojure-cli/repl-startup/","title":"Configure REPL on Startup","text":"A Clojure REPL starts in the user
namespace and automatically loads common tools to support REPL based development.
When interacting with the REPL prompt directly, use require
expressions to include additional functions into the user
nameapace rather than use potentially complex commands to set the namespace.
Clojure REPL only starts in user namespace The Clojure REPL only guarantees startup in the user
namespace. There is no specific mechanism to start the REPL in any other namespace than user
.
Clojure CLI could use the general --eval
flag as a hack to set a different namespace with an in-ns
expression, although this may affect other tools and add complexity to the startup process.
Default REPL Tools The Clojure REPL automatically loads common tools to support the foundation of a REPL driven workflow:
clojure.repl namespace loads:
- apropos - function names fuzzy matching a given regex pattern
- dir - sorted list of public vars (functions) in a given namespace
- doc - doc-string of a give Clojure function / symbol
- find-doc - doc-string of matching functions, given a string or regex pattern
- source - source code of a given function
- pst print stack trace, optionally setting depth
clojure.java.javadoc loads javadoc to show the doc-string of Java methods
clojure.pprint namepace loads pp & pprint to return pretty printed (human friendly format) evaluation results
"},{"location":"clojure-cli/repl-startup/#custom-user-namespace","title":"Custom user namespace","text":"Add a custom user
namespace to further enhance the Clojure REPL workflow:
- load code into the REPL by requiring namespaces
- call functions to start services that support development, e.g. logging publisher, print REPL command help menu
- launch development tools - e.g. portal data inspector
- start components (i.e for mount, component, integrant)
- hotload libraries into the REPL process without restart (Clojure 1.12 onward)
Create a project with custom user namespace
Projects created with Practicalli Project Templates contain a dev/user.clj
file for configuring the REPL at start up.
Practicalli custom user namespace supports the Practicalli REPL Reloaded workflow
Start the REPL with either the :dev/env
, :dev/reloaded
or :repl/reloaded
alias from Practicalli Clojure CLI Config to include dev
directory on the class path and automatically load dev/user.clj
code on REPL startup.
"},{"location":"clojure-cli/repl-startup/#define-user-namespace","title":"Define user namespace","text":"A custom user.clj
is typically placed in a dev
folder within the root of the project, with the dev
path defined in an alias to keep it separated from production code.
Create a dev/user.clj
file with a namespace called user
.
dev/user.clj(ns user)\n
Create an alias to include the dev
path when running a REPL process
Practicalli Clojure CLI ConfigManual Practicalli Clojure CLI Config includes aliases that add dev
directory to the class path
:dev/env
alias only adds the dev
directory to the classpath :dev/reloaded
adds library hotload, namespace reload, porta data inspector and testing libraries & test
:repl/reloaded
adds Rebel rich terminal UI to the tools provided by :dev/reloaded
Add an alias to the user deps.edn
configuration, i.e. $XDG_CONFIG_HOME/clojure/deps.edn
or $HOME/.clojure/deps.edn
Clojure User Config
:env/dev\n {:extra-paths [\"dev\"]}\n
Review Practicalli Clojure CLI Config for further alias examples. Run a Clojure REPL with the :repl/reloaded
alias (or :dev/reloaded
:dev/env
) to add the dev
directory to the class path and load the code in dev/user.clj
file into the REPL.
clojure -M:repl/reloaded\n
Keep user.clj
separate
The user.clj
code should not be included in live deployments, such as a jar or uberjar. Including the dev/
directory via an alias separates the user.clj
from deployment actions.
"},{"location":"clojure-cli/repl-startup/#requiring-namespaces","title":"Requiring namespaces","text":"Namespaces required in the user
ns form will also be loaded. If a required namespace also requires namespaces, they will also be loaded into the REPL during startup.
Functions (defn)
and data (def)
are immediately available.
Require namespace in user ns expression
Add a require expression to the namespace definition in dev/user.clj
dev/user.clj
(ns user\n (:require [practicalli.project-namespace]))\n
Requiring a large number of libraries may slow REPL start up time Require namespace in require expression
If the library is not always required, place a require
within a (comment ,,,)
expression to be evaluated by the developer any time after REPL startup. dev/user.clj
(ns user)\n\n(comment\n (require '[practicalli.project-namespace])\n#_())\n
"},{"location":"clojure-cli/repl-startup/#calling-functions","title":"Calling functions","text":"Use the fully qualified function name from the required namespace can be called, to start the application for example.
Example
dev/user.clj(ns user\n (:require [practicalli.project-namespace]))\n\n(practicalli.project-namespace/-main)\n
An alias can be used in the require expression, useful if multiple functions from a namespace are to be called
Example
dev/user.clj(ns user\n (:require [practicalli.service :as service]))\n\n(service/-main)\n
"},{"location":"clojure-cli/repl-startup/#repl-help-menu","title":"REPL Help menu","text":"Printing a menu of functions provided by the custom user namespace helps with the usability of a project.
Define a help
function that prints out commands with a breif explination of their purpose.
Add a (help)
expression to call the help function on REPL startup, displaying the help menu.
REPL Help menu
;; ---------------------------------------------------------\n;; Help\n\n(println \"---------------------------------------------------------\")\n(println \"Loading custom user namespace tools...\")\n(println \"---------------------------------------------------------\")\n\n(defn help\n []\n (println \"---------------------------------------------------------\")\n (println \"System components:\")\n (println \"(start) ; starts all components in system config\")\n (println \"(restart) ; read system config, reloads changed namespaces & restarts system\")\n (println \"(stop) ; shutdown all components in the system\")\n ;; (println \"(system) ; show configuration of the running system\")\n ;; (println \"(config) ; show system configuration\")\n (println)\n (println \"Hotload libraries: ; Clojure 1.12.x\")\n (println \"(add-lib 'library-name)\")\n (println \"(add-libs '{domain/library-name {:mvn/version \\\"v1.2.3\\\"}})\")\n (println \"(sync-deps) ; load dependencies from deps.edn\")\n (println \"- deps-* lsp snippets for adding library\")\n (println)\n (println)\n (println \"Portal Inspector:\")\n (println \"- portal started by default, listening to all evaluations\")\n (println \"(inspect/clear) ; clear all values in portal\")\n (println \"(remove-tap #'inspect/submit) ; stop sending to portal\")\n (println \"(inspect/close) ; close portal\")\n (println)\n (println \"(help) ; print help text\")\n (println \"---------------------------------------------------------\"))\n\n(help)\n\n;; End of Help\n;; ---------------------------------------------------------\n
"},{"location":"clojure-cli/repl-startup/#log-publisher","title":"Log publisher","text":"mulog is a very effective event log tool that also provides a range of log publishers. A custom user namespace can be used to start mulog log publishers to directly support the development workflow
- pretty print console output for easier to read event messages
- custom tap-publisher to send all log message to a
tap>
source, e.g. Portal data inspector
Mulog configuration and publishers
Require the mulog namespaces.
Set the global context for all mulog events, setting common key/value pairs that appear in every event created after the global context was evaluated.
Define a custom publisher to send all mulog events to the registered tap> sources, e.g. Portal data inspector.
dev/mulog_events.clj;; ---------------------------------------------------------\n;; Mulog Global Context and Custom Publisher\n;;\n;; - set event log global context\n;; - tap publisher for use with Portal and other tap sources\n;; - publish all mulog events to Portal tap source\n;; ---------------------------------------------------------\n\n(ns mulog-events\n (:require\n [com.brunobonacci.mulog :as mulog]\n [com.brunobonacci.mulog.buffer :as mulog-buffer]))\n\n;; ---------------------------------------------------------\n;; Set event global context\n;; - information added to every event for REPL workflow\n(mulog/set-global-context! {:app-name \"todo-basic Service\",\n :version \"0.1.0\", :env \"dev\"})\n;; ---------------------------------------------------------\n\n;; ---------------------------------------------------------\n;; Mulog event publishing\n\n(deftype TapPublisher\n [buffer transform]\n com.brunobonacci.mulog.publisher.PPublisher\n (agent-buffer [_] buffer)\n (publish-delay [_] 200)\n (publish [_ buffer]\n (doseq [item (transform (map second (mulog-buffer/items buffer)))]\n (tap> item))\n (mulog-buffer/clear buffer)))\n\n#_{:clj-kondo/ignore [:unused-private-var]}\n(defn ^:private tap-events\n [{:keys [transform] :as _config}]\n (TapPublisher. (mulog-buffer/agent-buffer 10000) (or transform identity)))\n\n(def tap-publisher\n \"Start mulog custom tap publisher to send all events to Portal\n and other tap sources\n `mulog-tap-publisher` to stop publisher\"\n (mulog/start-publisher!\n {:type :custom, :fqn-function \"mulog-events/tap-events\"}))\n\n#_{:clj-kondo/ignore [:unused-public-var]}\n(defn stop\n \"Stop mulog tap publisher to ensure multiple publishers are not started\n Recommended before using `(restart)` or evaluating the `user` namespace\"\n []\n tap-publisher)\n\n;; Example mulog event message\n;; (mulog/log ::dev-user-ns :message \"Example event message\" :ns (ns-publics *ns*))\n;; ---------------------------------------------------------\n
"},{"location":"clojure-cli/repl-startup/#reload-namespaces","title":"Reload Namespaces","text":"The REPL state can become 'stale' and contain vars (data and function names) that are no longer part of the source code, especially after a code refactor.
Rather than restart the repl, clojure.tools.namespace.repl provides functions that can clean the REPL state and reload changed namespaces from source code.
Clojure Namespace Tools - reload
Require the clojure.tools.namespace.repl
namespace to access the refresh
and set-refresh-dirs
functions to support reloading of source code into a clean REPL state.
dev/user.clj(ns user\n \"Tools for REPL Driven Development\"\n (:require\n [clojure.tools.namespace.repl :refer [set-refresh-dirs]]))\n
Use the set-refresh-dirs
function to define directories to reload when calling refresh
, effectively excluding dev
and other directories by not including their names as arguments.
dev/user.clj;; ---------------------------------------------------------\n;; Avoid reloading `dev` code\n;; - code in `dev` directory should be evaluated if changed to reload into repl\n(println\n \"Set REPL refresh directories to \"\n (set-refresh-dirs \"src\" \"resources\"))\n;; ---------------------------------------------------------\n
"},{"location":"clojure-cli/repl-startup/#hotload-libraries","title":"Hotload libraries","text":"Hotload is a way to add libraries to a running REPL process which were not include as a dependency during REPL startup.
Hotload libraries is SNAPSHOT feature - this guide will change when Clojure 1.12 is released Functions to hotload libraries are part of the Clojure 1.12 development releases and an official feature as of the stable 1.12 release.
For Clojure 1.11 and similar functions are available in the add-libs3 branch of the now deprecated clojure.tools.deps.alpha
library.
clojure/tools.deps is the official library for all released functions from the alpha library
This guide will be significantly rewritten once Clojure 1.12 is released.
Practicalli Clojure CLI ConfigManual :repl/reloaded
and dev/reloaded
aliases in Practicalli Clojure CLI Config provide the add-libs
function.
Edit the project deps.edn
configuration and add an :lib/hotload
alias for the clojure.tools.deps.alpha.repl
library. Or add an alias to the user level configuration for use with any Clojure CLI project.
The add-libs
code is on a separate add-libs3 branch, so requires the SHA from the head of add-libs3 branch
:lib/hotload\n {:extra-deps {org.clojure/tools.deps.alpha\n {:git/url \"https://github.com/clojure/tools.deps.alpha\"\n :git/sha \"e4fb92eef724fa39e29b39cc2b1a850567d490dd\"}}}\n
Alias example from Practicalli Clojure CLI Config
Start a REPL session using Clojure CLI with :repl/reloaded
, dev/reloaded
or :lib/hotload
aliases
clojure -M:repl/reloaded\n
Require and refer add-libs function
Require the clojure.tools.deps.alpha
library and refer the add-libs
function. The add-libs
function can then be called without having to use an alias or the fully qualified name.
(require '[clojure.tools.deps.alpha.repl :refer [add-libs]])\n
Hotload one or more libraries into the REPL using the add-lib
function, including the fully qualified name of the library and version string.
Hotload the hiccup library
The hiccup library converts clojure structures into html, where vectors represent the scope of keywords that represent html tags. Load the hiccup library using add-libs
(add-libs '{hiccup/hiccup {:mvn/version \"2.0.0-alpha2\"}})\n
Require the hiccup library so its functions are accessible from the current namespace in the REPL.
(require '[hiccup.core :as hiccup])\n
Enter an expression using the hiccup/html
function to convert a clojure data structure to html. (hiccup/html [:div {:class \"right-aligned\"}])\n
"},{"location":"clojure-cli/repl-startup/#system-components","title":"System Components","text":"Clojure has several library to manage the life-cycle of components that make up the Clojure system, especially those components with state. The order in which components are started and stopped can be defined to keep the system functioning correctly.
Components can include an http server, routing, persistence, logging publisher, etc.
Example system component management libraries included
- mount - manage system state in an atom
- integrant and Integrant REPL - data definition of system and init & halt defmethod interface
- donut system
- component
Require system namespace in user ns expression
Require the system namespace and use start
, restart
and stop
functions to manage the components in the system dev/user.clj
(ns user\n (:require [system]))\n\n(comment\n (system/start)\n (system/restart)\n (system/stop)\n )\n
Define code in the dev/system.clj
file which controls the component life-cycle services library for the project.
Create a dev/system.clj
to manage the components, optionally using one of the system component management libraries.
"},{"location":"clojure-cli/repl-startup/#life-cycle-functions","title":"life-cycle functions","text":"Start, stop and restart the components that a system is composed of, e.g. app server, database pool, log publisher, message queue, etc.
Atom restartMountIntegrant REPLDonut SystemComponent Clojure web services run ontop of an HTTP server, e.g. http-kit, Jetty.
A Clojure aton can be used to hold a reference to the HTTP server, allowing commands to stop that server.
Use clojure.tools.namespace.repl/refresh
when restarting the server (in between stop
and start
) to remove stale information in the REPL state.
Restart an HTTP server for Clojure Web Service & Refresh namespaces
dev/system_repl.clj;; ---------------------------------------------------------\n;; System REPL - Atom Restart \n;;\n;; Tools for REPl workflow with Aton reference to HTTP server \n;; https://practical.li/clojure-web-services/app-servers/simple-restart/\n;; ---------------------------------------------------------\n\n(ns system-repl\n (:require \n [clojure.tools.namespace.repl :refer [refresh]]\n [practicalli.todo-basic.service :as service]))\n\n;; ---------------------------------------------------------\n;; HTTP Server State\n\n(defonce http-server-instance \n (atom nil)) ; (1)! \n;; ---------------------------------------------------------\n\n;; ---------------------------------------------------------\n;; REPL workflow commands\n\n(defn stop\n \"Gracefully shutdown the server, waiting 100ms.\n Check if an http server isntance exists and \n send a `:timeout` key and time in milliseconds to shutdown the server.\n Reset the atom to nil to indicate no http server is running.\"\n []\n (when-not (nil? @http-server-instance)\n (@http-server-instance :timeout 100) ; (2)!\n (reset! http-server-instance nil) ; (3)!\n (println \"INFO: HTTP server shutting down...\")))\n\n(defn start\n \"Start the application server and run the application,\n saving a reference to the https server in the atom.\"\n [& port]\n (let [port (Integer/parseInt\n (or (first port)\n (System/getenv \"PORT\")\n \"8080\"))]\n (println \"INFO: Starting server on port:\" port)\n\n (reset! http-server-instance\n (service/http-server-start port)))) ; (4)!\n\n\n(defn restart\n \"Stop the http server, refresh changed namespace and start the http server again\"\n []\n (stop)\n (refresh) ; (5)! \n (start))\n;; ---------------------------------------------------------\n
-
A Clojure Aton holds a reference to the http server instance
-
Shut down http server instance without stopping the Clojure REPL
-
Reset the value in the atom to mil, indicating that no http server instance is running
-
Reset the value in the atom to a reference for the running http server. The reference is returned when starting the http server.
-
Refresh the REPL state and reload changed namespaces from source code using clojure.tools.namespace.repl/refresh
Define a dev.clj
file with go
, stop
and restart
functions that manage the life-cycle of mount components. A start
function contains the list of components with optional state.
Require the mount namespace and the main namespace for the project, which should contain all the code to start and stop services.
dev/user.clj(ns user\n :require [mount.core :refer [defstate]]\n [practicalli.app.main])\n
Define a start function to start all services
dev/user.clj(defn start []\n (with-logging-status)\n (mount/start #'practicalli.app.conf/environment\n #'practicalli.app.db/connection\n #'practicalli.app.www/business-app\n #'practicalli.app.service/nrepl))\n
The go
function calls start
and marks all components as ready.
dev/user.clj(defn go\n \"Start all states defined by defstate\"\n []\n (start)\n :ready)\n
The stop
function stops all components, removing all non-persistent state.
(defn stop [] (mount/stop))\n
The reset function that calls stop
, refreshes the namespaces so that stale definitions are removed and starts all components (loading in any new code).
dev/user.clj(defn reset\n \"Stop all states defined by defstate.\n Reload modified source files and restart all states\"\n []\n (stop)\n (namespace/refresh :after 'dev/go))\n
Example dev.clj file for mount
Use dev
namespace during development
Require practicalli.app.dev
namespace rather than main, to start components in a development environment.
Integrant REPL - Practicalli Clojure Web Services
User manager - Integrant
donut.system is a dependency injection library for Clojure and ClojureScript using system and component abstractions to organise and manage startup & shutdown behaviour.
Basic usage guide shows how to define a donut.system
seancorfield/usermanager-example is an example project that uses Component for lifecycle management
"},{"location":"clojure-cli/repl-startup/#reference","title":"Reference","text":" - Mount project on GitHub
- Mount - collection of Clojure/Script mount apps
- donut.system
- Component
- A tutorial to Stuart Sierra's Component
- Refactoring to Components - Walmart Labs Lacinia
- Integrant
- Compojure and Integrant
- Build a Clojure web app using Duct - CircleCI
- Reloading Woes - Lambda island
"},{"location":"clojure-cli/projects/","title":"Clojure projects","text":"Clojure CLI projects use a deps.edn
file to specifies source paths and libraries required for the project to run.
alias are defined in the deps.edn
file to support development tasks, providing additional libraries, paths and tools.
Generate a project from a template
Create a project from a template for a consistent project structure and include commonly used libraries.
Practicalli Project Templates create production grade projects providing a detailed starting point with configuration files for building and deploying the project.
"},{"location":"clojure-cli/projects/#create-minimal-project","title":"Create minimal project","text":"Create a deps.edn
file containing {}
in the root of a directory for a minimal configuration.
Create a src
directory as the root of the source code, and test
directory to contain unit test code.
Linux command to create a minimal clojure project Run these Linux commands in the root of a directory to create a minimal Clojure project structure.
touch deps.edn && echo '{}' > deps.edn && mkdir src test\n
The project can now be run with a REPL via a terminal UI or Clojure aware Editor.
Migrate project to Clojure CLI Guide to Migrating a project to Clojure CLI
"},{"location":"clojure-cli/projects/#project-structure","title":"Project Structure","text":"The essence of most Clojure CLI projects contains the following files and directories.
path purpose deps.edn core project configuration, paths, dependencies and aliases build.clj build specific configuration, create jars and uberjars src root directory of Clojure source files test root directory for Clojure test source files README.md Description of the project and how to develop / maintain it CHANGELOG.md Meaningful history of changes to the project organised by release .git Local git repository and configuration .gitignore Git ignore patterns for the project Example deps.edn configuration file
{:paths\n [\"src\" \"resources\"]\n\n :deps\n {org.clojure/clojure {:mvn/version \"1.11.1\"}}\n http-kit/http-kit {:mvn/version \"2.6.0\"} \n metosin/reitit {:mvn/version \"0.5.13\"}\n com.brunobonacci/mulog {:mvn/version \"0.9.0\"}\n\n :aliases\n {;; Clojure.main execution of application\n :run/service\n {:main-opts [\"-m\" \"practicalli.donuts.service\"]}\n\n ;; Clojure.exec execution of specified function\n :run/greet\n {:exec-fn practicalli.donuts.service/greet\n :exec-args {:name \"Clojure\"}}\n\n ;; Add libraries and paths to support additional test tools\n :test/env\n {}\n\n ;; Test runner - local and CI\n ;; call with :watch? true to start file watcher and re-run tests on saved changes\n :test/run\n {:extra-paths [\"test\"]\n :extra-deps {lambdaisland/kaocha {:mvn/version \"1.85.1342\"}}\n :main-opts [\"-m\" \"kaocha.runner\"]\n :exec-fn kaocha.runner/exec-fn\n :exec-args {:randomize? false\n :fail-fast? true}}\n\n ;; tools.build `build.clj` built script\n :build\n {:replace-paths [\".\"]\n :replace-deps {io.github.clojure/tools.build\n {:git/tag \"v0.9.4\" :git/sha \"76b78fe\"}}\n :ns-default build}}}\n
"},{"location":"clojure-cli/projects/add-libraries/","title":"Add libraries to a project","text":"The project deps.edn
file is used to add specific versions of libraries to a project.
The :deps
top-level key defines libraries that are always included, e.g when starting the REPL or packaging a project in an Uberjar.
Aliases are defined to include libraries only when the alias name is included, e.g. :dev/reloaded
alias includes several libraries only relevant during development of a Clojure project.
There are thousands of community Clojure and ClojureScript libraries available via clojars.org and Maven Central.
:deps
top level key contains a hash-map of dependencies, each dependency of the form domain/name {:mvn/version \"version-number\"}
Project deps.edn{:deps\n {org.clojure/clojure {:mvn/version \"1.11.1\"}\n hiccup/hiccup {:mvn/version \"2.0.0-alpha2\"}}}\n
Finding libraries Search for community libraries via the Clojars.org website or visit the Clojure Toolbox to browse some of the community libraries available
clojure -M:search/libraries pattern
where pattern is the name of the library to search for. Copy the relevant results into the project deps.edn
file.
clojure -M:search/libraries --format:merge pattern
will automatically add the library into the deps.edn
file.
clojure -X:deps find-versions :lib fully.qualified/library-name :n 5
returns the last 5 versions of the given library.
"},{"location":"clojure-cli/projects/add-libraries/#alias-libraries","title":"Alias libraries","text":":aliases
top-level key contains a hash-map of alias definitions.
Each alias has a unique name with :aliases
and is represented by a Clojure keyword associated with a Clojure hash-map, {}
:extra-deps
keyword is associated with hash-map that contains one or more fully qualified library names and the version of the library to use. The version of the library is defined with the maven form {:mvn/version \"0.4.2\"}
or Git form {:git/url \"https://github.com/clojure/tools.deps.alpha\" :git/sha \"e4fb92eef724fa39e29b39cc2b1a850567d490dd\"}
The following example can be added to a project deps.edn
, within the :aliases {}
form.
deps.edn alias definition with maven and git versions:dev/reloaded\n{:extra-deps {djblue/portal {:mvn/version \"0.34.2\"}\n lambdaisland/kaocha {:mvn/version \"1.71.1119\"}\n org.clojure/test.check {:mvn/version \"1.1.1\"}\n org.clojure/tools.namespace {:mvn/version \"1.3.0\"}\n org.clojure/tools.deps.alpha {:git/url \"https://github.com/clojure/tools.deps.alpha\"\n :git/sha \"e4fb92eef724fa39e29b39cc2b1a850567d490dd\"}}}\n
When the alias is included in the command to start the REPL, the libraries are placed on the class path and can be required for use.
clojure -M:dev/reloaded:repl/rebel\n
"},{"location":"clojure-cli/projects/add-libraries/#hotload-libraries","title":"Hotload libraries","text":"add-libs
is a function to load one or more libraries into a running REPL, negating the need to restart the REPL process.
Start a REPL process with clojure -M:repl/reloaded
to include the add-libs librar. Alternatively, include :dev/reloaded
or :lib/hotload
alias with any Clojure command to start a REPL.
Hotload Libraries explained
REPL Reloaded - Hotload Libraries details all the options for including the clojure.tools.deps.alpha library that contains the add-libs
function
Use a rich comment block or a dev/user.clj
file to require the clojure.tools.deps.alpha.repl
namespace and write add-libs
expressions to hot-load libraries.
A rich comment block ensures add-libs
code is only evaluated manually by a developer.
(comment\n (require '[clojure.tools.deps.alpha.repl :refer [add-libs]])\n (add-libs '{http-kit/http-kit {:mvn/version \"2.5.1\"}})\n)\n
rich-comment-hotload Clojure LSP snippet Snippets provided by Clojure LSP include rich-comment-hotload
, to add a rich comment block with a require for clojure.tools.deps.alpha
and an add-libs
expression, making it very quick to add this code.
deps-maven
and deps-git
snippets help ensure the correct syntax is used for the add-libs
expression for each library dependency to be added.
Practicalli Clojure LSP Config contains a wide range of snippets
"},{"location":"clojure-cli/projects/add-libraries/#hotload-example","title":"Hotload Example","text":"Create a web server from scratch, serving pages generated from hiccup, with all libraries hot-loaded as the code is being written. Demonstrates that it is possible to write an application when only starting the REPL once.
Web server from scratch (comment\n ;; run REPL with :lib/hotload alias\n (require '[clojure.tools.deps.alpha.repl :refer [add-libs]])\n\n ;; hotload the libraries required for the server\n (add-libs\n '{http-kit/http-kit {:mvn/version \"2.5.1\"}})\n ;; => (http-kit/http-kit)\n\n\n ;; Require the namespace from the http-kit library\n (require '[org.httpkit.server :as app-server])\n\n ;; Define a handler for http requests\n (defn welcome-page\n [request]\n {:status 200\n :body \"Welcome to the world of Clojure CLI hotloading\"\n :headers {}})\n\n ;; Start the application server with the handler\n (app-server/run-server #'welcome-page {:port (or (System/getenv \"PORT\") 8888)})\n\n ;; Visit http://localhost:8888/ to see the welcome-page\n\n ;; Hotload Hiccup to generate html for the welcome page\n (add-libs '{hiccup/hiccup {:mvn/version \"2.0.0-alpha2\"}})\n\n (require '[hiccup.core :as hiccup])\n (require '[hiccup.page :as hiccup-page])\n\n ;; Create a page template\n (defn page-template [content]\n (hiccup-page/html5\n {:lang \"en\"}\n [:head (hiccup-page/include-css \"https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css\")]\n [:body\n [:section {:class \"hero is-info\"}\n [:div {:class \"hero-body\"}\n [:div {:class \"container\"}\n [:h1 {:class \"title\"} (:title content) ]\n [:p {:class \"subtitle\"} (:sub-title content)]]]]]))\n\n ;; Check the page template returns HTML\n (page-template {:title \"Hotload Libraries in the REPL\"\n :sub-title \"REPL driven development enables experimentation with designs\"})\n\n\n ;; redefine the welcome page to call the page template\n (defn welcome-page\n [request]\n {:status 200\n :body (page-template {:title \"Hotload Libraries in the REPL\"\n :sub-title \"REPL driven development enables experimentation with designs\"})\n :headers {}})\n\n ;; Visit http://localhost:8888/ and refresh the page to see the new welcome-page\n )\n
"},{"location":"clojure-cli/projects/add-libraries/#excluding-dependencies","title":"Excluding dependencies","text":"Adding several libraries as dependencies to a project may cause conflicts. The :exclusions
key will prevent libraries within a library dependency from being included in the project
For example, library-a and library-b both have a dependency on library-c, as defined in the project configuration for library-a and library-b. When including library-a and library-b in the project as dependencies, there could be a conflict if the both libraries use a different version of library-c. Adding an exclude to library-a or library-b will stop library-c being included twice.
A Library that is self-contained and does not itself include any dependencies on any other libraries is unlikely to cause conflicts. Using these self-contained libraries simplifies the overall application design.
{:deps {:org.clojure/clojure {:mvn/version \"1.10.2\"}\n :cheshire/cheshire {:mvn/version \"5.10.0\"\n :exclusions \"com.fasterxml.jackson.core/jackson-core\"}}}\n
"},{"location":"clojure-cli/projects/hotload-in-project/","title":"Hotload libraries in Clojure Projects","text":"When starting a REPL process the dependencies listed in the project deps.edn
file are added to the class path. To add further dependencies the REPL has to be restarted to include new libraries added to the deps.edn
file.
Practicalli REPL Reloaded workflow allows new dependencies to be added to a running REPL process, negating the need to restart the REPL process which would loose the current REPL state.
"},{"location":"clojure-cli/projects/hotload-in-project/#hotload-repl","title":"Hotload REPL","text":"Start a REPL with an alias that includes the add-libs
library.
Terminal REPLClojure Editor Start a terminal REPL with the :repl/reloaded
alias and connect
clojure -M:repl/reloaded\n
Connect to the REPL process from a Clojure editor for an enhanced development experience. Run a Clojure REPL from the editor (jack-in command) configured with the :dev/reloaded
alias or :lib/hotload
alias in an Editor jack-in command or other REPL startup command.
Alternatively, run a Terminal REPL and connect the editor to that REPL process (connect command)
Practicalli REPL Reloaded Configuration
"},{"location":"clojure-cli/projects/hotload-in-project/#rich-comment-block","title":"Rich Comment Block","text":"Use a rich comment block or a dev/user.clj
file to require the clojure.tools.deps.alpha.repl
namespace and write add-libs
expressions to hot-load libraries.
A rich comment block ensures add-libs
code is only evaluated manually by a developer.
(comment\n (require '[clojure.tools.deps.alpha.repl :refer [add-libs]])\n (add-libs '{http-kit/http-kit {:mvn/version \"2.5.1\"}})\n)\n
Rich-comment-hotload Practicalli Clojure LSP Config includes the rich-comment-hotload
snippet which adds a rich comment block with a require for clojure.tools.deps.alpha
and an add-libs
expression, making it very quick to add this code.
deps-maven
and deps-git
snippets help ensure the correct syntax is used for the add-libs
expression for each library dependency to be added.
"},{"location":"clojure-cli/projects/hotload-in-project/#hotload-example","title":"Hotload Example","text":"Create a web server from scratch, serving pages generated from hiccup, with all libraries hot-loaded as the code is being written. Demonstrates that it is possible to write an application when only starting the REPL once.
(comment\n ;; run REPL with :lib/hotload alias\n (require '[clojure.tools.deps.alpha.repl :refer [add-libs]])\n\n ;; hotload the libraries required for the server\n (add-libs\n '{http-kit/http-kit {:mvn/version \"2.5.1\"}})\n ;; => (http-kit/http-kit)\n\n\n ;; Require the namespace from the http-kit library\n (require '[org.httpkit.server :as app-server])\n\n ;; Define a handler for http requests\n (defn welcome-page\n [request]\n {:status 200\n :body \"Welcome to the world of Clojure CLI hotloading\"\n :headers {}})\n\n ;; Start the application server with the handler\n (app-server/run-server #'welcome-page {:port (or (System/getenv \"PORT\") 8888)})\n\n ;; Visit http://localhost:8888/ to see the welcome-page\n\n ;; Hotload Hiccup to generate html for the welcome page\n (add-libs '{hiccup/hiccup {:mvn/version \"2.0.0-alpha2\"}})\n\n (require '[hiccup.core :as hiccup])\n (require '[hiccup.page :as hiccup-page])\n\n ;; Create a page template\n (defn page-template [content]\n (hiccup-page/html5\n {:lang \"en\"}\n [:head (hiccup-page/include-css \"https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css\")]\n [:body\n [:section {:class \"hero is-info\"}\n [:div {:class \"hero-body\"}\n [:div {:class \"container\"}\n [:h1 {:class \"title\"} (:title content) ]\n [:p {:class \"subtitle\"} (:sub-title content)]]]]]))\n\n ;; Check the page template returns HTML\n (page-template {:title \"Hotload Libraries in the REPL\"\n :sub-title \"REPL driven development enables experimentation with designs\"})\n\n\n ;; redefine the welcome page to call the page template\n (defn welcome-page\n [request]\n {:status 200\n :body (page-template {:title \"Hotload Libraries in the REPL\"\n :sub-title \"REPL driven development enables experimentation with designs\"})\n :headers {}})\n\n ;; Visit http://localhost:8888/ and refresh the page to see the new welcome-page\n )\n
Using add-libs with project deps.edn A project deps.edn
file can also be used to hotload libraries with add-lib
. This has the advantage that newly added libraries become part of the normal project dependency configuration.
Add a namespace definition to the deps.edn
file to help editors understand the deps.edn
file is being used for code. Use the #_
comment reader macro with the namespace definition to only evaluate this code manually as a developer.
Add the add-libs
expression after the :deps
key so that it is easy to slurp in the existing and new dependencies as a single hash-map. Use the comment reader macro #_
to only evaluate this code manually.
To hotload, remove the #_
temporarily and slurp in the hash-map of dependencies, placing a '
at the start of the hash-map. Add the name and version of libraries to hotload in the hash-map. Evaluate the add-libs
expression which should return a list of new namespaces added.
Once hotload has finished, barf the hash-maps of dependencies from the add-libs
expression, removing the '
. Add the #_
to the add-libs
expression and save the file.
The hotloaded libraries are now available by requiring their namespaces. If the REPL is restarted, the new dependencies will be included in the Classpath as they are now part of the project configuration.
;; ---------------------------------------\n;; Project Configuration with Hotload\n;; ---------------------------------------\n\n;; Hotload requires\n#_(ns deps.edn\n (:require [clojure.tools.deps.alpha.repl :refer [add-libs]]))\n\n;; Project configuration\n{:paths\n [\"src\" \"resources\"]\n\n :deps\n #_ (add-libs)\n {org.clojure/clojure {:mvn/version \"1.10.1\"}\n http-kit/http-kit {:mvn/version \"2.5.1\"}\n hiccup/hiccup {:mvn/version \"2.0.0-alpha2\"}}\n\n :aliases {}\n
"},{"location":"clojure-cli/projects/hotload-in-project/#live-coding-video","title":"Live Coding video","text":"See the REPL driven development video by Sean Corfield for this technique.
Jump to 23 minutes into the video to see this form of hotload in action.
"},{"location":"clojure-cli/projects/migrate-project/","title":"Migrating Project To Clojure CLI","text":"Migrating an existing project to Clojure CLI can be as simple as the addition of a deps.edn
configuration file.
Leiningen plugins that change code
A few Leiningen plugins inject code into a project to make it work. For example, lein-ring injects Clojure code into the project to run an application server. These type of plugins may require updates to the Clojure code in the project.
"},{"location":"clojure-cli/projects/migrate-project/#minimal-approach","title":"Minimal approach","text":"Create a deps.edn
file in the root of the project directory, containing an empty hash-map, {}
The Clojure version will be taken from the Clojure CLI tools install configuration.
This configuration is enough to run a terminal REPL UI for the project, although requiring namespaces from the project may require libraries to be added as dependencies first.
"},{"location":"clojure-cli/projects/migrate-project/#adding-dependencies","title":"Adding dependencies","text":"All Clojure projects require the org.clojure/clojure
library and a specific version is defined in the configuration that comes with the Clojure CLI install.
Use the :deps
key in deps.edn
to specify a version of the org.clojure/clojure
library, along with any dependencies required for the Clojure code to run.
{:deps\n {org.clojure/clojure {:mvn/version \"1.11.1\"}\n integrant/integrant {:mvn/version \"0.8.0\"}}}\n
REPL Reloaded - add-libs hotload dependencies Practicalli REPL Reloaded provides the add-libs function that can hotload libraries into the running REPL, without having to restart the REPL process.
The hotload approach can also be useful for diagnosing conflicts in dependencies by loading them in stages to narrow down the library causing the conflict.
"},{"location":"clojure-cli/projects/migrate-project/#adding-paths","title":"Adding paths","text":"It is advisable to specify the directory paths to define the location of the source code in the project, especially when running the project in other environments such as a continuous integration server.
Edit the deps.edn
file in the root of the project directory and add source directory and if relevant the resources directory.
{:paths\n [\"src\" `resource`]}\n
"},{"location":"clojure-cli/projects/migrate-project/#add-test-runner","title":"Add test runner","text":"Tests can be run locally using the :test/run
or :test/watch
aliases from the Practicalli Clojure CLI Config.
Continuous Integration Support A Continuous Integration server requires an alias in the project deps.edn
file to define a test runner.
A selection of test runners are provided via aliases defined in Practicalli Clojure CLI Config. Copy a test runner alias to the project deps.edn
file.
"},{"location":"clojure-cli/projects/migrate-project/#deployment","title":"Deployment","text":"A Continuous Delivery pipeline will require an alias in the project deps.edn
file to define how to build a jar or uberjar to package the Clojure project.
Project Package section details how to use tools.build
to create jar and uberjar archives of the project for deployment.
"},{"location":"clojure-cli/projects/migrate-project/#migration-tools","title":"Migration Tools","text":"Several tools exist to support migration from Leiningen projects to Clojure CLI projects. Results will be dependant on how complex the Leiningen project configuration is.
- lein-to-deps - create a
deps.edn
configuration from a project.clj
configuration - lein-tools-deps - share Clojure CLI dependencies with Leiningen project configuration.
"},{"location":"clojure-cli/projects/namespace/","title":"Namespaces","text":"Using namespaces makes code easier to work with by provide levels of abstraction that convey the overall design of the project. Clearly organized namespaces support a simple design approach for a project and make it easier to maintain.
A namespace is a logical separation of code, usually along features of the projects. Think of all namespaces as creating an API's within the project that communicate the architecture of the system.
"},{"location":"clojure-cli/projects/namespace/#controlling-scope","title":"Controlling scope","text":"Logically related data structures and functions are defined within a namespace, limiting their default scope to that namespace.
Namespaces should limit their interdependence on each other (limited number of required namespaces) to avoid a highly coupled design.
Within a namespace a var (def
, defn
) can be called by its short-form name. Outside of the namespace, a fully qualified name must be used, or required via an alias or directly referred.
Vars can be marked as private, def ^private name
, so they can be accessed only by functions in their own namespace (athough there are ways to by-pass that scope restiction).
"},{"location":"clojure-cli/projects/namespace/#including-another-namespace","title":"Including another namespace","text":"(ns namespace.name (:require [domain/filename :as purpose]))
is used to enable access to the functions & named data structures in another namespace than the current one. The included namespace is given an alias so its clear which code comes from that namespace.
Practicalli recommends using a meaningful alias that defines the purpose of the library you are including. This helps with the understanding and maintainability of the code, especially if you wish to refactor and replace the included library with an alternative. An alias name should be meaningful and you should avoid single character and cryptic aliases.
(ns my-namespace.core\n :require [clojure.java.io :as java-io])\n\n(defn read-the-file [filename]\n (line-seq (java-io/reader filename)))\n\n(read-the-file \"project.clj\")\n
Trying out a namespace
(require '[domain/filename])
can be used within you code if testing that namespace functions to see if they are useful to the project. Using a live linter such as clj-kondo, part of Clojure LSP, will highlight missing namespaces.
"},{"location":"clojure-cli/projects/namespace/#including-specific-parts-of-a-namespace","title":"Including specific parts of a namespace","text":":refer
in the require
expression includes one or more specific vars directly in the current namespace, as if it had been defined there. Referring a var means it no longer requires a namespace qualifier.
Use :refer
when the library being required the predominant focus of that namespace. A good example is clojure.test
which is included to specifically write unit tests.
(ns practicalli.gameboard.handler-test\n :require\n [clojure.test :refer [deftest is testing]]\n [practicalli.gameboard.handler :as handler])\n\n(deftest highscore-test\n (testing \"A description of the test\"\n (is (true? (handler/public-function 42)))))\n
(deftest public-function-in-namespace-test (testing \"A description of the test\" (is (= 1 (public-function arg))) (is (predicate-function? arg))))
Rarely used options - include exclude rename vars These other options on required functions are rarely used in practice. They tend to cause more issues than they solve, so use with care.
:exclude
will prevent a var from being used from a required namespace.
:only
will include only that var from the required namespace.
:rename
changes the name of the original function, if there conflicts
"},{"location":"clojure-cli/projects/namespace/#adding-multiple-namespaces","title":"Adding multiple namespaces","text":"The idiom in Clojure is to include multiple namespaces with just one :require
statement
Here is an example namespace expression with multiple require statements from the duct web framework template
(ns duct-test.main\n (:require [clojure.java.io :as io]\n [com.stuartsierra.component :as component]\n [duct.middleware.errors :refer [wrap-hide-errors]]\n [meta-merge.core :refer [meta-merge]]\n [duct-test.config :as config]\n [duct-test.system :refer [new-system]]))\n
Avoid use form - require should be used
The use
or :use
form is not recommended as it pulls in everything the namespace and everything that the included namespace also included. This can lead to conflicts, especially in larger projects.
As Clojure is typically composed of many libraries, its prudent to only include the specific things you need from another namespace.
"},{"location":"clojure-cli/projects/namespace/#design-refactor","title":"Design & Refactor","text":"When starting a new project all the code is typically in one namespace, unless you are using a template that creates multiple namespaces with sample code.
Practicalli recommends adding comment sections as the code is developed, grouping code by its purpose. As the namespace grows in size and complexity, these groups can be moved into their own namespaces as necessary. A code refactor is much simpler as the code is already grouped logically by purpose.
Code comment sections ;; --------------------------------------------------\n;; State\n\n;; --------------------------------------------------\n\n;; --------------------------------------------------\n;; Helper functions\n\n;; --------------------------------------------------\n\n;; --------------------------------------------------\n;; System / Lifecycle\n\n;; --------------------------------------------------\n
Clojure LSP Snippets
Practicalli Clojure LSP Config defines snippets to create sections within a Clojure file
comment-header
to describe the overall purpose of the namespace
comment-section
creates a start and end comment line and text comment
One pass evaluation
A Clojure file is evaluated from top to bottom, so var (def
, defn
) definitions should come before they are used in the code.
"},{"location":"clojure-cli/projects/rich-comments/","title":"Rich Comments","text":"The (comment ,,,)
form is commonly used to contain living experimental code, so it is often referred to as a rich comment as its purpose is more than just commenting out code.
"},{"location":"clojure-cli/projects/rich-comments/#experimental-design","title":"Experimental design","text":"Whilst iterating through designs, much experimental code can be created which is not (yet) ready to be part of the main namespace.
Experimental code can be written in a (comment ,,,)
form to keep it separate from more finalised implementations.
When a namespace is evaluted, code within the (comment ,,,)
form is not automatically loaded.
Most editors support evaluation of Clojure code within the (comment ,,,)
form, allowing a range of design implementations to be evaluated against each other.
Rich comment blocks are very useful for rapidly iterating over different design decisions by including the same function but with different implementations. Hide clj-kondo linter warnings for redefined vars (def
, defn
) when using this approach.
Practicalli Clojure LSP Config - rich-comment-hotload snippet
;; Rich comment block with redefined vars ignored\n#_{:clj-kondo/ignore [:redefined-var]}\n(comment\n\n (def data-model {:nested {:hash \"map\" :design \"choice\"}})\n (def data-model [{:collection \"of\" :hash \"maps\" :design \"choice\"}\n {:collection \"of\" :hash \"maps\" :design \"choice\"}])\n\n (defn value-added-tax []\n ;; algorithm - initial design)\n\n (defn value-added-tax []\n ;; algorithm - alternate design)\n\n ) ; End of rich comment block\n
"},{"location":"clojure-cli/projects/rich-comments/#design-journal","title":"Design Journal","text":"When the problem domain or libraries selected are relatively unknown, a significant amount of learning and experimentation may be required. This learning can be captured in a separate namespace, often referred to as a design journal.
Creating a journal of the decisions made as code is designed makes the project easier to understand and maintain. Journals avoid the need for long hand-over or painful developer on-boarding processes as the journey through design decisions are already documented.
A design journal can be added as a (comment ,,,)
section at the bottom of each namespace, or more typically in its own namespace.
A journal should cover the following aspects
- Relevant expressions use to test assumptions about design options.
- Examples of design choices not taken and discussions why (saves repeating the same design discussions)
- Expressions that can be evaluated to explain how a function or parts of a function work
The design journal can be used to create meaningful documentation for the project very easily and should prevent time spent on repeating the same conversations.
Practicalli example journal
Design journal for TicTacToe game using Reagent, ClojureScript and Scalable Vector Graphics
"},{"location":"clojure-cli/projects/rich-comments/#snippets","title":"Snippets","text":"clojure-lsp contains a number of snippets to create variations of a comment form.
rich-comment
a basic comment form rich-comment-rdd
comment form that informs clj-kondo to ignore duplicate function definitions, avoids warnings when testing multiple implementations of the same function rich-comment-hotload
- comment form with Clojure CLI library hotloading
"},{"location":"clojure-cli/projects/rich-comments/#migrating-design-to-tests","title":"Migrating design to tests","text":"REPL code experiements within rich comment blocks are often a good source of code that can be converted into formal unit tests.
Example values used to test functions as they are designed can be useful to create meaningful sets of test data, especially when testing edge conditions.
"},{"location":"clojure-cli/projects/rich-comments/#live-examples","title":"Live examples","text":"A rich comment at the end of a namespace can include code that demonstrates how to use the key aspects of the namespace API.
"},{"location":"clojure-cli/projects/package/","title":"Package with Clojure tools.build","text":"The Clojure.org tools.build project is used to build jar files to deploy libraries and uberjar files to run deployed projects (e.g. in Docker containers or directly on an Operating System with Java JVM installed).
Clojure tools.build is a library to define build related tasks using Clojure code.
Practicalli Project Templates includes tools.build configuration
Clojure projects created with Practicalli Project Templates include a build.clj
configuration to build an uberjar of the project.
The make build-jar
runs the clojure -T:build jar
command to build an uberjar.
Java ARchive - jar file A .jar
file is a zip archive of the project containing all the files for running a Clojure project. The archive should contain metatdata files such as Manifest and pom.xml and can contain Clojure sources or compiled class files from the project (or both).
An ubjerjar is .jar
file that also contains all the project dependencies including Clojure. The uberjar is a self-contained file that can be easily deployed and requires only a Java run-time (Java Virtual Machine), using the java -jar project-uberjar.jar
command, with the option to pass arguments to the Uberjar also.
Practicalli Project Build tasksClojure tools.build Practicalli Project templates include a build.clj
configuration with jar
and uberjar
tasks.
Create a runnable Clojure archive
clojure -T:project/build uberjar\n
Create a Clojure library archive
clojure -T:project/build jar\n
tools.build provides an API for pragmatically defining tasks to build Clojure projects.
Create a build.clj
configuration with tasks for building a library jar or runable uberjar.
Define build.clj configuration for tools.build
"},{"location":"clojure-cli/projects/package/tools-build/","title":"Package projects with tools.build","text":"Improved build script examples have been added Please report any issues using the new examples
Clojure.org tools.build is a library to define build related tasks using Clojure code.
The tools.build API provides a consistent interface to access the project configuration (project basis) and common tasks that facilitate building and packaging projects.
Include a build alias and build script in each project to make use of Clojure tools.build:
:build/task
alias adding tools.build library to the class path in the project deps.edn
file build.clj
defines a namespace requiring tools.build, a project configuration and functions as build tasks
Practicalli Project Templates include tools.build tasks Practicalli Project templates include a build.clj
tasks to generate a library jar
or a service uberjar
.
"},{"location":"clojure-cli/projects/package/tools-build/#define-build-alias","title":"Define build alias","text":"Add an alias to the project deps.edn
file which includes the org.clojure/tools.build
project.
:build/task alias created by Practicalli Project Templates
Project deps.edn ;; tools.build `build.clj` built script\n :build/task\n {:replace-paths [\".\"]\n :replace-deps {io.github.clojure/tools.build\n {:git/tag \"v0.9.6\" :git/sha \"8e78bcc\"}}\n :ns-default build}\n
Use Clojure CLI to run any of the tasks defined in the build
namespaces.
clojure -T:build/task task-name\n
tools.build release information Clojure.org tools.build release information shows the current values for git/tag
and :git/sha
Developing code in the build script :replace-paths [\".\"]
includes the build.clj
file on the class path to allow for REPL development of the build tasks
Include :build
alias in the Clojure command when starting the REPL.
clojure -M:build/task:repl/rebel\n
"},{"location":"clojure-cli/projects/package/tools-build/#build-script","title":"Build Script","text":"Create a build.clj
file which defines a namespace requiring tools.build, a project configuration and functions as build tasks
An Uberjar file is built to deploy a Clojure service, e.g. in test, staging or production environment.
A Jar file is built to published a Clojure library to a Maven repository, e.g. Clojars.org, Maven Central or a private Maven repository.
"},{"location":"clojure-cli/projects/package/tools-build/#namespace-definition","title":"Namespace definition","text":"Define the namespace and require the clojure.tools.build.api and any additional libraries.
Service UberjarLibrary Jar Namespace definition with tools.build.api and Pretty Print
build.clj(ns build\n (:require\n [clojure.tools.build.api :as build-api]\n [clojure.pprint :as pprint]))\n
Namespace definition with tools.build.api and Pretty Print
build.clj(ns build\n (:require\n [clojure.tools.build.api :as build-api]\n [deps-deploy.deps-deploy :as deploy-api]\n [clojure.pprint :as pprint]))\n
"},{"location":"clojure-cli/projects/package/tools-build/#build-configuration","title":"Build configuration","text":"Define a hash-map containing keys and values required to build the project.
Service UberjarLibrary Jar Define a project configuration for building an Uberjar file to run a service using the java -jar
command.
The Uberjar can be deployed to run the service in test, staging and production environments.
Clojure Service build tasks
build.clj;; ---------------------------------------------------------\n;; Project configuration\n\n(def project-config\n \"Project configuration to support build tasks\"\n {:class-directory \"target/classes\"\n :main-namespace 'practicalli/project-name/service\n :project-basis (build-api/create-basis)\n :uberjar-file \"target/practicalli-servicename-standalone.jar\"})\n\n(defn config\n \"Display build configuration\"\n [config]\n (pprint/pprint (or config project-config)))\n\n;; End of Build configuration\n;; ---------------------------------------------------------\n
Define a project configuration for building a jar file for deployment on Clojars and Maven Central, or a private repository.
pom-template
is the standard structure for generating a pom.xml file, required by Maven repositories, i.e. Clojars.org and Maven Central project-config
specific values for building the project, e.g. name, version, etc. config
function to pretty print the build configuration
Clojure Library build tasks
build.clj;; ---------------------------------------------------------\n;; Build configuration\n\n(defn- pom-template\n \"Standard structure for a `pom.xml` file, a Maven project configuration \n required to deploy libraries to Clojars.org, Maven Central or private Maven repositories\n https://maven.apache.org/guides/introduction/introduction-to-the-pom.html\"\n [project-version]\n [[:description \"FIXME: add purpose of library.\"]\n [:url \"https://github.com/organisation/project-name\"]\n [:licenses\n [:license\n [:name \"Creative Commons Attribution-ShareAlike 4.0 International\"]\n [:url \"https://creativecommons.org/licenses/by-sa/4.0/\"]]]\n [:developers\n [:developer\n [:name \"Organisation name\"]]]\n [:scm\n [:url \"https://github.com/organisation/project-name\"]\n [:connection \"scm:git:https://github.com/organisation/project-name.git\"]\n [:developerConnection \"scm:git:ssh:git@github.com:organisation/project-name.git\"]\n [:tag (str \"v\" project-version)]]])\n\n\n(def project-config\n \"Project configuration to support build tasks\"\n (let [library-name 'net.clojars.organisation/project-name\n version \"0.1.0-SNAPSHOT\"]\n {:library-name library-name\n :project-version version\n :jar-file (format \"target/%s-%s.jar\" (name library-name) version)\n :project-basis (build-api/create-basis)\n :class-directory \"target/classes\"\n :src-directory [\"src\"]\n :target-directory \"target\"\n :pom-config (pom-template version)}))\n\n\n(defn config\n \"Display build configuration\"\n [config]\n (pprint/pprint (or config project-config)))\n;; End of Build configuration\n;; ---------------------------------------------------------\n
"},{"location":"clojure-cli/projects/package/tools-build/#build-task","title":"Build Task","text":"Service UberjarLibrary Jar Define Clojure functions to run the required build tasks
clean
to remove build artefacts, e.g. target
directory Uberjar
creates a Jar file for a Clojure library, ready for publishing
Clojure Service build tasks
build.clj;; ---------------------------------------------------------\n;; Build tasks\n\n(defn clean\n \"Remove a directory\n - `:path '\\\"directory-name\\\"'` for a specific directory\n - `nil` (or no command line arguments) to delete `target` directory\n `target` is the default directory for build artefacts\n Checks that `.` and `/` directories are not deleted\"\n [directory]\n (when\n (not (contains? #{\".\" \"/\"} directory))\n (build-api/delete {:path (or (:path directory) \"target\")})))\n\n\n(defn uberjar\n \"Create an archive containing Clojure and the build of the project\n Merge command line configuration to the default project config\"\n [options]\n (let [config (merge project-config options)\n {:keys [class-directory main-namespace project-basis uberjar-file]} config]\n (clean \"target\")\n (build-api/copy-dir {:src-dirs [\"src\" \"resources\"]\n :target-dir class-directory})\n\n (build-api/compile-clj {:basis project-basis\n :class-dir class-directory\n :src-dirs [\"src\"]})\n\n (build-api/uber {:basis project-basis\n :class-dir class-directory\n :main main-namespace\n :uber-file uberjar-file})))\n\n;; End of Build tasks\n;; ---------------------------------------------------------\n
Define Clojure functions to run the required build tasks
clean
to remove build artefacts, e.g. target
directory jar
creates a Jar file for a Clojure library, ready for publishing install
a built jar into the local Maven repository, e.g. `~/.m2/repository/ publish
a built jar to Clojars.org
Clojure Library build tasks
build.clj;; ---------------------------------------------------------\n;; Build tasks\n\n(defn clean\n \"Remove a directory\n - `:path '\\\"directory-name\\\"'` for a specific directory\n - `nil` (or no command line arguments) to delete `target` directory\n `target` is the default directory for build artefacts\n Checks that `.` and `/` directories are not deleted\"\n [directory]\n (when (not (contains? #{\".\" \"/\"} directory))\n (build-api/delete {:path (or (:path directory) \"target\")})))\n\n(defn jar \"Run the CI pipeline of tests (and build the JAR).\"\n [config]\n (clean \"target\")\n (let [config (project-config config)\n class-directory (config :class-directory)]\n (println \"\\nWriting pom.xml...\")\n (build-api/write-pom (merge (pom-template config)))\n (println \"\\nCopying source...\")\n (build-api/copy-dir {:src-directory [\"resources\" \"src\"] :target-directory class-directory})\n (println \"\\nBuilding JAR...\" (:jar-file config))\n (build-api/jar config))\n config)\n\n(defn install\n \"Install a built JAR in the local Maven repository, e.g. `.m2/repository`\"\n [config]\n (let [config (project-config config)]\n (build-api/install config))\n config)\n\n(defn publish \n \"Publish the built JAR to Clojars.\" \n [config]\n (let [{:keys [jar-file] :as config} (project-config config)]\n (deploy-api/deploy\n {:installer :remote :artifact (build-api/resolve-path jar-file)\n :pom-file (build-api/pom-path (select-keys config [:library-name :class-directory]))}))\n config)\n\n;; End of Build tasks\n;; ---------------------------------------------------------\n
"},{"location":"clojure-cli/projects/package/tools-build/#resources","title":"Resources","text":" Clojure.org tools.build Guide
Clojure.org tools.build API Docs
Clojure.org tools.build release information
"},{"location":"clojure-cli/projects/templates/","title":"Creating projects from templates","text":"Creating projects using a template is a quick way to get started or create a common . A template will create the project structure, add libraries and even include example code.
deps-new provides Clojure CLI specific templates and Practicalli Project Templates provides production level templates with a REPL Reloaded workflow
deps-new built-in templates The deps-new built-in templates for creating a project - app
- simple project for a running application (uberjar) - lib
- simple project for a library (jar) - scratch
- a deps.edn
file and src/scratch.clj
- template
- project for defining a custom template
Practicalli Project Templates Practicalli Project Templates provide production level templates that include Practicalli REPL Reloaded Workflow tools, Docker & Compose configurations, Makefile tasks for a consistent command line UI and GitHub workflows to manage quality of code and configuration.
practicalli/minimal
- essential tools, libraries and example code practicalli/application
- general Clojure production level project template practicalli/service
- production level web services template with component management, Http-kit, Reitit and Swagger pracicalli/landing-page
- simple clojurescript website with bulma.io CSS and Figheel-main build tool.
clj-new provides Leiningen format templates The Practicalli :project/new
alias provides the seancorfield/clj-new tool which can use a wide range of templates (although some may only create Leinginen projects). This project has been archived and deps-new is the recommended approach.
Migrate to a Clojure CLI project if the template does not include a deps.edn
file
Clojure Projects with the REPL video demonstrates shows how to use clj-new
clj-new
can create projects from deps.edn
and Leiningen templates. A wide range of templates have been created by the Clojure community which can be found by searching on Clojars.org:
- clj-templates website - leiningen and boot templates
- deps.edn projects
- Leiningen projects
"},{"location":"clojure-cli/projects/templates/#add-deps-new","title":"Add deps-new","text":"Add deps-new via a Clojure CLI user alias or install as a tool.
Practicalli Clojure CLI ConfigAlias Definitions :project/create
alias provided by Practicalli Clojure CLI Config runs the seancorfield/deps-new tool to create Clojure CLI specific projects.
:project/create
alias includes the Practicall Project templates , extending the range of available templates
Create the following alias definitions in the Clojure CLI user configuration, e.g. $XDG_CONFIG_HOME/clojure/deps.edn
or $HOME/.clojure/deps.edn
Clojure CLI user deps.edn configuration - :aliases {}
:project/create\n{:replace-deps {io.github.seancorfield/deps-new\n {:git/tag \"v0.5.2\" :git/sha \"253f32a\"}\n io.github.practicalli/project-templates\n {:git/tag \"2023-08-02\" :git/sha \"eaa11fa\"}}\n :exec-fn org.corfield.new/create\n :exec-args {:template practicalli/minimal\n :name practicalli/playground}}\n
"},{"location":"clojure-cli/projects/templates/#create-a-project","title":"Create a project","text":"Open a terminal window and change to a suitable folder and create a project.
Create a project using the :project/create
alias.
The practicalli/minimal
template and practicalli/playground
name are used if :template
and :name
arguments are not specified.
clojure -T:project/create\n
The -T
execution option runs the tool with Clojure.exec which uses keywords to specify the options for creating the project.
Use the form domain/app-or-lib-name
to specify a project name, typically with a company name or Git Service account name as the domain
.
:template
can be one of the deps-new built-in templates (app
, lib
) or one of the Practicalli Project Templates.
Create a project using the practicalli/application
template and random-function name.
clojure -T:project/create :template practicalli/application :name practicalli/random-function\n
"},{"location":"clojure-cli/projects/templates/#run-project-in-a-repl","title":"Run Project in a REPL","text":"Change into the directory and test the project runs by starting a REPL with Terminal REPL
cd playground && clojure -M:repl/rebel\n
A repl prompt should appear.
Type code expressions at the repl prompt and press RETURN to evaluate them.
(+ 1 2 3 4 5)\n
Try the project with your preferred editor Using a Clojure aware editor, open the playground project and run the REPL. Then write code expressions in the editor and evaluate them to see the result instantly.
"},{"location":"clojure-cli/projects/templates/#running-the-project","title":"Running the project","text":"Run project with or without an alias:
clojure -M:alias -m domain.app-name\nclojure -M -m domain.app-name\n
In the project deps.edn
file it can be useful to define an alias to run the project, specifying the main namespace, the function to run and optionally any default arguments that are passed to that function.
:project/run\n{:ns-default domain.main-namespace\n :exec-fn -main\n :exec-args {:port 8888}}\n
Then the project can be run using clojure -X:project/run
and arguments can optionally be included in this command line, to complement or replace any default arguments in exec-args
.
"},{"location":"clojure-cli/projects/templates/design-templates/","title":"Design templates","text":"Create a custom template project for yourself, your team / organisation or an open source project
Either copy one of the Practicalli Project Templates or create a base template project
clojure -T:project/create :template template :name domain/template-name\n
Local only template If a template is only used by yourself locally, then all that is needed is a deps.edn
config with deps-new, resources/domain/template-name/
directory containing a template.edn
and files to make up the new project and optionally a src/domain/template-name.clj
for programmatic transform
"},{"location":"clojure-cli/projects/templates/design-templates/#add-project-files","title":"Add project files","text":"resources/domain/project_name/
directory contains files that are used to create a new project when using the template.
resources/domain/project_name/template.edn
defines the declarative copy rules that manage where files are copied too, allowing for renaming of files and directories.
"},{"location":"clojure-cli/projects/templates/design-templates/#deps-new-template-project","title":"deps-new template project","text":"deps-new specification defined with clojure.spec
(s/def ::root string?)\n(s/def ::description string?)\n(s/def ::data-fn symbol?)\n(s/def ::template-fn symbol?)\n(s/def ::files (s/map-of string? string?))\n(s/def ::open-close (s/tuple string? string?))\n(s/def ::opts #{:only :raw})\n(s/def ::dir-spec (s/cat :src string?\n :target (s/? string?)\n :files (s/? ::files)\n :delims (s/? ::open-close)\n :opts (s/* ::opts)))\n(s/def ::transform (s/coll-of ::dir-spec :min-count 1))\n(s/def ::template (s/keys :opt-un [::data-fn ::description ::root ::template-fn ::transform]))\n
"},{"location":"clojure-cli/projects/templates/design-templates/#use-template-locally","title":"Use template locally","text":"Create projects from the new template locally by defining a Clojure CLI user alias using :local/root that points to the root directory of the template project.
:project/create-local\n {:replace-deps {io.github.seancorfield/deps-new\n {:git/tag \"v0.5.2\" :git/sha \"253f32a\"}\n practicalli/project-templates\n {:local/root \"/home/practicalli/projects/practicalli/project-templates/\"}}\n :exec-fn org.corfield.new/create\n :exec-args {:template practicalli/minimal\n :name practicalli/playground}}\n
Create a new project with the project/create-local
alias
clojure -T:project/create-local :template domain/template-name\n
"},{"location":"clojure-cli/projects/templates/design-templates/#unit-tests","title":"Unit tests","text":"Each template should have a unit test that checks against the deps-new template specification (written in clojure.spec)
Once the unit test pass, create a new project from the template just created
Checks should be made of the following aspects of a new project created with the new template.
- check library dependency versions
- run main and exec functions
- run test runner
- test buld task clean and jar | uberjar
template.edn contains a declarative configuration of the project a template will generate
src/domain/template-name.clj
test/domain/template_name_test.clj
defines a unit test with clojure.test
and clojure.spec
which test the practicalli/template/service/template.edn
configuration.
"},{"location":"clojure-cli/projects/templates/design-templates/#template-specification","title":"Template specification","text":"The template configuration is tested against the org.corfield.new/template
specification
Specification defined with clojure.spec
(s/def ::root string?)\n(s/def ::description string?)\n(s/def ::data-fn symbol?)\n(s/def ::template-fn symbol?)\n(s/def ::files (s/map-of string? string?))\n(s/def ::open-close (s/tuple string? string?))\n(s/def ::opts #{:only :raw})\n(s/def ::dir-spec (s/cat :src string?\n :target (s/? string?)\n :files (s/? ::files)\n :delims (s/? ::open-close)\n :opts (s/* ::opts)))\n(s/def ::transform (s/coll-of ::dir-spec :min-count 1))\n(s/def ::template (s/keys :opt-un [::data-fn ::description ::root ::template-fn ::transform]))\n
"},{"location":"clojure-cli/projects/templates/design-templates/#publish-template","title":"Publish template","text":"Templates are a shared Git repository, so push the template project to GitHub
Include the shared repository within an alias definition within the Clojure CLI user deps.edn configuration, e.g. Practicalli Clojure CLI Config.
Create a new project with the template using the alias.
clojure -T:project/create :template domain/template-name :name domain/project-name\n
:project/crate includes Practicalli Project Templates
Practicalli Clojure CLI Config has been updated to include the practicalli/project-templates dependency, making available all the Practicalli templates.
Default values for template.edn keys can also be defined in the :exec-args {}
of an alias for the project template
:exec-args {:template practicalli/service\n :name practicalli.gameboard/service}\n
"},{"location":"clojure-cli/projects/templates/practicalli/","title":"Practicalli Project Templates","text":"Practicalli Project templates provides tools for a REPL Reloaded Workflow and several production grade project configurations.
:project/create
alias defined in Practicalli Clojure CLI Config provides` provides seancorfield/deps-new tool for creating projects, including the Practicalli Project Templates
clojure -T:project/create \n
"},{"location":"clojure-cli/projects/templates/practicalli/#available-templates","title":"Available Templates","text":"Use the :template
command line argument to specify a project template to generate the new Clojure project.
practicalli/minimal
- essential tools, libraries and example code practicalli/application
- general Clojure production level project template practicalli/service
- production level web services template with Http-kit, Reitit and Swagger. Optional : component
management with :donut
or :integrant
pracicalli/landing-page
- simple clojurescript website with bulma.io CSS and Figheel-main build tool.
Create service project with Donut System components
clojure -T:project/create :template practicalli/service :name practicalli/todo-list :component :donut\n
"},{"location":"clojure-cli/projects/templates/practicalli/#common-template-design","title":"Common Template Design","text":" practicalli/project-templates provide production level templates that include Practicalli tools, Docker & Compose configurations, Makefile tasks for a consistent command line UI and GitHub workflows to manage quality of code and configuration.
"},{"location":"clojure-cli/projects/templates/practicalli/#custom-user-namespace","title":"Custom user namespace","text":"Practicalli dev/user.clj
adds tools to the REPL on start up
mulog_events.clj
custom publisher sends log events to portal portal.clj
launch portal data inspector and set log global context system_repl.clj
Component services e.g. donut-party/system, integrant REPL user.clj
provides help for custom user namespace, loads portal, mulog and tools.namespace.repl to support reloading Clojure code
"},{"location":"clojure-cli/projects/templates/practicalli/#make-tasks","title":"Make tasks","text":"Makefile
defines targets used across Practicalli projects, following the make standard targets for users
all
calling all targets to prepare the application to be run. e.g. all: deps test-ci dist clean deps
download library dependencies (depend on deps.edn
file) dist
create a distribution tar file for this program or zip deployment package for AWS Lambda lint
run lint tools to check code quality - e.g MegaLinter which provides a wide range of tools format-check
report format and style issues for a specific programming language format-fix
update source code files if there are format and style issues for a specific programming language pre-commit
run unit tests and code quality targets before considering a Git commit repl
run an interactive run-time environment for the programming language test-unit
run all unit tests test-ci
test running in CI build (optionally focus on integration testing) clean
remove files created by any of the commands from other targets (i.e. ensure a clean build each time)
Practicalli Makefile also defines docker targets to build and compose images locally, inspect images and prune containers and images.
docker-build
build Clojure project and run with docker compose docker-build-clean
build Clojure project and run with docker compose, removing orphans docker-down
shut down containers in docker compose swagger-editor
start Swagger Editor in Docker swagger-editor-down
stop Swagger Editor in Docker
"},{"location":"clojure-cli/projects/templates/practicalli/#docker","title":"Docker","text":"Docker configuration builds and runs the Clojure project in a Docker container, orchestrating with other services including a Database.
The service and application project templates include the following files
Dockerfile
multi-stage build and run, with JVM optomisations for a Docker container .dockerignore
patterns to opomise copying of files to the docker build image compose.yaml
configuration for orchestrating additional services, e.g. postgresql database
"},{"location":"clojure-cli/projects/templates/practicalli/application/","title":"Practicalli Application template","text":"Create a general Clojure application with REPL Reloaded workflow.
"},{"location":"clojure-cli/projects/templates/practicalli/application/#using-the-project","title":"Using the project","text":"Run the REPL
make repl\n
The REPL prompt is displayed using Rebel for a rich UI experience.
Portal data inspector window is displayed and all evaluation results and mulog events are automatically sent to Portal.
An nREPL server is running in the background for connecting Clojure aware editors.
Run tests (stopping on first failing test)
make test\n
"},{"location":"clojure-cli/projects/templates/practicalli/landing-page/","title":"Practicalli Landing Page","text":"Build simple websites and landing pages using ClojureScript and figwheel-main.
clojure -T:project/create :template landing-page :name practicalli/website-name\n
"},{"location":"clojure-cli/projects/templates/practicalli/landing-page/#using-the-project","title":"Using the project","text":"Run the REPL
make repl\n
Run tests (stopping on first failing test)
make test\n
"},{"location":"clojure-cli/projects/templates/practicalli/landing-page/#template-design","title":"Template design","text":"Configuration files
deps.edn
project dependencies and aliases defining figwheel builds dev.cljs.edn
development build configuration live.cljs.edn
live build configuration (GitHub pages deployment by default) figwheel-main.edn
general figwheel configuration
Clojure code
src/project/landing-page.clj
compose components to render the website src/project/components.clj
functions that define component and associated helper functions src/project/data.clj
data structure passed in part or whole to each component, via the landing-page
.
project.data
namespace defines an example data structure as a static value (def). Use an atom to contain the data structure if the data should be updated by components in the project.
"},{"location":"clojure-cli/projects/templates/practicalli/minimal/","title":"Practicalli Minimal Project Template","text":"A Clojure project with minimal dependencies and example code, useful for experimenting or building a new project from a minimal setup.
"},{"location":"clojure-cli/projects/templates/practicalli/minimal/#using-the-project","title":"Using the project","text":"Run the REPL
make repl\n
The REPL prompt is displayed using Rebel for a rich UI experience.
Portal data inspector window is displayed and all evaluation results and mulog events are automatically sent to Portal.
An nREPL server is running in the background for connecting Clojure aware editors.
Run tests (stopping on first failing test)
make test\n
"},{"location":"clojure-cli/projects/templates/practicalli/service/","title":"Practicalli Service template","text":"Develop web services and APIs using the practicalli/service
template.
clojure -T:project/create :template practicalli/service\n
The practicalli/services
includes:
- http-kit provides an HTTP web server responding to HTTP requests
- reitit routing
- mulog event logging and publisher
- Portal data inspector
- Makefile with common Practicalli tasks
A component system can be included by providing the :component :donut
or :component integrant
command line arguments.
"},{"location":"clojure-cli/projects/templates/practicalli/service/#component-systems","title":"Component systems","text":"Components in the system can be managed during development by evaluating functions in the REPL.
(start)
starts all components in order (stop)
stops all components in order (restart)
reload changed namespaces and restart all components in order (system)
prints out the system configuration
The system-repl.clj
defines the functions to manage components, using the chosen component library, e.g. Donut system, Integrant REPL.
When running the application from the command line, the src/domain/project/service/-main
function calls the initialisation of components and creates a var called running-system
that contains the initialised system components. -main
contains a shutdown hook that responds to SIGTERM signals, triggering a shutdown of components in the running-system
.
Donut SystemIntegrant Include Donut system configuration and REPL functions
clojure -T:project/create :template practicalli/service :component :donut\n
src/domain/project/system.clj
defines the system components dev/system-repl.clj
defines funtions to manage the system components
Each component is defined within the system
namespace in the domain.project.system/main
hash-map. Each component definition has a start and stop function, optionally passing configuration options and environment variables for that component.
Include Integrant system configuration and Integrant REPL functions to support development
clojure -T:project/create :template practicalli/service :component :integrant\n
src/domain/project/system.clj
defines the system components dev/system-repl.clj
defines funtions to manage the system components
Each component is started with an init multi-method with a the specific component name (keyword). Each init
multi-method provides the specific Clojure code to start the component.
A halt
multi-method is provided for each component that requires shutting down, e.g. http server, database pool, logging publisher, etc.
During development and testing, the components are managed from the user
namespace by evaluating the (start)
, (stop) or (restart)
functions.
"},{"location":"clojure-cli/projects/templates/practicalli/service/#using-the-project","title":"Using the project","text":"Run the REPL
make repl\n
The REPL prompt is displayed using Rebel for a rich UI experience. Portal data inspector window is displayed and all evaluation results and mulog events are automatically sent to Portal.
An nREPL server is running in the background for connecting Clojure aware editors.
Run tests (stopping on first failing test)
make test\n
"},{"location":"clojure-cli/repl/","title":"Clojure REPL","text":"The REPL is the environment in which all Clojure code runs, whether that be during development, testing or in production systems.
A Terminal REPL provides a simple way to interact with the REPL, sending code expressions for evaluation and returning results.
Use a terminal REPL for
- quick experiments
- long running processes (e.g. http severs running Clojure)
- interact with the REPL state and manage components (e.g restarting system components, querying UI component state or services system state).
- a REPL process separate from a specific editor control
REPL connected Editor
A Clojure aware editor connected to the REPL is used for the majority of Clojure development. One or more expressions from a source code file can be sent to the REPL for evaluation, displaying the results inline.
"},{"location":"clojure-cli/repl/#rebel-terminal-repl-ui","title":"Rebel Terminal REPL UI","text":"Rebel is a REPL terminal UI that provides auto-completion, function call syntax help and documentation, themes and key binding styles to enhance the development experience. Clojure tools also include a REPL with a minimal interface by default.
"},{"location":"clojure-cli/repl/#install-rebel","title":"Install Rebel","text":"Practicalli Clojure CLI ConfigDefine Rebel AliasClojure CLI REPL :repl/rebel
alias is provided by Practicalli Clojure CLI Config to run rebel readline.
:repl/reloaded
alias runs Rebel with tools to support the Practicalli REPL Reloaded, providing a custom REPL startup with support for Portal data inspector and Mulog event logs.
Both aliases will start an nREPL server for Clojure aware editors to connect.
Rebel libraries are downloaded the first time the Rebel alias is used.
Add an alias called :repl/rebel
to the user deps.edn
configuration, e.g. ~/.config/clojure/deps.edn
Basic Rebel terminal UI alias
~/.config/clojure/deps.edn:repl/rebel \n{:extra-deps {com.bhauman/rebel-readline {:mvn/version \"0.1.5\"}}\n :main-opts [\"-m\" \"rebel-readline.main\"]}\n
Rebel terminal UI alias with nREPL for editor connection
~/.config/clojure/deps.edn:repl/rebel\n{:extra-deps {nrepl/nrepl {:mvn/version \"1.0.0\"}\n cider/cider-nrepl {:mvn/version \"0.31.0\"}\n com.bhauman/rebel-readline {:mvn/version \"0.1.4\"}}\n :main-opts [\"-e\" \"(apply require clojure.main/repl-requires)\"\n \"--main\" \"nrepl.cmdline\"\n \"--middleware\" \"[cider.nrepl/cider-middleware]\"\n \"--interactive\"\n \"-f\" \"rebel-readline.main/-main\"]}\n
Practicalli Clojure CLI Config contains aliases for a basic terminal UI and a headless (non-interactive) terminal UI, each starting an nREPL server for editor connection.
Alias definitions for a basic terminal UI REPL
Interactive client REPL with nREPL server for Clojure Editor support
:repl/basic\n{:extra-deps {nrepl/nrepl {:mvn/version \"1.0.0\"}\n cider/cider-nrepl {:mvn/version \"0.28.7\"}}\n :main-opts [\"-m\" \"nrepl.cmdline\"\n \"--middleware\" \"[cider.nrepl/cider-middleware]\"\n \"--interactive\"]}\n
Headless REPL with nREPL server for Clojure Editor support
:repl/headless\n{:extra-deps {nrepl/nrepl {:mvn/version \"1.0.0\"}\n cider/cider-nrepl {:mvn/version \"0.28.7\"}}\n :main-opts [\"-m\" \"nrepl.cmdline\"\n \"--middleware\" \"[cider.nrepl/cider-middleware]\"]}\n
To have a basic terminal UI REPL prompt use the :repl/basic
alias to start a REPL process with nREPL connection.
clj -M:repl/basic\n
To only have the REPL process without a REPL prompt, use the :repl/headless
aliase to start a REPL process with nREPL connection. This approach is useful to separate the REPL output from the editor whilst keeping all the interaction with the REPL via the editor.
clj -M:repl/headless\n
Terminal REPL and Editor Including an nREPL server when starting the REPL allows clojure ware editors to connect to the REPL process, providing a more effective way to write and extend Clojure code.
An external REPL can still be of use even when only evaluating code in a Clojure editor. Separating the REPL process from the editor process allows the editor to be closed, upgraded or swapped for a different editor without having to end the REPL session. Different editors could be connected to the same REPL to use particular features they provide.
A REPL process can be long running, staying alive for days, weeks or months when working on larger projects. Avoiding stop and start of the REPL maintains state in the REPL, maintaining the flow of the Clojure workflow.
"},{"location":"clojure-cli/repl/#customize-rebel-readline","title":"Customize Rebel Readline","text":":repl/help
in the repl prompt shows the Rebel configuration options
Set configuration options in a rebel_readline.edn
file, in $XDG_CONFIG_HOME/clojure/
or $HOME/.clojure
Practicalli Rebel Readline Configuration options $XDG_CONFIG_HOME/clojure/rebel_readline.edn;; ---------------------------------------------------------\n;; Rebel Readline Configuration\n;;\n;; Customise use and appearance\n;; ---------------------------------------------------------\n\n{;; Vi or Emacs style key-map\n ;; :viins or :emacs. Default :emacs\n :key-map :viins\n\n ;; Color theme - light or dark\n ;; :color-theme :light-screen-theme\n :color-theme :dark-screen-theme\n\n ;; Enable syntax highlight. Default true}\n :hihighlight true\n\n ;; Enable complete on tab. Default true}\n :completion true\n\n ;; Enable function documentation Default true\n :eldoc true\n ;; auto indent code on newline. Default true}\n :indent true\n\n ;; rebind root *out* during read to protect linereader, Default true}\n :redirect-output true\n\n ;; Custom key-bindings applied after all other \n :key-bindings {}}\n
"},{"location":"clojure-cli/repl/#next-steps","title":"Next Steps","text":" Code In The REPL
Managing Libraries In The REPL
Help In The REPL
Custom REPL Startup
REPL Uncovered
"},{"location":"clojure-cli/repl/coding/","title":"Coding in the REPL","text":"Clojure code can be typed into the REPL directly and the result instantly returned. Code can also be loaded from a project source code files, to run pre-written code.
Clojure Editors are the main tool for writing code An editor connected to a Clojure REPL and evaluating from source code files is the most effective way for writing Clojure code.
Evaluating code in an editor automatically uses the correct namespace, avoiding the need to change namespaces or fully qualify function calls. Evaluation results can be shown in-line, as comments next to the code or in a data inspector.
Editors provide structural editing and Clojure syntax checking, along with general editor features. and
"},{"location":"clojure-cli/repl/coding/#using-the-repl","title":"Using the REPL","text":"Use the clojure
command to start a REPL with Rebel, or the clj
wrapper with the Clojure CLI REPL (requires rlwrap
binary).
Rebel REPLClojure CLI REPL Start a Clojure REPL with Rebel terminal UI which also starts an nREPL server which a Clojure editor can connect too.
clojure -M:repl/rebel\n
A REPL prompt displays ready to evaluate a Clojure expression.
Start a Clojure REPL with a basic UI which also starts an nREPL server which a Clojure editor can connect too.
clj -M:repl/basic\n
The clj
wrapper requires rlwrap
binary.
A REPL prompt displays ready to evaluate a Clojure expression.
Project dependencies automatically downloaded on REPL start When a REPL is started from the root of a Clojure project the project dependencies are automatically downloaded (unless previously downloaded to the local maven cache, .m2/
) and project specific paths are added, e.g. src
tree.
Use REPL with a Clojure project A REPL can run without a Clojure project, however, libraries and code are simpler to manage within project source and configuration files.
"},{"location":"clojure-cli/repl/coding/#repl-start-state","title":"REPL start state","text":"The Clojure REPL always starts in the user
namespace.
During startup the the clojure.core
functions are required (made available) in the user namespace, so (map inc [1 2 3])
can be called without specifying the clojure.core
namespace in which those functions are defined.
If clojure.core were not required, then the expression would be (clojure.core/map clojure.core/inc [1 2 3])
"},{"location":"clojure-cli/repl/coding/#evaluating-code","title":"Evaluating code","text":"Type Clojure code at the => user
REPL prompt
Press Enter
to evaluate the code and see the result.
Up and Down navigate the REPL history, providing an efficient way to evaluate the same code many times.
In Rebel, typing part of function name shows matches available, Tab to cycle through the choices, Enter to select.
"},{"location":"clojure-cli/repl/coding/#load-code-from-file","title":"Load code from file","text":"Clojure code is usually saved in files and each file has a namespace definition that matches the file path, using the ns
function. The file src/practicalli/playground.clj
has the namespace practicalli.playground
(ns practicalli.playground)\n
Requiring the namespace of a file will evaluate (load) the code from that file in the REPL.
(require 'practicalli.playground)\n
Functions defined in that namespace can be called using their fully qualified names. e.g. if the namespace contains a function called main
, that function can be called using (practicalli.playground/main)
.
Change namespaces Change the namespace to practicalli.playground
to call functions defined in that namespace by their unqualified function name, eg. (main)
, rather than the fully qualified name, e.g. (practicalli.playground/main)
in-ns
will change change the current namespace to the one specified as an argument.
(in-ns 'practicalli.playground)\n
Now the (main)
function can be called without having to include the full namespace name.
Typically it is more efficient to stay in the user
namespace and require all other namespaces required.
"},{"location":"clojure-cli/repl/coding/#reload-code-changes","title":"Reload code changes","text":"The :reload
option to require
will load in any changes to a namespace that happened outside of the REPL, eg. using an editor to change the source code in the file.
(require 'practicalli.playground :reload)\n
Use the :verbose
option when issues occur loading a particular namespace. As the namespace being required may also require other namespaces, multiple namespaces may be loaded from one require
expression.
:verbose
shows a full list of the namespaces being loaded.
(require 'practicalli.playground :reload :verbose)\n
Reload in Terminal REPL for unconnected editor When using an editor that is not connected to the Clojure REPL, then reloading is an effective way of updating the code with all the changes saved in the file.
"},{"location":"clojure-cli/repl/coding/#close-repl","title":"Close REPL","text":":repl/quit
at the REPL prompt will end the REPL session and all code not saved to a file will be lost.
Ctrl+c if the repl process does not return to the shell prompt.
"},{"location":"clojure-cli/repl/coding/#next-steps","title":"Next steps","text":"Managing Library dependencies in REPL
"},{"location":"clojure-cli/repl/help/","title":"Help at the REPL","text":"rebel readline provides tools to help you discover and use functions from clojure.core and any other libraries you add to the REPL.
:repl/help
will show all the commands available for rebel readline
Tab to autocomplete the current characters into a function name. All functions that match the characters will be show, allowing quick discovery of functions available. Typing in the first few characters of a function and press
Moving the cursor after the name of a function will show the signatures available, so a function can be called with the correct number and form of arguments.
Ctrl+C+Ctrl+d on a function name shows the docstring to help understand the functions purpose.
clojure.repl/doc
function also shows the docstring of a function (clojure.repl/doc doc)
Ctrl+C+Ctrl+a on a name shows all the possible matching functions to help you discover what is available. Tab through the list of matches, Enter to select a function
"},{"location":"clojure-cli/repl/help/#rebel-commands","title":"Rebel Commands","text":"Type :repl/help
or :repl
TAB to see a list of available commands.
Keybinding Description :repl/help
Prints the documentation for all available commands. :repl/key-bindings
search or list current key bindings :repl/quit
Quits the REPL :repl/set-color-theme
Change the color theme :dark-screen-theme
:light-screen-theme
:repl/set-key-map
Change key bindings to given key-map, :emacs
:vicmd
:viins
:repl/toggle-color
Toggle ANSI text coloration on and off :repl/toggle-completion
Toggle the completion functionality on and off :repl/toggle-eldoc
Toggle the auto display of function signatures on and off :repl/toggle-highlight
Toggle readline syntax highlighting on and off :repl/toggle-indent
Toggle the automatic indenting of Clojure code on and off"},{"location":"clojure-cli/repl/help/#key-bindings","title":"Key-bindings","text":"Keybinding Description Ctrl-C
aborts editing the current line Ctrl-D
at the start of a line => sends an end of stream message TAB
word completion or code indent when cursor in whitespace at the start of line Ctrl-X_Ctrl-D
Show documentation for word at point Ctrl-X_Ctrl-S
Show source for word at point Ctrl-X_Ctrl-A
Show apropos for word at point Ctrl-X_Ctrl-E
Inline eval for SEXP before the point Examine key-bindings with the :repl/key-bindings
command.
"},{"location":"clojure-cli/repl/libraries/","title":"Using Clojure libraries in the REPL","text":"A library should be included as a dependency in order to use it within the REPL.
Add library dependencies to the top level :deps
key in a project deps.edn
configuration file, or add via an alias if the library is use at development time.
Aliases from a user configuration can also add optional libraries when running a REPL, e.g. Practicalli Clojure CLI config
{:paths [\"src\" \"resources\"]\n\n :deps\n {org.clojure/clojure {:mvn/version \"1.10.3\"}}\n\n :aliases\n {\n :database/h2\n {:extra-deps {com.h2database/h2 {:mvn/version \"2.1.210\"}\n com.github.seancorfield/next.jdbc {:mvn/version \"1.2.772\"}}}\n #_()}\n
Finding libraries Search for community libraries via the Clojars.org website
clojure -M:search/libraries pattern
where pattern is the name of the library to search for. Copy the relevant results into the project deps.edn
file.
clojure -M:search/libraries --format:merge pattern
will automatically add the library into the deps.edn
file.
clojure -X:deps find-versions :lib fully.qualified/library-name :n 5
returns the last 5 versions of the given library.
"},{"location":"clojure-cli/repl/libraries/#include-library","title":"Include library","text":"Open a terminal and change to the root of the Clojure project directory, where the deps.edn
file can be found.
Start the REPL including the :database/h2
alias to include every library defined in the :deps
key and libraries in the :database/h2
alias. This example is using rebel readline rich terminal UI
clojure -M:repl/rebel\n
This command will include
Add aliases to include optional libraries, such as those used for development. In this example, the H2 database and next.jdbc libraries are included along with those libraries in the :deps
key of deps.edn
clojure -M:database/h2:repl/rebel\n
"},{"location":"clojure-cli/repl/libraries/#load-namespace","title":"Load namespace","text":"At the REPL prompt, require a namespace from the project to load all the code from that namespace and any namespaces required.
If a project was created with the command clojure -T:project/new :template app :name practicalli/status-monitor
then the main namespace will be practicalli.status-monitor
(require '[practicalli.status-monitor])\n
The require
function loads all the code from the main namespace. When an ns
form is read, required namespaces in the ns
form are also loaded.
"},{"location":"clojure-cli/repl/libraries/#reloading-namespace","title":"Reloading namespace","text":"Clojure is a dynamic environment, so changes to function definitions (defn
) and shared symbol names (def
) can be updated without restarting the REPL.
require
loads the code from the specified namespace. Using the :reload
option forces the namespace to be loaded again, even if it was already loaded.
When changes are made to a namespace in the source code file, :reload
ensures those changes become the code running in the REPL
(require '[practicalli.status-monitor] :reload)\n
If errors occur when loading or reloading the namespace with require, the :verbose
option will show all the namespaces that are loaded. This may show issues or help track down conflicting namespaces or functions.
(require '[practicalli.status-monitor] :reload :verbose)\n
"},{"location":"clojure-cli/repl/libraries/#hotload-libraries","title":"Hotload libraries","text":"Hotload Libraries in the REPL add-libs
function from the clojure.tools.deps.alpha
library is an experimental approach to hot-loading library dependencies without having to restart the REPL or add those dependencies to the project deps.edn
. This provides a simple way to try out libraries.
hotload libraries secion for more details and how to use with Clojure editors.
Start a REPL session using Clojure CLI with the :lib/hotload alias
, including rebel readline for an enhance REPL terminal UI.
clojure -M:lib/hotload:repl/rebel\n
Require the clojure.tools.deps.alpha
library and refer the add-libs
function. The add-libs
function can then be called without having to use an alias or the fully qualified name.
(require '[clojure.tools.deps.alpha.repl :refer [add-libs]])\n
Hotload a library into the REPL using the add-lib
function in the following form, where domain/library
is the fully qualified name of the library and RELEASE
is a string of the version number of that library to use.
(add-libs '{domain/library {:mvn/version \"RELEASE\"}})\n
Multiple libraries can be hot-loaded in a single add-libs
expression
(add-libs '{hhgttg/meaning {:mvn/version \"4.2.0\"}\n eternity/room {:mvn/version \"1.0.1\"}})\n
"},{"location":"clojure-cli/repl/libraries/#hotload-hiccup-in-a-terminal-repl","title":"Hotload hiccup in a terminal REPL","text":"The hiccup library converts clojure structures into html, where vectors represent the scope of keywords that represent html tags.
Load the hiccup library using add-libs
(add-libs '{hiccup/hiccup {:mvn/version \"2.0.0-alpha2\"}})\n
Require the hiccup library so its functions are accessible from the current namespace in the REPL.
(require '[hiccup.core :as hiccup])\n
Enter an expression using the hiccup/html
function to convert a clojure data structure to html.
(hiccup/html [:div {:class \"right-aligned\"}])\n
The hiccup expression returns a string of the html code.
"},{"location":"clojure-cli/repl/repl-uncovered/","title":"Read, Evaluate Print Loop (REPL)","text":"The REPL provides a fast, powerful and fun way to develop code and is the hard of the Clojure developers workflow. The REPL allows you to quickly test out designs and your domain knowledge of the system you are building, easily accommodating multiple designs to help you evaluate the best approach.
Starting a REPL is the first thing you do after creating or downloading a project.
The REPL allows you to run any existing code, write new code and change code. Each time you can see the results of your code instantly.
The REPL can run all of your code or simply get the result of an individual expression. You can inspect run-time values and continually develop your code without having to restart each time.
Hint If you are not using the REPL for your Clojure development you are missing out on a highly productive workflow. Once you start using a REPL as part of you development cycle you will feel lost without one.
"},{"location":"clojure-cli/repl/repl-uncovered/#how-the-repl-works-simple-version","title":"How the REPL works (simple version)","text":"A Clojure REPL has 4 stages:
- Read - read in the code
- Evaluate - evaluate the code
- Print - show the results
- Loop - on to the next expression
Its useful to understand the difference between Read and Evaluate, especially when you get as far as writing macro's for Clojure.
"},{"location":"clojure-cli/repl/repl-uncovered/#the-reader","title":"The Reader","text":"The Reader parses the Clojure source code, form by form, producing the Clojure data structures an [Abstract Syntax Tree] (AST).
Due to the syntax of Clojure, much of the source code is already in the right structure. Any macros will be expanded into its Clojure structure.
These data structures are then evaluated: Clojure traverses the data structures and performs actions like function application or var lookup based on the type of the data structure.
For example, when Clojure reads the text (+ 1 2), the result is a list data structure whose first element is a + symbol, followed by the numbers 1 and 2. This data structure is passed to Clojure\u2019s evaluator, which looks up the function corresponding to + and applies that function to 1 and 2.
"},{"location":"clojure-cli/repl/repl-uncovered/#the-reader_1","title":"The Reader","text":"Hint Clojure is a homoiconic language, which is a fancy term describing the fact that Clojure programs are represented by Clojure data structures. This is a very important difference between Clojure and most other programming languages. It means that Clojure is defined in terms of the evaluation of data structures and not in terms of the syntax of character streams/files.
It is quite common, and easy, for Clojure programs to manipulate, transform and produce other Clojure programs.
The reader has syntax defined in terms of characters, and the Clojure language has syntax defined in terms of symbols, lists, vectors, maps etc. The reader is represented by the function read, which reads the next form (not character) from a stream, and returns the object represented by that form.
There are also Reader Macros that define special rules on top of the Clojure syntax. They give the language some additional syntax sugar, making your Clojure code compact. See the reference section on reader macros for more information
"},{"location":"clojure-cli/repl/repl-uncovered/#evaluator","title":"Evaluator","text":"The Evaluator takes the data structure as an argument (from the Reader) and processes it using rules corresponding to the data structure\u2019s type, returning the result.
To evaluate a symbol, Clojure looks up what the symbol refers to.
To evaluate a list, Clojure looks at the first element of the list and calls a function, macro, or special form.
Any other values including strings, numbers and keywords simply evaluate to themselves.
Hint Read the section on Reading, Evaluation and Macros from BraveClojure to see examples of the REPL process.
"},{"location":"clojure-cli/repl/troubleshooting/","title":"Troubleshooting the REPL","text":"The aspects to consider when a REPL process fails to run are:
- Some code expressions are not correct
- Dependencies are not available
- Project (or editor) misconfigured
"},{"location":"clojure-cli/repl/troubleshooting/#code-expression-failing","title":"Code expression failing","text":"All code in the project must compile and be syntactically correct, even if that code is in a rich (comment ,,,)
block.
A Clojure expression following a Reader comment, #_
does not have to compile, however it must be syntactically correct, i.e. balanced parentheses.
Add a line comment, ;;
, to any code that is suspected of not compiling or being syntactically incorrect (or delete that code).
"},{"location":"clojure-cli/repl/troubleshooting/#editor-repl-fails-to-start","title":"Editor REPL fails to start","text":"If using a jack-in approach with the editor to start the repl, run a terminal UI REPL with an nREPL server and try connecting to that REPL from the editor.
Clojure CLI repl - rebel terminal UI
clojure -M:repl/rebel\n
Then require the main namespace and see if there are issues, optionally using :verbose to see which libraries are being loaded.
(require '[practicalli.service] :verbose)\n
If the REPL runs correctly, it is likely the editor configuration is missing something or is incorrect. Check the configuration for running a Clojure project with the editor.
"},{"location":"clojure-cli/repl/troubleshooting/#terminal-ui-repl-fails-in-project","title":"Terminal UI REPL fails in project","text":"If the REPL does not run correctly or the namespace fails to load, run a repl without any extra development dependencies (tooling, dev libraries, etc) and load the main namespace
clj\n
"},{"location":"clojure-cli/repl/troubleshooting/#repl-doesnt-start-in-any-project","title":"REPL doesnt start in any project","text":"Run the clojure
command in a directory that is not part of any existing Clojure project. This will run the REPL with only the org.clojure/clojure
dependency
Run clojure -Sdescribe
to check that the Clojure CLI is using the correct configuration files and is the expected version.
If a REPL prompt appears, then Clojure CLI is working. If a REPL prompt does not appear, then reinstall the Clojure CLI or upgrade to a newer version.
Clojure CLI install - Practicalli Guide
"},{"location":"clojure-cli/repl/troubleshooting/#repl-starts-but-requiring-code-fails","title":"REPL starts but requiring code fails","text":"Creating a new project is a fast way to check development tooling is working correctly. A project can be created with clojure -T:project/create
(with Practicalli Clojure CLI Config installed)
If a REPL process starts correctly for a new project but not the existing project, then its most likely one or more expressions in the existing project that are causing an error or the project deps.edn
configuration.
Copy the deps.edn
configuration from the existing project to the root of the new project (or just the :deps
section of the deps.edn
configuration). Run the REPL again using the clojure
command. If the REPL fails then it is likely an issue with the exiting projects deps.edn
file or one of the dependencies
"},{"location":"clojure-cli/repl/troubleshooting/#dependency-issues","title":"Dependency issues","text":"Projects typically depend on many other libraries and sometimes those libraries depend on other libraries too.
When running the clojure
command to run a terminal UI REPL, libraries are retrieved from remote repositories (Maven Central, Clojars.org) and stored in a local cache ~/.m2/repositories
If a dependency is not available then a warning should state which library cannot be downloaded and from which repository
Check the extent of the dependencies for the existing project:
clojure -Stree\n
Use the antq tool to check for a newer version of a dependency
clojure -T:project/outdated\n
If libraries are likely to become unavailable (i.e. old versions) then consider creating a local repository service with Artefactory or Nexus, which can share library depenencies between development teams of an organisation.
"},{"location":"clojure-editors/","title":"Editors for Clojure development","text":"The best editor to use for learning Clojure is the editor already familiar with (or want to learn).
Use SublimeText & ClojureSublimed if unsure where to start as it will be the simplest tool to use.
"},{"location":"clojure-editors/#clojure-editor-features","title":"Clojure editor features","text":"An ideal Clojure editor includes the these core features
- running / connecting to a REPL process
- evaluation results inline or in a repl window (fast feedback on what the code does)
- syntax highlighting (including highlight of matching parens)
- structural editing to ensure parens are balanced when writing and refactor code
- data inspector to visualise large and nested data, or connection to data inpector tools
"},{"location":"clojure-editors/#clojure-aware-editors","title":"Clojure aware editors","text":"Emacs (Spacemacs, Doom, Prelude), Neovim (Conjure) and VSCode (Calva) are the most common open source Editors for Clojure and ClojureScript development.
SublimeText and IntelliJ are commercial editors (with limited free editions) which also provide Clojure support
EmacsNeovimVSCodeSublimeTextPulsar (Atom)Intellij Emacs is a very powerful editor with thousands of packages enabling a person to do almost any digital task concievable. Emacs is highly extensible via the ELisp programming language used to write configuration and the numerous Emacs packages. Native Compilation of Emacs packages dramatically speeds up many common tasks.
Emacs uses CIDER and Clojure LSP for a feature rich clojure development experience.
Use one of the popular community configurations for Emacs or visit the CIDER documentation to learn how to add Clojure support to Emacs.
SpacemacsPrelude EmacsDoom EmacsVanilla Emacs & Cider Spacemacs is a community configuration bringing Emacs features and Vim style editing together. Spacemacs uses a mnemonic menu system that makes it easy to learn and provides detailed documentation for configuring and using Emacs.
Practicalli Spacemacs provides a guide to Clojure development, vim-style editing, documenting with org-mode, Git version control with Magit, Issues & Pull Requests with Forge and dozens of other features.
Practicalli Spacemacs Config contains a customised configuration for Clojure development and supporting tools.
Free Desktop XDG ConfigClassic Config git clone https://github.com/practicalli/spacemacs.d.git $XDG_CONFIG_HOME/spacemacs`\n
git clone https://github.com/practicalli/spacemacs.d.git $HOME/.spacemacs.d`\n
The Practicalli configuration should replace the ~/.spacemacs
file if it exists
Spacemacs install guide - Practicalli Spacemacs
Emacs Prelude is an easy to use Emacs configuration for Emacs newcomers and lots of additional power for Emacs power users, from the author of CIDER - the definitive Clojure IDE for Emacs.
Prelude uses the traditional chorded key bindings to drive Emacs, e.g. Ctrl+c Ctrl+c to evaluate the current top-level form.
Prelude Install Guide
Doom Emacs is a community configuration for Emacs that provides a minimalistic configuration that is readily customisable. Doom Emacs is most suited to those coming from Vim and have a strong experience for multi-modal editing.
Practicalli Doom Emacs Book
Practicalli Doom Emacs Config contains a customised configuration for Clojure development and supporting tools.
Free Desktop XDG ConfigClassic Config git clone https://github.com/practicalli/doom-emacs-config.git $XDG_CONFIG_HOME/doom`\n
The Practicalli configuration should replace the ~/.config/doom/
directory created by the doom install
command. git clone https://github.com/practicalli/doom-emacs-config.git $HOME/.doom.d`\n
The Practicalli configuration should replace the ~/.doom.d/
directory created by the doom install
command.
Emacs 29 is recommended as it includes native compilation support and optomised JSON support which is valuable for Language Server Protocol servers.
Emacs is available for Linux, MacOSX and Windows.
Ubuntu / DebianHomebrew / MacOSXWindowsMsys2 apt-cache show emacs
to check available versions of Emacs in the Ubuntu package manager. If version 28 is available, install Emacs using the Ubuntu package manager.
sudo apt install emacs\n
Additional versions of Emacs are available via the Ubuntu Emacs Team Personal Package Archive.
sudo apt install emacs-snapshot
package to use the latest nightly build of Emacs, although be aware that some things may break.
Build Emacs 29 from source Building Emacs 29 from source code on Ubuntu is relatively straight forward task, although it will take a little time to compile. Building Emacs allows customisation of some features, such as native compilatin of elisp to enhance the performance of Emacs.
Emacs Plus from Homebrew provides many options, including native compilation and Spacemacs Icon for application launchers.
brew tap d12frosted/emacs-plus`\nbrew install emacs-plus@28 --with-native-comp --with-spacemacs-icon\n
Emacs.app is installed to: /usr/local/opt/emacs-plus@28
Optionally run Emacs plus as a service
brew services start d12frosted/emacs-plus/emacs-plus@28\n
Run emacs
Get a hot cup of something as Emacs native compilation compiles all the things.
The Spacemacs README lists other options for MacOSX.
Download Emacs-28.2 from the GNU repository and extract the zip file to %AppData%/local/Programs/emacs
.
Alternatively, if you are using the Chocolatey package manager then install Emacs version 28
Add the Emacs directory to the PATH
variable in your user account environment variables.
To start Emacs run the command runemacs.exe
. You can also pin this to the start menu or task bar.
Access to common Unix tools Command line tools, such as diff
, are used by Emacs. To have these command line tools available in Windows, install Emacs as above but then run emacs from a Unix shell such as GitBash.
Install Emacs (64bits build) with the following:
pacman -S mingw-w64-x86_64-emacs\n
Once Emacs is installed, add the cider package for essential Clojure support.
Cider Install Guide
Neovim is a hyper-extensible text editor that runs in a terminal, configured with the Lua programming language. Configuration can also be written in Fennel (a lisp dialect), using nfnl to generate Lua code.
Neovim is based on multi-model editing (e.g. normal, insert, visual editing states) providing a highly effective tool for writing code, configuration and documentation.
Neovim includes Treesitter which understands the syntax of a great many programming and configuration languages, which can be coupled with analysist from Language Sever Protocol (LSP) servers to provide live feedback on code quality.
Conjure provides Clojure interactive (REPL) development, supporting Clojure CLI, Leiningen and Babashka projects (as well as several other Lisp dialects and interesting languages)
Try the Conjure interactive :ConjureSchool
tutorial which only requires a recent version of neovim
curl -fL conjure.fun/school | sh\n
:q
to quit the tutorial.
Practicalli AstroNvimPracticalli NeovimSpaceVimVimIced Practicalli Neovim - AstroNvim install
AstroNvim community configuration for Neovim provides an engaging UI, using Lazy plugin manger and Mason to manage LSP servers, format & lint tools.
Practicalli AstroNvim Config provides a user configuration for Astronvim, including Conjure, parinfer, LSP server and treesitter parser for Clojure development.
Practicalli Neovim provides an install and user guide for Neovim and Conjure for Clojure development, folloiwng a REPL driven workflow.
Archived project - to be reimplemented using nfnl rather than aniseed. Recommend using AstroNvim instead.
Practicalli Neovim Config Redux configuration for Neovim which adds a range of Neovim plugins for a rich development experience.
- mnemonic of key bindings to make adoptiong Neovim easier
- visual navigation for files, project, environment variables and any other list items
- version control and GitHub issue & pull request management
Conjure provides interactive environment for evaluating Clojure code and providing inline results (or see results in an Heads Up Display or Log buffer).
Practicalli Neovim provides an install and user guide for Neovim and Conjure for Clojure development, folloiwng a REPL driven workflow.
SpaceVim is a fully featured vim experience that includes a minimal Clojure development environment based around vim-fireplace
Follow the Quick Start Guide to install SpaceVim
Add the Clojure layer to the SpaceVim custom configuration file ~/.SpaceVim.d/init.toml
[[layers]]\n name = \"lang#clojure\"\n
SpaceVim quickstart guide SpaceVim - Clojure Layer
Interactive Clojure Environment for Vim8/Neovim, aimed at the more experienced Vim/Neovim user.
vim-iced uses vim-sexp for structural editing
vim-plug is required to install the vim-iced packages.
vim-iced documentation
VS Code is a freely available editor build on open source and available for Linux, MacOS and Microsoft Windows.
VSCode Getting Started Guide
VS Code has a large marketplace of extensions, proiding additional tools for a complete development environment.
Calva is the most commonly used extension for Clojure support and aims to provide similar features to Emacs Cider (and uses some of the same Clojure libraries).
Clojure CLI User Aliases not directly supported
Calva does not support Clojure CLI user aliases directly (only project deps.edn). A JSON mapping must be added to the Calva configuration for each user alias (duplicating configuration)
Practicalli recommends starting the Clojure REPL in a terminal and specifying the required Clojure CLI user aliases, using Calva connect once the REPL has started.
VSpaceCode provides a mnemonic menu to drive VS Code by keyboard alone, vim editing and rich Git client (edamagit). VSpaceCode extension also provides key bindings for common Calva commands. Users experienced with Neovim and Emacs (Spacemacs / Doom) will find this extension makes VS Code easiter to use than vanilla VS Code or VS Code with an Neovim backend.
Clover provides a mininal, highly customisable environment using Socket REPL.
CalvaVSpaceCodeClover The Calva extension adds Clojure REPL support to VS Code editor, including Clojure LSP, formatting, structural editing and many other features.
Calva is under active development and the #calva channel on the Clojurians Slack community can be supportive.
Calva Getting Started Guide Calva - VS Code Marketplace
VSpaceCode is a Spacemacs-like community configuration for Microsoft VS Code. Drive VS Code entirely from the keyboard, using easy to remember mnemonic keys for all commands and full vim-stile editing tools.
Calva extension must be added as it is not part of VSpaceCode, although Calva commands are included in the VSpaceCode mneomoic menu when a Clojure file is open.
Edamagit is a sophisticated text based Git client (like magit for Emacs) is also included in the VSpacemacs extension.
Practicalli VSpaceCode install guide Practicalli VSpaceCode user guide
Clover is a Socket REPL based development tool for Clojure with some ClojureScript support (not including Figwheel).
Clojure GitLab repository includes usage details.
SublimeText 4 is a lightweight and feature rich text editor, especially of interest to those that like a simple and uncluttered UI. SublimeText is a commercial project although has free trial version available (check conditions of use).
Clojure-Sublimed provides Clojure support for SublimeText 4, with support for Clojure & Edn syntax, code formatting and an nREPL client to connect to a Clojure REPL process.
Tutkain is an Sublime 4 package that supports Clojure development (self-described as alpha software).
Build configuration to start a REPL
Clojure Sublime connects to a REPL via an nREPL server. Run a terminal REPL using Clojure CLI, Leinginen (lein repl
) or Shadow-cljs (shadow-cljs watch app
)
Alternatively, configure Clojure Sublimed to run a REPL process by creating a new build system via Tools \u00bb Build System \u00bb New Build System. The following example starts a Clojure CLI REPL with nREPL server and assumes Java and Clojure CLI are installed.
{\"env\": {\"JAVA_HOME\": \"/path/to/java\"},\n \"cmd\": [\"/usr/local/bin/clojure\", \"-Sdeps\", \"{:deps {nrepl/nrepl {:mvn/version \\\"1.0.0\\\"}}}\", \"-M\", \"-m\", \"nrepl.cmdline\"]}\n
Run a REPL process via Tools \u00bb Build With\u2026 and connect to the REPL using the command Clojure Sublimed: Connect SublimeText install Clojure-Sublimed install SublimeText Documentation
Pulsar Community-led Hyper-Hackable Editor is a very new project to create a community version of the Atom editor from GitHub.
Chlorine plugin provides a Clojure and ClojureScript development environment using Socket-REPL integration
Pulsar Community-led Hyper-Hackable Editor Pulsar Chlorine plugin
Atom not actively developed
Atom will be archived on December 15 2022 with no further updates from GitHub team
Consider using VSCode with Clover or Calva plugin or help support the evolution of the Pulsar project
Cursive may be an appropriate choice for people from a Java background who are already familiar with IntelliJ. Cursive will run static analysis of Clojure code when opening a Clojure project, as IntelliJ does with other languages.
Follow the Cursive user guide to configure IntelliJ and install Cursive.
Requires license for commercial development
There is a free license when development is not for commercial projects, however, a license must be purchased for each developer working on a commercial project.
IntelliJ & Cursive install guide
"},{"location":"clojure-editors/clojure-lsp/","title":"Language Server Protocol","text":" Language Server Protocol provides a standard to provide a common set of development tools, e.g. code completion, syntax highlighting, refactor and language diagnostics.
Each language requires a specific LSP server implementation.
An editor or plugin provides an LSP client that uses data from language servers, providing information about source code and enabling development tools to understand the code structure.
"},{"location":"clojure-editors/clojure-lsp/#clojure-lsp","title":"Clojure LSP","text":" clojure-lsp is an implementation of an LSP server for Clojure and ClojureScript languages. Clojure LSP is built on top of clj-kondo which provides the static analysis of Clojure and ClojureScript code.
Most Clojure aware editors provide an LSP client.
"},{"location":"clojure-editors/clojure-lsp/#install","title":"Install","text":"Clojure LSP installation guide covers multiple operating systems.
LinuxHomebrew Practicalli recommends downloading the clojure-lsp-native-linux-amd64
from GitHub release page
Extracts the clojure-lsp
binary to ~/.local/bin/clojure-lsp
Clojure LSP project provides a custom tap for installing the latest version.
brew install clojure-lsp/brew/clojure-lsp-native\n
Homebrew default package deprecated The clojure-lsp
formula is deprecated and should not be used.
brew remove clojure-lsp
if the default clojure-lsp was installed
Check Clojure LSP server is working via the command line
clojure-lsp --version\n
Editors may provide install mechanism for Clojure LSP Spacemacs LSP layer will prompt to install a language server when first opening a file of a major mode where LSP is enabled. E.g. when a Clojure related file is opened, the Clojure LSP server is downloaded if not installed (or not found on the Emacs path).
Neovim package called mason manages the install of lint & format tools as well as LSP servers, or an externally installed LSP server can also be used.
VSCode Calva plugin includes the clojure-lsp server, although an external server can be configured.
"},{"location":"clojure-editors/clojure-lsp/#configure","title":"Configure","text":"Practicalli Clojure LSP Configuration config.edn is the recommended configuration from Practicalli.
;; ---------------------------------------------------------\n;; Clojure LSP user level (global) configuration\n;; https://clojure-lsp.io/settings/\n;;\n;; Complete config.edn example with default settings\n;; https://github.com/clojure-lsp/clojure-lsp/blob/master/docs/all-available-settings.edn\n;; default key/value are in comments\n;; ---------------------------------------------------------\n\n;; Refact config from all-available-settings example\n\n{;; ---------------------------------------------------------\n ;; Project analysis\n\n ;; auto-resolved for deps.edn, project.clj or bb.edn projects\n ;; :source-paths #{\"src\" \"test\"}\n\n ;; Include :extra-paths and :extra-deps from project & user level aliases in LSP classpath\n ;; :source-aliases #{:dev :test}\n :source-aliases #{:dev :test :dev/env :dev/reloaded}\n\n ;; Define a custom project classpath command, e.g. Clojure CLI\n ;; :project-specs [{:project-path \"deps.edn\"\n ;; :classpath-cmd [\"clojure\" \"-M:env/dev:env/test\" \"-Spath\"]}]\n ;; Check the default at clojure-lsp.classpath/default-project-specs\n ;; :project-specs []\n\n ;; ignore analyzing/linting specific paths\n :source-paths-ignore-regex [\"target.*\" \"build.*\" \"console-log-.*\"]\n\n ;; Additional LSP configurations to load from classpath\n ;; https://clojure-lsp.io/settings/#classpath-config-paths\n ;; :classpath-config-paths []\n\n ;; :paths-ignore-regex []\n\n ;; Watch for classpath changes\n ;; :notify-references-on-file-change true\n ;; :compute-external-file-changes true\n\n ;; Approach for linking dependencies\n ;; :dependency-scheme \"zipfile\"\n\n ;; generate and analyze stubs for specific namespaces on the project classpath\n ;; typically for closed source dependencies, e.g. datomic.api\n ;; https://clojure-lsp.io/settings/#stub-generation\n ;; :stubs {:generation {:namespaces #{}\n ;; :output-dir \".lsp/.cache/stubs\"\n ;; :java-command \"java\"}\n ;; :extra-dirs []}\n\n ;; Java Sources from Ubuntu package openjdk-17-source\n ;; jdk-source-uri takes precedence\n ;; :java\n ;; {:jdk-source-uri \"https://raw.githubusercontent.com/clojure-lsp/jdk-source/main/openjdk-19/reduced/source.zip\"\n ;; :home-path nil ;; jdk-source-uri takes precedence\n ;; :download-jdk-source? false\n ;; :decompile-jar-as-project? true}\n ;; :java\n ;; {:jdk-source-uri \"file:///usr/lib/jvm/openjdk-17/lib/src.zip\"}\n :java nil\n\n ;; End of Project analysis\n ;; ---------------------------------------------------------\n\n ;; ---------------------------------------------------------\n ;; Linter configuration\n\n ;; clj-kondo Linter rules\n ;; https://github.com/clj-kondo/clj-kondo/blob/master/doc/config.md#enable-optional-linters\n ;; :linters {:clj-kondo {:level :on\n ;; :report-duplicates true\n ;; :ns-exclude-regex \"\"}\n ;; :clj-depend {:level :info}} ;; Only if any clj-depend config is found\n\n ;; asynchronously lint project files after startup, for features like List project errors\n ;; :lint-project-files-after-startup? true\n\n ;; copy clj-kondo hooks configs exported by libs on classpath during startup\n ;; :copy-kondo-configs? true\n\n ;; End of Linter configuration\n ;; ---------------------------------------------------------\n\n ;; ---------------------------------------------------------\n ;; Refactor code\n\n ;; Namespace format\n ;; :clean {:automatically-after-ns-refactor true\n ;; :ns-inner-blocks-indentation :next-line\n ;; :ns-import-classes-indentation :next-line\n ;; :sort {:ns true\n ;; :require true\n ;; :import true\n ;; :import-classes {:classes-per-line 3} ;; -1 for all in single line\n ;; :refer {:max-line-length 80}}}\n\n ;; Do not sort namespaces\n :clean {sort {:ns false\n :require false\n :import false}}\n\n ;; Automatically add ns form to new Clojure/Script files\n ;; :auto-add-ns-to-new-files? true\n\n ;; use ^private metadata rather than defn-\n ;; :use-metadata-for-privacy? false\n :use-metadata-for-privacy? true\n\n ;; Keep parens around single argument functions in thread macro\n ;; :keep-parens-when-threading? false\n :keep-parens-when-threading? true\n\n ;; End of Refactor code\n ;; ---------------------------------------------------------\n\n ;; ---------------------------------------------------------\n ;; Clojure formatting configuration - cljfmt\n\n ;; location of cljfmt configuration for formatting\n ;; Path relative to project root or an absolute path\n ;; :cljfmt-config-path \".cljfmt.edn\"\n :cljfmt-config-path \"cljfmt.edn\"\n\n ;; Specify cljfmt configuration within Clojure LSP configuration file\n ;; :cljfmt {}\n\n ;; End of Clojure formatting configuration - cljfmt\n ;; ---------------------------------------------------------\n\n ;; ---------------------------------------------------------\n ;; Visual LSP components\n\n ;; :hover {:hide-file-location? false\n ;; :arity-on-same-line? false\n ;; :clojuredocs true}\n\n ;; :completion {:additional-edits-warning-text nil\n ;; :analysis-type :fast-but-stale}\n\n ;; :code-lens {:segregate-test-references true}\n\n ;; LSP semantic tokens server support for syntax highlighting\n ;; :semantic-tokens? true\n\n ;; Documentation artefacts\n ;; :document-formatting? true\n ;; :document-range-formatting? true\n\n ;; End of Visual LSP components\n ;; ---------------------------------------------------------\n\n ;; ---------------------------------------------------------\n ;; LSP general configuration options\n\n ;; Exit clojure-lsp if any errors found, e.g. classpath scan failure\n ;; :api {:exit-on-errors? true}\n\n ;; Synchronise whole buffer `:full` or only related changes `:incremental`\n ;; :text-document-sync-kind :full\n\n ;; End of LSP general configuration options\n ;; ---------------------------------------------------------\n\n ;; ---------------------------------------------------------\n ;; File locations\n\n ;; project analysis cache to speed clojure-lsp startup\n ;; relative path to project root or absolute path\n ;; :cache-path \".lsp/.cache\"\n\n ;; Absolute path\n ;; :log-path \"/tmp/clojure-lsp.*.out\"\n\n ;; End of file locations\n ;; ---------------------------------------------------------\n\n ;; ---------------------------------------------------------\n ;; LSP snippets\n ;; https://clojure-lsp.io/features/#snippets\n\n :additional-snippets\n [;; Documentation / comments\n\n {:name \"comment-heading\"\n :detail \"Comment Header\"\n :snippet\n \";; ---------------------------------------------------------\n ;; ${1:Heading summary title}\n ;;\n ;; ${2:Brief description}\\n;; ---------------------------------------------------------\\n\\n$0\"}\n\n {:name \"comment-separator\"\n :detail \"Comment Separator\"\n :snippet\n \";; ---------------------------------------------------------\\n;; ${1:Section title}\\n\\n$0\"}\n\n {:name \"comment-section\"\n :detail \"Comment Section\"\n :snippet\n \";; ---------------------------------------------------------\\n;; ${1:Section title}\\n\\n$0\\n\\n\n ;; End of $1\\n;; ---------------------------------------------------------\\n\\n\"}\n\n {:name \"wrap-reader-comment\"\n :detail \"Wrap current expression with Comment Reader macro\"\n :snippet \"#_$current-form\"}\n\n {:name \"rich-comment\"\n :detail \"Create rich comment\"\n :snippet\n \"(comment\n $0\n #_()) ;; End of rich comment\"}\n\n {:name \"rich-comment-rdd\"\n :detail \"Create comment block\"\n :snippet\n \"#_{:clj-kondo/ignore [:redefined-var]}\n (comment\n $0\n #_()) ; End of rich comment\"}\n\n {:name \"rich-comment-hotload\"\n :detail \"Rich comment library hotload\"\n :snippet\n \"#_{:clj-kondo/ignore [:redefined-var]}\n (comment\n ;; Add-lib library for hot-loading\n (require '[clojure.tools.deps.alpha.repl :refer [add-libs]])\n (add-libs '{${1:domain/library-name} {:mvn/version \\\"${2:1.0.0}\\\"}$3})\n $0\n #_()) ; End of rich comment block\"}\n\n {:name \"wrap-rich-comment\"\n :detail \"Wrap current expression with rich comment form\"\n :snippet\n \"(comment\n $current-form\n $0\n #_()) ;; End of rich comment\"}\n\n ;; Core functions\n\n {:name \"def\"\n :detail \"def with docstring\"\n :snippet \"(def ${1:name}\\n \\\"${2:doc-string}\\\"\\n $0)\"}\n\n {:name \"def-\"\n :detail \"def private\"\n :snippet \"(def ^:private ${1:name}\\n \\\"${2:doc-string}\\\"\\n $0)\"}\n\n {:name \"defn\"\n :detail \"Create public function\"\n :snippet \"(defn ${1:name}\\n \\\"${2:doc-string}\\\"\\n [${3:args}]\\n $0)\"}\n\n {:name \"defn-\"\n :detail \"Create public function\"\n :snippet \"(defn ^:private ${1:name}\\n \\\"${2:docstring}\\\"\\n [${3:args}]\\n $0)\"}\n\n {:name \"ns\"\n :detail \"Create ns\"\n :snippet \"(ns ${1:name}\\n \\\"${2:doc-string}\\\"\\n ${3:require})\"}\n\n ;; Clojure CLI alias snippets\n\n {:name \"deps-alias\"\n :detail \"deps.edn alias with extra path & deps\"\n :snippet\n \":${1:category/name}\n {:extra-paths [\\\"${2:path}\\\"]\n :extra-deps {${3:deps-maven or deps-git}}}$0\"}\n\n {:name \"deps-alias-main\"\n :detail \"deps.edn alias with extra path & deps\"\n :snippet\n \":${1:category/name}\n {:extra-paths [\\\"${2:path}\\\"]\n :extra-deps {${3:deps-maven or deps-git}}\n :main-opts [\\\"-m\\\" \\\"${4:main namespace}\\\"]}$0\"}\n\n {:name \"deps-alias-exec\"\n :detail \"deps.edn alias with extra path & deps\"\n :snippet\n \":${1:category/name}\n {:extra-paths [\\\"${2:path}\\\"]\n :extra-deps {${3:deps-maven or deps-git}}\n :exec-fn ${4:domain/function-name}\n :exec-args {${5:key value}}}$0\"}\n\n {:name \"deps-alias-main-exec\"\n :detail \"deps.edn alias with extra path & deps\"\n :snippet\n \":${1:category/name}\n {:extra-paths [\\\"${2:path}\\\"]\n :extra-deps {${3:deps-maven or deps-git}}\n :main-opts [\\\"-m\\\" \\\"${4:main namespace}\\\"]\n :exec-fn ${4:domain/function-name}\n :exec-args {${5:key value}}}$0\"}\n\n {:name \"deps-maven\"\n :detail \"deps.edn Maven dependency\"\n :snippet\n \"${1:domain/library-name} {:mvn/version \\\"${2:1.0.0}\\\"}$0\"}\n\n {:name \"deps-git\"\n :detail \"deps.edn Git dependency\"\n :snippet\n \"${1:domain/library-name}\n {:git/sha \\\"${2:git-sha-value}\\\"}$0\"}\n\n {:name \"deps-git-tag\"\n :detail \"Git dependency\"\n :snippet\n \"${1:domain/library-name}\n {:git/tag \\\"${2:git-tag-value}\\\"\n :git/sha \\\"${3:git-sha-value}\\\"}$0\"}\n\n {:name \"deps-git-url\"\n :detail \"Git URL dependency\"\n :snippet\n \"${1:domain/library-name}\n {:git/url \\\"https://github.com/$1\\\"\n :git/sha \\\"${2:git-sha-value}\\\"}$0\"}\n\n {:name \"deps-local\"\n :detail \"deps.edn Maven dependency\"\n :snippet\n \"${1:domain/library-name} {:local/root \\\"${2:/path/to/project/root}\\\"}$0\"}\n\n ;; Requiring dependency snippets\n\n {:name \"require-rdd\"\n :detail \"require for rich comment experiments\"\n :snippet \"(require '[${1:namespace} :as ${2:alias}]$3)$0\"}\n\n {:name \"require\"\n :detail \"ns require\"\n :snippet \"(:require [${1:namespace}])$0\"}\n\n {:name \"require-refer\"\n :detail \"ns require with :refer\"\n :snippet \"(:require [${1:namespace} :refer [$2]]$3)$0\"}\n\n {:name \"require-as\"\n :detail \"ns require with :as alias\"\n :snippet \"(:require [${1:namespace} :as ${2:alias}]$3)$0\"}\n\n {:name \"use\"\n :detail \"require refer preferred over use\"\n :snippet \"(:require [${1:namespace} :refer [$2]])$0\"}\n\n ;; Unit Test snippets\n\n {:name \"deftest\"\n :detail \"deftest clojure.test\"\n :snippet\n \"(deftest ${1:name}-test\n (testing \\\"${2:Context of the test assertions}\\\"\n (is (= ${3:assertion-values}))$4)) $0\"}\n\n {:name \"testing\"\n :detail \"testing asserting group for clojure.test\"\n :snippet \"(testing \\\"${1:description-of-assertion-group}\\\"\\n $0)\"}\n\n {:name \"is\"\n :detail \"assertion for clojure.test\"\n :snippet \"(is (= ${1:function call} ${2:expected result}))$0\"}\n\n ;; ---------------------------------------------------------\n ;; Clojure LSP and Clj-kondo snippets\n\n {:name \"lsp-ignore-redefined\"\n :detail \"Ignore redefined Vars\"\n :snippet\n \"#_{:clj-kondo/ignore [:redefined-var]}\n $0\"}\n\n ;; End of Clojure LSP and Clj-kondo snippets\n ;; ---------------------------------------------------------\n ]\n ;; End of LSP snippets\n ;; ---------------------------------------------------------\n\n }\n
Include :extra-paths
and :extra-deps
from project & user level aliases in LSP classpath. e.g. support a custom user
namespace in dev/user.clj
:source-aliases #{:dev :test :env/dev :env/test :lib/reloaded}\n
Include Java Sources installed via Debian / Ubuntu package openjdk-21-source
to support calls to Java Objects and Methods.
:java\n {:jdk-source-uri \"file:///usr/lib/jvm/openjdk-21/lib/src.zip\" ;;\n :home-path nil ;; jdk-source-uri takes precedence\n :download-jdk-source? false}\n
Disable Java analysis
If not using Java Interop with Clojure, it can be an advantage to disable the Java analysis. This should remove Java functions from autocomplete.
:java nil\n
Clean namespace ns
forms but do not sort require names
:clean {:automatically-after-ns-refactor true\n :ns-inner-blocks-indentation :next-line\n :ns-import-classes-indentation :next-line\n :sort {:ns false\n :require false\n :import false\n :import-classes {:classes-per-line 3} ;; -1 for all in single line\n :refer {:max-line-length 80}}}\n
Use ^private
metadata for private function definitions rather than defn-
:use-metadata-for-privacy? true\n
Location of cljfmt configuration for formatting, path relative to project root. The defaults for cljfmt are used, except :remove-consecutive-blank-lines?
which is set to false to enable more readable code.
:cljfmt-config-path \"cljfmt.edn\"\n
cljfmt configuration included example :indents
rules for clojure.core, compojure, fuzzy rules and examples used by the Clojure LSP maintainer.
"},{"location":"clojure-editors/clojure-lsp/#practicalli-snippets","title":"Practicalli snippets","text":"Practicalli Snippets are defined in the :additional-snippets
section of the Practicalli Clojure LSP config.
"},{"location":"clojure-editors/clojure-lsp/#docs-comments","title":"Docs / comments","text":" comment-heading
- describe purpose of the namespace comment-separator
- logically separate code sections, helps identify opportunities to refactor to other name spaces comment-section
- logically separate large code sections with start and end line comments wrap-reader-comment
- insert reader comment macro, #_
before current form, informing Clojure reader to ignore next form
"},{"location":"clojure-editors/clojure-lsp/#repl-driven-development","title":"Repl Driven Development","text":" rich-comment
- comment block rich-comment-rdd
- comment block with ignore :redefined-var for repl experiments rich-comment-hotload
- comment block with add-libs code for hotloading libraries in Clojure CLI repl wrap-rich-comment
- wrap current form with comment reader macro require-rdd
- add a require expression, for adding a require in a rich comment block for RDD
"},{"location":"clojure-editors/clojure-lsp/#standard-library-functions","title":"Standard library functions","text":" def
- def with docstring def-
- private def with docstring defn
- defn with docstring defn-
private defn with docstring ns
- namespace form with docstring
"},{"location":"clojure-editors/clojure-lsp/#clojure-cli-depsedn-aliases","title":"Clojure CLI deps.edn aliases","text":" deps-alias
- add Clojure CLI alias deps-maven
- add a maven style dependency deps-git
- add a git style dependency using :git/sha
deps-git-tag
- as above including :git/tag
deps-git-url
- add git style dependency using git url (url taken from dependency name as it is typed - mirrored placeholder) deps-local
- add a :local/root
dependency
"},{"location":"clojure-editors/clojure-lsp/#requiring-dependencies","title":"Requiring dependencies","text":" require-rdd
- add a require expression, for adding a require in a rich comment block for RDD require
- simple require require-refer
- require with :refer
require-as
- require with :as
alias use
- creates a require rather than the more troublesome use
"},{"location":"clojure-editors/clojure-lsp/#unit-testing","title":"Unit testing","text":" deftest
- creates a deftest with testing directive and one assertion testing
- creates a testing testing directive and one assertion is
- an assertion with placeholders for test function and expected results
"},{"location":"clojure-editors/clojure-lsp/#references","title":"References","text":" - LSP mode - A guide on disabling / enabling features - if the Emacs UI is too cluttered or missing visual features
- Configure Emacs as a Clojure IDE
- Language Server Protocol support for Emacs
"},{"location":"clojure-editors/clojure-lsp/practicalli-snippets/","title":"Practicalli Snippets for Clojure LSP","text":"Custom snippets created by Practicalli and added via the :additional-snippets
key in the Clojure LSP configuration (.lsp/config.edn
or user level configuration). Snippets are defined as a vector of hash-maps
{:additional-snippets [{} {} {} ,,,]}\n
Practicalli Snippets available in practicalli/clojure-lsp-config
Move or delete the clojure configuration directory and clone the Practicalli Clojure CLI Config
"},{"location":"clojure-editors/clojure-lsp/practicalli-snippets/#documentation","title":"Documentation","text":"A comment heading to describe the purpose and important information about the current namesapce.
{:name \"comment-heading\"\n :detail \"Comment Header\"\n :snippet\n \";; ---------------------------------------------------------\n ;; ${1:Heading summary title}\n ;;\n ;; ${2:Brief description}\\n;; ---------------------------------------------------------\\n\\n$0\"}\n
A comment separator for marking logical sections within a namespace, useful for navigating code and identifying opportunities to refactor a namespace into multiple namespaces.
{:name \"comment-separator\"\n :detail \"Comment Separator\"\n :snippet\n \";; ---------------------------------------------------------\\n;; ${1:Section title}\\n\\n$0\"}\n
A comment section with start and end titles for marking logical sections within a namespace, again for navigation and identifying opportunities to refactor a namespace.
{:name \"comment-section\"\n :detail \"Comment Section\"\n :snippet\n \";; ---------------------------------------------------------\\n;; ${1:Section title}\\n\\n$0\\n\\n\n ;; End of $1\\n;; ---------------------------------------------------------\\n\\n\"}\n
"},{"location":"clojure-editors/clojure-lsp/practicalli-snippets/#repl-driven-development","title":"REPL Driven Development","text":"A rich comment block typically used to hold function calls to show how to make use of the important aspects of the current namespace. For example, calls to start
, restart
, stop
functions in a namespace that defines the service life-cycle.
This provides a live executable guide to using the namespace, without being called if the whole namespace is evaluated.
A commented expression is placed before the closing paren to ensure that closing paren is not folded up into the previous line. This makes it easier to add further code to the rich comment block.
{:name \"rich-comment\"\n :detail \"Create rich comment\"\n :snippet\n \"(comment\n $0\n #_()) ;; End of rich comment\"}\n
A modified rich comment block with clj-kondo configuration to suppress warnings for duplicate function definition names, supporting alternative function implementations as part of a REPL driven development workflow.
{:name \"rich-comment-rdd\"\n :detail \"Create comment block\"\n :snippet\n \"#_{:clj-kondo/ignore [:redefined-var]}\n (comment\n $0\n #_()) ;; End of rich comment\"}\n
Wrap an existing form in a rich comment
{:name \"wrap-rich-comment\"\n :detail \"Wrap current expression with rich comment form\"\n :snippet\n \"(comment\n $current-form\n $0\n #_()) ;; End of rich comment\"}\n
Comment an existing form with the Clojure Comment macro, _#
{:name \"wrap-reader-comment\"\n :detail \"Wrap current expression with Comment Reader macro\"\n :snippet \"#_$current-form\"}\n
"},{"location":"clojure-editors/clojure-lsp/practicalli-snippets/#hot-loading-library-dependencies","title":"Hot loading library dependencies","text":"Clojure CLI projects can hotload library dependencies into a running Clojure REPL. This requires starting a REPL with the clojure.tools.deps.alpha
library as a dependency which can be done by including the :lib/hotload
alias from practicalli/clojure-deps-edn. Note this library is alpha and the API could change in future.
Create a rich comment block that requires the clojure.tools.deps.alpha
namespace and an add-libs
expression to hotload one or more libraries in a hash-map. Tab stops with placeholders are included for adding the first library to hotload.
{:name \"rich-comment-hotload\"\n :detail \"Rich comment library hotload\"\n :snippet\n \"#_{:clj-kondo/ignore [:redefined-var]}\n (comment\n ;; Add-lib library for hot-loading\n (require '[clojure.tools.deps.alpha.repl :refer [add-libs]])\n (add-libs '{${1:domain/library-name} {:mvn/version \\\"${2:1.0.0}\\\"}$3})\n $0\n #_()) ;; End of rich comment block\"}\n
"},{"location":"clojure-editors/clojure-lsp/practicalli-snippets/#core-functions","title":"Core functions","text":"Create a public var using a def
form with a doc-string, with placeholders for name and value.
{:name \"def\"\n :detail \"def with docstring\"\n :snippet \"(def ${1:name}\\n \\\"${2:docstring}\\\"\\n $0)\"}\n
Create a private var using a def
form with ^:private
meta data and a doc-string, with placeholders for name and value.
{:name \"def-\"\n :detail \"def private\"\n :snippet \"(def ^:private ${1:name}\\n \\\"${2:doc-string}\\\"\\n $0)\"}\n
A defn
form with name, doc-string and args tab-stops
{:name \"defn\"\n :detail \"Create public function\"\n :snippet \"(defn ${1:name}\\n \\\"${2:docstring}\\\"\\n [${3:args}]\\n $0)\"}\n
A defn
form with private metatdata. Including name, doc-string and args tab-stops
{:name \"defn-\"\n :detail \"Create public function\"\n :snippet \"(defn ^:private ${1:name}\\n \\\"${2:docstring}\\\"\\n [${3:args}]\\n $0)\"}\n
A namespace form with name, doc-string and require tab-stop.
{:name \"ns\"\n :detail \"Create ns\"\n :snippet \"(ns ${1:name}\\n \\\"${2:docstring}\\\"\\n ${3:require})\"}\n
"},{"location":"clojure-editors/clojure-lsp/practicalli-snippets/#clojure-cli-aliases-and-library-dependencies","title":"Clojure CLI aliases and library dependencies","text":"Add Clojure CLI alias to deps.edn
, with an :extra-paths
and :extra-deps
section
{:name \"deps-alias\"\n :detail \"deps.edn alias with extra path & deps\"\n :snippet\n \":${1:category/name}\n {:extra-paths [\\\"${2:path}\\\"]\n :extra-deps {${3:deps-maven or deps-git}}}$0\"}\n
Add a Maven style dependency to a Clojure CLI deps.edn
project.
{:name \"deps-maven\"\n :detail \"deps.edn Maven dependency\"\n :snippet\n \"${1:domain/library-name} {:mvn/version \\\"${2:1.0.0}\\\"}$0\"}\n
Add a dependency from a Git repository, where the library is named after the remote Git repository, i.e io.github.user|org/library-name for the GitHub repository https://github.com/user|org/library-name
.
The :git/sha
defines a specific commit to use for the dependency.
{:name \"deps-git\"\n :detail \"deps.edn Git dependency\"\n :snippet\n \"${1:domain/library-name}\n {:git/sha \\\"${2:git-sha-value}\\\"}$0\"}\n
Additionally a Git tag can be specified, enabling the use of the short SHA value for :git/sha
(short sha is the first 7 characters of the 40 character SHA-1 value).
A Git client can obtain the short form of a SHA from a Git repository
git rev-parse --short 1e872b59013425b7c404a91d16119e8452b983f2\n
{:name \"deps-git-tag\"\n :detail \"Git dependency\"\n :snippet\n \"${1:domain/library-name}\n {:git/tag \\\"${2:git-tag-value}\\\"\n :git/sha \\\"${3:git-sha-value}\\\"}$0\"}\n
If a library is not named after the domain of the Git repository, the URL of the Git repository must be specified using the :git/url
key.
{:name \"deps-git-url\"\n :detail \"Git URL dependency\"\n :snippet\n \"${1:domain/library-name}\n {:git/url \\\"https://github.com/$1\\\"\n :git/sha \\\"${2:git-sha-value}\\\"}$0\"}\n
Add a library dependency that is a local Clojure project.
{:name \"deps-local\"\n :detail \"deps.edn Maven dependency\"\n :snippet\n \"${1:domain/library-name} {:local/root \\\"${2:/path/to/project/root}\\\"}$0\"}\n
"},{"location":"clojure-editors/clojure-lsp/practicalli-snippets/#require-library-dependencies","title":"Require Library Dependencies","text":"Require a library when using REPL driven development in a rich comment block, adding a (require ,,,)
form when evaluating the use of a library without forcing it to be loaded when loading the namespace.
{:name \"require-rdd\"\n :detail \"require for rich comment experiments\"\n :snippet \"(require '[${1:namespace} :as ${2:alias}]$3)$0\"}\n
A basic :require
expression for an ns
form.
{:name \"require\"\n :detail \"ns require\"\n :snippet \"(:require [${1:namespace}])$0\"}\n
A :require
expression for an ns
form, including a :as
directive to define an alias for the required namespace.
{:name \"require-as\"\n :detail \"ns require with :as alias\"\n :snippet \"(:require [${1:namespace} :as ${2:alias}]$3)$0\"}\n
A :require
expression for an ns
form, including a :refer
directive to include specific function definitions and vars by name.
{:name \"require-refer\"\n :detail \"ns require with :refer\"\n :snippet \"(:require [${1:namespace} :refer [$2]]$3)$0\"}\n
It is idiomatic to use require with refer to pull in specific functions and vars from another namespace. The use
function is not recommended as it can easily pull more transitive dependencies into the current namespace, causing unexpected results
{:name \"use\"\n :detail \"require refer preferred over use\"\n :snippet \"(:require [${1:namespace} :refer [$2]])$0\"}\n
"},{"location":"clojure-editors/clojure-lsp/practicalli-snippets/#clojuretest-snippets","title":"Clojure.test snippets","text":"When writing a deftest
, a new assertion written may be better in a new group. The testing
snippet will create a new testing
form and pull in the following assertion.
{:name \"deftest\"\n :detail \"deftest clojure.test\"\n :snippet\n \"(deftest ${1:name}-test\n (testing \\\"${2:Context of the test assertions}\\\"\n (is (= ${3:assertion-values}))$4))\n $0\"}\n
Create a new assertion group using the clojure.test/testing
form.
Using testing
before an assertion form pull that assertion into the group
{:name \"testing\"\n :detail \"testing clojure.test\"\n :snippet \"(testing \\\"${1:description-of-assertion-group}\\\"\\n $0)\"}\n
Define an is
assertion for a deftest
{:name \"is\"\n :detail \"assertion for clojure.test\"\n :snippet \"(is (= ${1:function call} ${2:expected result}))$0\"}\n
"},{"location":"clojure-editors/clojure-lsp/snippets/","title":"Clojure LSP Snippets","text":"Custom snippets are defined in the Clojure LSP EDN configuration using the :additional-snipets
key. The snippet body uses the same tab stop and placeholder syntax as Yasnipets, although the body is contained within a string.
Built-in snippets can include Clojure code for generating the text of the snippet when expanded. Custom snippets do not currently support evaluation of code in the snippet.
Clojure LSP Configuration locations Project specific configuration resides in .lsp/config.edn
User level configuration is either $XDG_CONFIG_HOME/clojure-lsp/config.edn
or $HOME/.clojure-lsp/config
"},{"location":"clojure-editors/clojure-lsp/snippets/#snippet-definition","title":"Snippet definition","text":"The :additional-snippets
key is associated with a vector or hash-maps, [{}{},,,]
with each hash-map defining a snippet using the keys:
:name
- name of the snippet, typed into the editor for completion
:detail
- a meaningful description of the snippet
:snippet
- the definition of the snippet, with tab stops and current-form syntax
The :snippet
can be any text, ideally with syntax that is correct for the particular language
"},{"location":"clojure-editors/clojure-lsp/snippets/#snippet-tab-stops","title":"Snippet Tab Stops","text":"Include $
with a number, e.g. $1
,$2
,$3
, to include tab stops in the snippet. Once the snippet code has been generated, TAB
key jumps through the tab stops in sequence, allowing customisation of a generic snippet.
$0
marks the final position of the cursor, after which TAB
has no more positions in the snippet to jump to.
"},{"location":"clojure-editors/clojure-lsp/snippets/#snippet-current-form","title":"Snippet current-form","text":"When a Clojure LSP snipped includes $current-form
then typing a snippet name in front of an existing Clojure form includes that form in the generated code.
{:additional-snippets [{:name \"wrap-let-sexpr\"\n :detail \"Wrap current sexpr in let\"\n :snippet \"(let [$1 $current-form] $0)\"}]}\n
Limited scope with current-form
A Snippet including $current-form
is only active when typed in front of an existing expression. A snippet is not recognised when typed at the top level.
"},{"location":"clojure-editors/clojure-lsp/snippets/#placeholders","title":"Placeholders","text":"Tab Stops can also include default values or text used as hint on what each tab stop value is for. These are referred to as placeholders.
${1:default-value}
is the form of a placeholder for tab stop 1. When the cursor tabs to tab stop 1, the default-value text is highlighted and replaces as soon as characters are typed.
Placeholder text is not replaced for $0
tab-stop, as the snippet interaction is effectively over at this point.
The deftest
custom snippet shows examples of placeholders for three tab stops.
{:name \"deftest\"\n :detail \"deftest clojure.test\"\n :snippet\n \"(deftest ${1:name}-test\n (testing \\\"${2:Context of the test assertions}\\\"\n (is (= ${3:assertion-values}))$4))\n $0\"}\n
Escape string quotes in snippet body
Use \\
character before the \"
character within the snippet body. For example, doc-strings in defn
function definitions or the string in testing
function.
"},{"location":"clojure-editors/clojure-lsp/snippets/#code-driven-snippet","title":"Code driven snippet","text":"The built-in defn
snippet uses Clojure code to help generate the snippet.
%s
is a substitution point within a snippet, used by the standard Clojure format
command, used to included either defn ^:private
or defn-
, depending on the value returned from the if
expression.
:use-metadata-for-privacy?
is a key from the Clojure LSP configuration
{:label \"defn-\"\n :detail \"Create private function\"\n :insert-text (format \"(defn%s ${1:name} [$2]\\n ${0:body})\"\n (if (:use-metadata-for-privacy? settings)\n \" ^:private\"\n \"-\"))}\n
The syntax for built-in snippets is slightly different that the :additional-syntax
form. The internal form uses :label
for :name
and :insert-text
for :snippet
.
Code supported only in built-in snippets
Clojure code only works for built-in snippets and not for :additional-snippets
.
Clojure LSP is compiled by Graal to a native binary, including the built-in snippets. To include Clojure code in a snippet then consider submitting a pull request to the Clojure LSP project to add a built-in snippet.
"},{"location":"clojure-spec/","title":"Clojure Specifications","text":"Clojure Spec is a library for defining specifications around data and functions to test for correctness.
A spec defines the expected shape of specific values in Clojure and specs are intended to be used across multiple projects. Specifications for more complex values are composed of specific value specifications, providing a flexible way to define what key parts of the system should look like.
Clojure specs are used to generate comprehensive test data, identifying more scenarios and edge cases with less code.
Spec is included in Clojure version 1.9 onward and can be used by requiring the clojure.spec.alpha
in the REPL or in namespaces of a Clojure project.
"},{"location":"clojure-spec/#recommended-reading","title":"Recommended Reading","text":"What is Clojure spec - an illustrated guide
"},{"location":"clojure-spec/#purpose-of-clojure-spec","title":"Purpose of Clojure spec","text":"A summary highlighting the common purposes that Clojure Spec is used for
Purpose Description Living documentation Use spec to include specifications in Function documentation (fdef
) Data Validation Ensure the data entering and leaving the system or its key functions are of the correct form Test Data Generation Provide extensive test data coverage with minimal code maintenance Generative testing of functions Test functions using their spec defined contract (fdef
) Generative scenario testing Specific correct usage paths for known states Development time checking Instrument functions to ensure correctness Derive code from specifications Specify a system of record for data structures, internal and external to the system."},{"location":"clojure-spec/#example-use-cases","title":"Example use cases","text":" - API requests (schema is often used here, but so can spec)
- Checking data pulled from / pushed to message systems (e.g. Kafka, TIBCO)
- Data specifications (eg. Vega-lite)
"},{"location":"clojure-spec/#example-code","title":"Example code","text":" practicalli/leveraging-spec
practicalli/leveraging-spec - basic examples of using spec, following the Practicalli Spec broadcasts
"},{"location":"clojure-spec/#understanding-the-basics-of-clojure-spec","title":"Understanding the basics of Clojure Spec","text":""},{"location":"clojure-spec/#trying-clojurespec","title":"Trying clojure.spec","text":"Follow the examples in these two excellent videos
"},{"location":"clojure-spec/#why-is-the-spec-library-called-alpha","title":"Why is the spec library called alpha?","text":"The library is called clojure.spec.alpha
as the design of spec is still evolving and there may be some changes to the design in later versions. Clojure aims for backwards compatibility, so new versions typically do not break existing use of libraries.
There are some important changes being developed for spec version 2 and a few things may change, however, the large majority of Spec will remain the same and is safe to use.
"},{"location":"clojure-spec/#references","title":"References","text":"spec guide - clojure.org Introducing clojure.spec clojure.spec - rational and overview
spec.alpha API reference
How do you use clojure.spec - Sean Corfield
Specifications for clojure.core
Leveraging clojure.spec - Stuart Halloway spec.test - Stuart Halloway
Clojure Spec: Expressing Data Constraints without Types
"},{"location":"clojure-spec/add-spec-to-projects/","title":"Add Spec to a project","text":"Create a new project or clone practicalli/leveraging-spec which includes several examples of using Clojure Spec.
Create new projectClone Practicalli Leveraging Spec project Create a new Clojure CLI project using the Practicalli project templates
Create new projectclojure -T:project/create :template practicalli/minimal :name practicalli/leveraging-spec\n
Practicalli Clojure CLI Config - :project/create alias Practicalli Clojure CLI Config repository includes the :project/create
alias for creating new Clojure projects from a template using deps-new
.
The project is created with Clojure as a dependency, which includes the clojure.spec.alpha
library.
Clojure 1.9.0 or higher versions include Clojure Spec. Clojure 1.11.1 is recommended.
practicalli/leveraging-spec project includes Clojure Spec examples for values and functional arguments, along with unit tests using clojure spect-test.
https://github.com/practicalli/leveraging-spec.git\n
"},{"location":"clojure-spec/add-spec-to-projects/#project-dependencies","title":"Project Dependencies","text":"Clojure Spec is included in Clojure so only org.clojure/clojure
dependency is required in the deps.edn
file for the project.
Clojure project dependency
deps.edn{:paths [\"src\" \"resources\"]\n :deps {org.clojure/clojure {:mvn/version \"1.11.1\"}}}\n
"},{"location":"clojure-spec/add-spec-to-projects/#use-spec-in-namespace","title":"Use spec in namespace","text":"Require the clojure.spec.alpha
namespace in the ns
definition using the spec
alias name. Practicalli recommends using spec
(rather than s
as it is much clearer as to where the functions are defined)
Clojure project dependency
(ns practicalli.leveraging-spec\n (:require [clojure.spec.alpha :as spec]))\n
Evaluate the namespace definition to start using the functions from the namespace.
All functions from clojure.spec.alpha
are now accessible using the spec
alias, e.g. spec/conform
, spec/valid?
, spec/def
.
"},{"location":"clojure-spec/add-spec-to-projects/#testing-with-spec","title":"Testing with spec","text":"Add the clojure.spec.test.alpha
using the spec-test
alias, along with clojure.spec.test.alpha
as spec
alias
Clojure project dependency
src/practicalli/leveraging_spec.clj(ns practicalli.leveraging-spec\n (:require\n [clojure.spec.alpha :as spec]\n [clojure.spec.test.alpha :as spec-test]))\n
"},{"location":"clojure-spec/organising-specs/","title":"Organizing Specifications","text":"Data and function definition specifications are typically placed in a dedicated specification
namespaces, e.g src/domain/project/specification.clj
.
Add the data specifications (spec/def
), custom predicate functions and function specifications (spec/fdef
) to the specifications
namespace.
Specifications for an architecture layer can be organised next to the namespaces managing the layer, e.g. database.
Migrate specifications to a library once they are applicable to multiple projects.
"},{"location":"clojure-spec/organising-specs/#instrumenting-functions","title":"Instrumenting functions","text":"Add spec-test/instrument
expressions to the specifications
file, after the spec/fdef
expressions.
Rather than create individual expressions, create a clojure.core/def
to contain a collection of all the spec/fdef
expressions. This list can then be used to instrument
and unstrument
all the spec/fdef
specifications.
(def ^:private function-specifications\n [`card-game/deal-cards\n `card-game/winning-player])\n
Write simple helper functions to wrap the instrumenting of function specifications
(defn instrument-all-functions\n []\n (spec-test/instrument function-specifications))\n\n(defn unstrument-all-functions\n []\n (spec-test/unstrument function-specifications))\n
"},{"location":"clojure-spec/organising-specs/#unit-testing","title":"Unit testing","text":"Specifications can be incorporated into the existing unit tests, so it is sensible to keep them under the corresponding test
directory files.
"},{"location":"clojure-spec/organising-specs/#generative-testing","title":"Generative testing","text":"Using spec-test/check
will generate 1000 data values for each expression, so by default these tests will take far longer that other tests.
Configuring generative tests to only generate a small number of values will make spec-test/check
expressions return almost instantaneously. In this example, only 10 data values are generated
(spec-test/check `deal-cards\n {:clojure.spec.test.check/opts {:num-tests 10}})\n
Generative testing with small generators can be run regularly during development without impacting fast feedback.
Testing with Clojure Spec
"},{"location":"clojure-spec/using-spec-in-the-repl/","title":"REPL Experiments with Clojure Spec","text":"Create a minimal Project Clojure Spec can be tried without creating a Clojure project, although creating a project is useful if saving the Clojure Spec code experiments.
Create a minimal Clojure project with a Clojure CLI deps.edn configuration.
clojure -T:project/create :template practicalli/minimal :name practicalli/spec-experiments\n
Run a Clojure REPL with a rich terminal UI
REPL RebelREPL Reloaded A REPL with a rich terminal UI
clojure -M:repl/rebel\n
A REPL with a rich terminal UI and tools to support the Practicalli REPL Reloaded workflow.
clojure -M:repl/reloaded\n
Require the clojure.spec.alpha
using an alias called spec
to use functions from that namespace.
(require '[clojure.spec.alpha :as spec])\n
NOTE: clojure.spec.alpha
is often aliased as s
, although Practicalli avoids
"},{"location":"clojure-spec/using-spec-in-the-repl/#spec-auto-completion","title":"Spec auto-completion","text":"Using rebel-readline for the Clojure REPL will show autocompletion for all spec functions once the spec namespace has been required.
Type (spec /
and press TAB
to list all the functions in the namespace.
Typing a space character after the full name of a function shows the function signature with arguments that should be passed to that function.
Ctrl x Ctrl d displays the documentation for the current function
"},{"location":"clojure-spec/using-spec-in-the-repl/#check-data-conforms-to-specification","title":"Check data conforms to specification","text":"Use the spec/conform
and spec/valid?
functions to test if data matches a specification. In these examples, predicate functions are used as a specification.
"},{"location":"clojure-spec/using-spec-in-the-repl/#example-expressions","title":"Example expressions","text":"spec/conform
will return the value if it conforms to the specification, or :clojure.spec.alpha/invalid
if the data does not conform.
Clojure Spec - Conform values
(spec/conform odd? 101)\n\n(spec/conform integer? 1)\n\n(spec/conform seq? [1 2 3])\n\n(spec/conform seq? (range 10))\n\n(spec/conform map? {})\n\n(spec/conform map? (hash-map :a 1 :b 2))\n
spec/valid?
returns true or false
Clojure Spec - validate values
(spec/valid? even? 180)\n\n(spec/valid? string? \"Am I a valid string\")\n\n(spec/valid? (fn [value] (> value 10000)) 30076)\n\n(spec/valid? #(> % 10000) 30076)\n\n(spec/conform #(> % 10000) 30076)\n
"},{"location":"clojure-spec/data/","title":"Clojure Spec for data","text":"Specifications can be defined for any data in Clojure, be that simple values or complex data structures. More complex specifications are composed of individual specifications, providing a flexible way to define specifications without building a brittle hierarchy.
"},{"location":"clojure-spec/data/#what-is-a-specification","title":"What is a specification","text":"Specifications can be predicates (return true or false), literal values in sets and entity maps.
There are many predicate functions that come with Clojure which help speed the creation of specifications. Clojure function definitions (fn
, defn
) can be used to define custom predicate functions too.
"},{"location":"clojure-spec/data/#do-values-meet-a-specification","title":"Do values meet a specification","text":"The functions use to compare data with a specification are:
conform
- test if data conforms to a specification, returning the conformed value valid?
- predicate to test if data conforms to a specification, returning true of false explain
- explain why a value is not conforming to a specification
There are variations on explain, that present the results in different formats.
"},{"location":"clojure-spec/data/#workflow-for-data-specifications","title":"Workflow for data specifications","text":"Using Clojure Specification is very flexible, they can be used as much or as little as required.
Typically Specifications are created when data structures are being modeled, which can be done via experimenting in the REPL or taking a test first approach. Either way is viable.
The generative tests section shows how specifications are used to generate mock data, so creating specifications earlier on in the development process will provide a wider range of data for unit tests and repl experimentation.
Spec and nil values
Some predicates do not consider nil
as a valid value, espcially those predicates that check for a specific type
spec/nilable
will transform a predicate to use nil
(spec/valid? string? nil)\n\n(type \"what type am I\")\n(type nil)\n\n(spec/valid? (spec/nilable string?) nil)\n
"},{"location":"clojure-spec/data/and-or-specifications/","title":"Combining specifications with and and or","text":"clojure.core/and
function and clojure.core/or
function can be used to define a specification with multiple parts.
"},{"location":"clojure-spec/data/and-or-specifications/#conform-to-one-or-more-specifications","title":"Conform to One or more specifications","text":"A specification for residential address included either a house number or name. The clojure.core/or
function allows either type of value to be used and conform to the specification.
(spec/def ::house-name-number (or string? int?))\n
Using spec/or
then unique keys are required for each possible type of value. Keys are used to explain where a failure occurred if values do not conform to the specification.
(spec/def ::house-name-number (spec/or :string string?\n :number int?))\n
If specifications are uses as the options in the clojure.spec.alpha/or
then those specification names are used as the keys to explain where failure to conform to the specification happened.
(spec/def ::social-security-id (spec/or ::social-security-id-uk\n ::social-security-id-usa))\n
"},{"location":"clojure-spec/data/and-or-specifications/#conform-to-all-specifications","title":"Conform to all specifications","text":"Create a composite specification using clojure.spec.alpha/and
when all specifications should be conformed by the values
For an arranged banking overdraft limit, the value should be a positive number, that is an integer type and is less than 1000.
(spec/def ::arranged-overdraft-limit (spec/and pos? int? #(> 1000 %)))\n
If a value does not conform to any of the three specifications then the value fails the ::arranged-overdraft-limit
specification.
"},{"location":"clojure-spec/data/composite-specifications/","title":"Composing Specifications","text":"No spec is an island
Composing individual specifications is an effective way to build larger abstractions in specifications without creating fixed hierarchical structures that are harder to refactor.
Require specification namespace to the page
(ns practicalli.clojure\n (:require [clojure.spec.alpha :as spec]))\n
spec/and
is used when all specifications should be true.
(spec/def ::meaning-of-life\n (spec/and int?\n even?\n #(= 42 %)))\n
spec/or
is use when one or more specifications should be true
(spec/def ::meaning-of-life-int-or-string\n (spec/or :integer #(= 42 %)\n :string #(= \"forty two\" %)))\n
Each condition in the spec is annotated with a label for each conditional branches.
Labels are included in the return result from spec/explain
when values do not conform to a specification, providing context as to why a value failed the specification.
When an or is conformed, it returns a vector with the condition name and conformed value.
(spec/conform ::meaning-of-life-int-or-string 42)\n
(spec/conform ::meaning-of-life-int-or-string \"forty two\")\n
(spec/conform ::meaning-of-life-int-or-string :entropy)\n
(spec/explain ::meaning-of-life-int-or-string :entropy)\n
"},{"location":"clojure-spec/data/composite-specifications/#individual-specifications","title":"Individual specifications","text":"Before composing a more abstract specification, first define individual specifications
(spec/def ::first-name string?)\n
(spec/def ::last-name string?)\n
(spec/def ::residential-address string?)\n
"},{"location":"clojure-spec/data/composite-specifications/#composing-hash-map-specification","title":"Composing hash-map specification","text":"The individual specifications can now be composed into a single specification.
keys
function combines specifications to form a composite specification in the form of a Clojure hash-map.
(spec/def ::customer-details\n (spec/keys\n :req [::first-name ::last-name ::residential-address]))\n
Use the composite specification with a value
(spec/conform ::customer-details\n {::first-name \"Jenny\"\n ::last-name \"Jetpack\"\n ::residential-address \"42 meaning of life street, Earth\"})\n
"},{"location":"clojure-spec/data/conform/","title":"Conform","text":""},{"location":"clojure-spec/data/conform/#does-a-value-conform-to-a-specification","title":"Does a value conform to a specification?","text":"clojure.spec.alpha/conform
takes two arguments
- a specification
- a value to test against the specification
:clojure.spec.alpha/invalid
is returned when a value does not conform to a specification.
If the value does conform to the specification, then the value is returned. This value is referred to as a conformed value.
"},{"location":"clojure-spec/data/conform/#require-the-clojure-spec-library","title":"Require the Clojure spec library","text":"Set the namespace for the page and require clojure.spec.alpha library, setting the alias to spec
(ns practicalli.clojure.specifications\n (:require [clojure.spec.alpha :as spec]))\n
"},{"location":"clojure-spec/data/conform/#using-conform","title":"Using conform","text":"If the value conforms to the spec, a conformed value is returned
(spec/conform odd? 101)\n
When a value does not conform to a spec, the value :clojure.spec.alpha/invalid
is returned
(spec/conform even? 101)\n
(spec/conform integer? 1)\n
(spec/conform seq? [1 2 3])\n
(spec/conform seq? (range 10))\n
(spec/conform map? {})\n
(spec/conform map? (hash-map :a 1 :b 2))\n
"},{"location":"clojure-spec/data/defining-specifications/","title":"Defining specifications","text":"clojure.spec.alpha/def
binds a name to a specification, just like clojure.core/def
binds a name to a value.
Binding a name means specifications are available throughout the code and in other projects if the project is included as a library.
"},{"location":"clojure-spec/data/defining-specifications/#naming-fully-qualified-keywords","title":"Naming - fully qualified keywords","text":"Specification names should use fully qualified keywords, typically using the namespace in which the specification is defined in.
Define a namespace for the page and require Clojure Spec
(ns practicalli.clojure.specifications\n (:require [clojure.spec.alpha :as spec]))\n
(spec/def :practicalli.clojure.specifications/number number?)\n
"},{"location":"clojure-spec/data/defining-specifications/#auto-resolve-macro","title":"auto-resolve macro","text":"::
double colon is the auto-resolve macro, which will pre-pend the current namespace to the specification keyword. The ::
notation removes the need to edit fully qualified names should a specification be moved to a different namespace.
(spec/def ::number number?)\n
Fully Qualified keywords
Using fully qualified keywords ensures they are unique and therefore can be used across all projects.
Namespaces are usually unique as they include the name of the company or organization behind the code and any project or component names used to organize the code.
"},{"location":"clojure-spec/data/entity-maps/","title":"Entity maps","text":"An entity map is a Specification for a Clojure hash-map of key-value pairs.
A hash-map is a very effective way to express information in Clojure. The key should be a descriptive label to express meaning of the value it is associated with. Without the keys describing the meaning, it is harder for a developer to understand the data.
{:account-id 12345 :user-name \"jenny1999\" :name \"Jenny Jetpack\" :address \"42 Meaning place, Earth\" :social-security \"ABC-123-45-6789\"}\n
A hash-map contains any number of key-value pairs, keys are used for efficient lookup so there is no concern over ordering. Passing a hash-map as an argument to a function reduces refactoring required as the signature of the function remains the same and functions can be selective as to which key-value pairs they use.
For these reasons, hash-maps are a very common data structure to pass data between functions.
"},{"location":"clojure-spec/data/entity-maps/#defining-entity-maps","title":"Defining entity maps","text":"The Clojure Spec keys
function is used to create a specification for a hash-map of key-value pairs.
keys
creates a specification from required keys, :req
, and optional keys :opt
.
To define the specification for a player in an online game, first the individual specifications that make up the player hash-map are created.
(spec/def ::account-id uuid?)\n(spec/def ::name string?)\n(spec/def ::score int?)\n(spec/def ::profile string?)\n(spec/def ::games-played #{:vectron :utrazap :avakato})\n
Those specifications are composed together to create a specification for the player
(spec/def\n ::player-account\n (spec/keys :req [::account-id ::name ::score]\n :opt [::profile ::games-played]))\n
For a hash-map to meet the ::player-account
specification it must contain the :req
keys with values that conform to the individual specifications. The hash-map can also include any key-value pairs that conform to the :opt
specifications.
If any keys are in the map that do not appear in either :req
or :opt
then that hash-map does not conform to the ::player-account
specification.
"},{"location":"clojure-spec/data/entity-maps/#example-covid19-dashboard","title":"Example: covid19-dashboard","text":"The coronavirus-cases-data
function takes a hash-map of values to make that function easier to extend without breaking existing calls
Default values can be used if no value is passed as an argument. Extra values can be ignored without breaking the code.
Coronavirus Cases Specification
(defn fun-name\n [csv location date])\n\n(defn coronavirus-cases-data\n \"Extract and transform cases data for specific locations and date\"\n [{:keys [csv-file locations date]}]\n #_(-> (extract-data-from-csv csv-file)\n (data-set-remove-locations locations)\n (data-set-specific-date date)))\n\n(coronavirus-cases-data\n {:csv-file \"data-sets/uk-coronavirus-cases.csv\"\n :locations #{\"Nation\" \"Country\" \"Region\"}\n :date \"2020-04-30\"})\n\n;; Define the individual keys for the hash-map\n(spec/def ::csv-file string?)\n(spec/def ::locations set?)\n(spec/def ::date string?)\n\n(spec/def ::cases-data\n (spec/keys :req [::csv-file ::locations ::date]))\n
"},{"location":"clojure-spec/data/explain/","title":"Explaining non-conforming values","text":"clojure.spec.alpha/explain
describes why a value does not satisfy a specification.
clojure.spec.alpha/explain
takes two arguments
- a specification
- a value to test against the specification
Success
string is sent to standard out if the value meets the specification
A string explaining where the value deviates from the specification is sent to standard out if the value does not meet the specification.
There are several variations on the explain function for different situations
explain
- sends the return value to the standard out / REPL explain-str
- returns a human readable result. explain-data
- returns a data structure of the error to be processed by other code
"},{"location":"clojure-spec/data/explain/#example-of-a-failing-value","title":"Example of a failing value","text":"First define a namespace and require the Clojure Spec namespace
(ns practicalli.clojure.specifications\n (:require [clojure.spec.alpha :as spec]))\n\n(spec/def ::meaning-of-life #(= 42 %))\n
Given the following specification
(spec/explain ::meaning-of-life 24)\n
Using the value 24
with that specification will fail. Using explain we can see why
(spec/def ::meaning-of-life-int-or-string\n (spec/or :integer #(= 42 %)\n :string #(= \"forty two\" %)))\n
In this case explain returned the
- value being checked against the spec
- result of that check (failed)
- predicate used to check the value
- spec name used to check the value
Notice that the value failed on the first condition, :integer
, then stopped without checking the second, :string
. The spec/and
macro works the same as clojure.core/and
in that is stops as soon as something fails.
(spec/explain ::meaning-of-life-int-or-string 24)\n
In this case we still have the value checked, the result and the predicate More information is provided as to where in the spec the value failed :at
shows the path in the spec where the failure occurred, very useful for nested structures This shows the value of naming your specs descriptively
"},{"location":"clojure-spec/data/explain/#explain-with-a-string","title":"Explain with a string","text":"rather than send information to the system out
(spec/explain-str ::meaning-of-life 24)\n
(spec/explain-data ::meaning-of-life 24)\n
"},{"location":"clojure-spec/data/hierarchical-specifications/","title":"Hierarchical Specifications","text":"Defining specifications for data that is hierarchical or nested in nature.
(ns practicalli.clojure\n (:require [clojure.spec.alpha :as spec]))\n
"},{"location":"clojure-spec/data/hierarchical-specifications/#example-hierarchical-data","title":"Example hierarchical data","text":"{:top-level-key {:nested-key \"value\"}}\n
"},{"location":"clojure-spec/data/hierarchical-specifications/#individual-specifications","title":"Individual specifications","text":"(spec/def ::first-name string?)\n
(spec/def ::last-name string?)\n
(spec/def ::residential-address string?)\n
"},{"location":"clojure-spec/data/hierarchical-specifications/#composite-specification","title":"Composite Specification","text":"keys
function combines specifications to form a composite specification in the form of a Clojure hash-map.
(spec/def ::customer-details\n (spec/keys\n :req [::first-name ::last-name ::residential-address]))\n
"},{"location":"clojure-spec/data/hierarchical-specifications/#hierarchical-specification","title":"Hierarchical Specification","text":"A user account is composed of a user-id and customer details. Rather than include the individual customer details, the composite customer-details specification.
The ::user-id
specification is as follows
(spec/def ::user-id uuid?)\n
The ::user-account
specification
(spec/def ::user-account\n (spec/keys\n :req [::user-id ::customer-details]))\n
The following data structure will conform to the specification
{::user-id #uuid \"97bda55b-6175-4c39-9e04-7c0205c709dc\"\n ::customer-details {::first-name \"Jenny\"\n ::last-name \"Jetpack\"\n ::residential-address \"Earth\"}}\n
"},{"location":"clojure-spec/data/literal-values/","title":"Literal values","text":"Sets can be used as predicate functions returning true if the value is within the set
Checking valid playing cards
Define a namespace for the page and require Clojure Spec
(ns practicalli.clojure\n (:require [clojure.spec.alpha :as spec]))\n
(spec/valid? #{:club :diamond :heart :spade} :club)\n
(spec/valid? #{:club :diamond :heart :spade} 42)\n
Answer to the ultimate question?
(spec/valid? #{42} 42)\n
Using sets for literal values is similar to using the clojure.core/contains?
function with a set collection type.
(contains? #{:clubs :diamonds :hearts :spades} :hearts )\n
"},{"location":"clojure-spec/data/map-literals/","title":"Map literal syntax - #:
and #::
","text":"#:
map literal macro for Clojure hash-maps adds a given namespace to all the keywords contained in the hash-map.
#::
map literal macro for keyword auto-resolve adds the current fully qualified namespace to all the keywords in the hash-map
"},{"location":"clojure-spec/data/map-literals/#require-clojure-spec-in-the-namespace-definition","title":"Require clojure spec in the namespace definition","text":"(ns practicalli.clojure\n (:require [clojure.spec.alpha :as spec]))\n
In this example the keys in the map are unqualified.
{:simplifying []\n :keyword-names []\n :with-autoresolve []\n :map-literal []}\n
"},{"location":"clojure-spec/data/map-literals/#qualifying-keys-with-auto-resolve","title":"Qualifying keys with auto-resolve","text":"Using the map literal macro for auto-resolve instructs Clojure to treat all keys in the map as qualified to the current namespace
The following hash-map has the map literal macro.
#::{:simplifying []\n :keyword-names []\n :with-autoresolve []\n :map-literal []}\n
This is the same as explicitly writing out the fully qualified domain for each key in the map.
However, if we move the map to another namespace, then the explicit namespaces would need to be updated.
{:practicalli.clojure/simplifying []\n :practicalli.clojure/keyword-names []\n :practicalli.clojure/with-autoresolve []\n :practicalli.clojure/map-literal []}\n
"},{"location":"clojure-spec/data/map-literals/#qualifying-keywords-with-a-specific-name","title":"Qualifying keywords with a specific name","text":"Rather than take the name from the current namespace, an explicit name can be added to all the keys in the map
#:practicalli.naming {:simplifying []\n :keyword-names []\n :with-autoresolve []\n :map-literal []}\n
This is the same as explicitly writing that name in front of each of the keywords in the map.
# {:practicalli.naming/simplifying []\n :practicalli.naming/keyword-names []\n :practicalli.naming/with-autoresolve []\n :practicalli.naming/map-literal []}\n
Map literals are relevant to Entity maps with spec.
"},{"location":"clojure-spec/data/predicate-functions/","title":"Spec - Predicate functions","text":"A predicate is a function that returns a true or false value and their names end with ?
by convention.
(odd? 1)\n
(string? \"am i a string\")\n
(int? 2.3)\n
(int? 2.3)\n
clojure.core
predicate functions
clojure.core
defines 80+ predicate functions
"},{"location":"clojure-spec/data/predicate-functions/#predicate-functions-in-specs","title":"Predicate functions in specs","text":"Predicate functions can be used as un-named specifications to test values conform.
Include the clojure.spec.alpha
namespace to access the spec functions.
(require '[clojure.spec.alpha :as spec])\n
(spec/conform int? 42)\n
(spec/conform seq? (range 4))\n
"},{"location":"clojure-spec/data/predicate-functions/#custom-predicate-functions","title":"Custom predicate functions","text":"Define custom predicate functions with defn
or fn
or the short form #()
Using an anonymous function
(spec/conform (fn [value] (= value 42)) 42)\n
When the expression is quite terse, then the short form of an anonymous function is typically used. The %
represents the value passed as an argument.
(spec/conform #(= % 42) 42)\n
"},{"location":"clojure-spec/data/registry/","title":"Registry for unique and re-usable specifications","text":"So far we have just use predicate functions directly in the code examples.
Using a registry, specs can be uniquely defined across the whole project. Defining a spec gives that spec a name that has a fully qualified namespace
Use the spec specific def
function to bind a new spec name and fully qualified namespace and place it in the registry
(spec/def :playing-card/suit #{:club :diamond :heart :spade} )\n
(spec/conform :playing-card/suit :diamond)\n
(spec/def :show-cats/cat-bread #{:abyssinian :birman :chartreau :devon-rex\n :domestic-short-hair :domestic-long-hair})\n
"},{"location":"clojure-spec/data/registry/#removing-specs-from-the-registry","title":"Removing specs from the registry","text":"Named specifications can be removed from the registry by binding the name to nil
.
If specification names are to be refactored, then the original name should be set to nil
and evaluated, before changing the name. This will ensure stale specifications are not residing in the REPL.
Here is a named specification as an example
(spec/def ::unwanted #{:abandoned})\n
The specification is evaluated in the REPL (above) and currently works.
(spec/conform ::unwanted :abandoned)\n
Remove this specification from the registry by binding it to nil
(spec/def ::unwanted nil)\n
Now the specification is unavailable
(spec/conform ::unwanted :abandoned)\n
Registry not persistent
Restarting the REPL will loose all specification names in the registry as it is not persistent across REPL sessions.
"},{"location":"clojure-spec/data/valid-q/","title":"Is the value valid?","text":"clojure.spec.alpha/valid?
takes two arguments
- a specification
- a value to test against the specification
clojure.spec.alpha/valid?
is a predicate function.
true
is returned if the value meets the specification, otherwise false
is returned.
"},{"location":"clojure-spec/data/valid-q/#require-the-clojure-spec-library","title":"Require the Clojure spec library","text":"Set the namespace for the page and require clojure.spec.alpha library, setting the alias to spec
(ns practicalli.clojure.specifications\n (:require [clojure.spec.alpha :as spec]))\n
"},{"location":"clojure-spec/data/valid-q/#using-valid","title":"Using valid?","text":"If the value is valid then a boolean true is returned. Experiment with different values and predicate functions.
(spec/valid? even? 180)\n
(spec/valid? string? \"Am I a valid string\")\n
"},{"location":"clojure-spec/data/valid-q/#using-custom-predicate-functions","title":"using custom predicate functions","text":"Create fn
definitions to use as predicate functions. Any function that returns true or false can be used.
(spec/valid? (fn [value] (> value 1024)) 8080)\n
The custom predicate function may also be written in the shorter form of a fn
definition
(spec/valid? #(> % 1024) 8080)\n
Use def
to bind names to custom predicate functions if they are used more than once in the code base.
In this example a name is bound to a function that checks if a port is within the range of IANA registered networking ports.
(def registered-port-range?\n \"Network port number within IANA registered port range\"\n #(and (> % 1024) #(< % 49151) )\n\n(spec/valid? registered-port-range? 8080)\n
"},{"location":"clojure-spec/functions/","title":"Specification for function definitions","text":"Define specifications for your custom functions
- Additional documentation - argument and return values and the relationship between them.
- Instrumenting functions - checking for correct argument values
- Generative testing - using argument specifications to generate comprehensive test data.
Many of the functions in clojure.core
have specifications in the latest version of Clojure. The specifications for clojure.core functions can be found in the clojure/core.specs.alpha repository on GitHub.
"},{"location":"clojure-spec/functions/#clojurecore-examples","title":"clojure.core examples","text":"Specifications used for the defn
, defn-
, fn
functions in clojure.core
clojure.core specification examples
(s/def ::param-list\n (s/and\n vector?\n (s/cat :params (s/* ::binding-form)\n :var-params (s/? (s/cat :ampersand #{'&} :var-form ::binding-form)))))\n\n(s/def ::params+body\n (s/cat :params ::param-list\n :body (s/alt :prepost+body (s/cat :prepost map?\n :body (s/+ any?))\n :body (s/* any?))))\n\n(s/def ::defn-args\n (s/cat :fn-name simple-symbol?\n :docstring (s/? string?)\n :meta (s/? map?)\n :fn-tail (s/alt :arity-1 ::params+body\n :arity-n (s/cat :bodies (s/+ (s/spec ::params+body))\n :attr-map (s/? map?)))))\n
"},{"location":"clojure-spec/functions/documentation/","title":"Documentation","text":"The Clojure doc
function shows the doc string included in a function definition, eg. defn
expressions.
When a specification is defined for a function using fdef
the specification is included in the output of the Clojure doc
function.
Including specification details clarifies the precise way to use the function and the information it expects. When a function has a specification the doc string for that function can focus on the purpose of the function rather than the specific types of data used, as that is covered by the function specification.
"},{"location":"clojure-spec/functions/documentation/#example","title":"Example","text":"(clojure.repl/doc ::rank)\n\n;; :practicalli.card-game-specifications/rank\n;; Spec\n;; (into #{:king :queen :ace :jack} (range 2 11))\n
When adding a specification to a function definition, doc
will also show the specification details along with the function doc-string.
"},{"location":"clojure-spec/functions/documentation/#live-example","title":"Live example","text":"Define the namespace and include clojure spec and clojure.repl (which contains the doc function)
(ns practicalli.clojure\n (:require [clojure.repl :as repl]\n [clojure.spec.alpha :as spec]))\n
Print the documentation for the map
function
(repl/doc map)\n
Print the documentation for the :playing-card/suit
(clojure.repl/doc :playing-card/suit)\n
#{:spade :heart :diamond :club}\n
(repl/doc :cat-show:cat-bread)\n
"},{"location":"clojure-spec/functions/function-definition-specifications/","title":"Function definition specifications","text":"clojure.spec.alpha/fdef
defines a specification for a function definition, providing specific specification for
- arguments passed when calling a function
- return value expected
- relationships between arguments and return value
"},{"location":"clojure-spec/functions/function-definition-specifications/#examples","title":"Examples","text":"The practicalli.database-access/new-account-holder
function takes a customer details specification and returns an account-holder-id
specification.
practicalli.database-access/new-account-holder
(spec/fdef practicalli.database-access/new-account-holder\n :args (spec/cat :customer ::customer-details)\n :ret ::account-holder-id)\n
"},{"location":"clojure-spec/functions/higher-order-functions/","title":"Higher order functions","text":"Higher order functions are common in Clojure and spec provides fspec to support spec\u2019ing them.
(defn value-added-tax\n [tax-rate]\n #(+ (* tax-rate %) %))\n
The value-added-tax function returns an anonymous function that adds the value of tax to the given value.
Define a namespace for the page and require Clojure Spec
(ns practicalli.clojure\n (:require [clojure.spec.alpha :as spec]))\n
Declare a function spec for value-added-tax using clojure.spec.alpha/fspec
for the return value:
(s/fdef value-added-tax\n :args (spec/cat :tax-rate number?)\n :ret (spec/fspec :args (s/cat :value number?)\n :ret number?))\n
The :ret
specification uses fspec
to declare that the returning function takes and returns a number.
"},{"location":"clojure-spec/generative-testing/","title":"Generative testing with Spec and Spec Test","text":"Clojure spec has been used so far to create specifications for both data and functions.
Now spec and spec test libraries are used not just validity checking, but also to generate random samples of the data that can be used for extensive testing.
Generative testing provides a far more effective alternative to unit testing.
clojure.spec.test/check/instrument
verifies that calls to a function satisfy the function's specification, the :arg
in fdef
.
clojure.spec.test/check
function generates 1000 data values to be used as the inputs to a function, checks that the invocation of the function satisfies its specification, the :ret
and :fn
in fdef
. The argument specification, :arg
in fdef
is used to generate a wide range of results, which are more capable of finding edge cases that fail.
"},{"location":"clojure-spec/generative-testing/#example-card-game","title":"Example: card game","text":"practicalli/spec-generative-testing is a simple card game with specifications that are used for basic generative testing.
"},{"location":"clojure-spec/generative-testing/#references","title":"References","text":" - Clojure.org guides: Spec - Generators
- API reference: clojure.spec.gen.alpha
- API reference: clojure.spec.test.alpha
- Video: How to do Stateful Property Testing in Clojure?
"},{"location":"clojure-spec/generative-testing/predicate-generators/","title":"Generators for predicate specifications","text":"Specifications are used to generate a wide range of random data. A generator for the specification is obtained and then data is generated.
"},{"location":"clojure-spec/generative-testing/predicate-generators/#predicate-generators","title":"Predicate generators","text":"(spec-gen/generate (spec/gen int?))\n
(spec-gen/generate (spec/gen nil?))\n
(spec-gen/sample (spec/gen string?))\n
(spec-gen/generate (spec/gen #{:club :diamond :heart :spade}))\n
(spec-gen/sample (spec/gen #{:club :diamond :heart :spade}))\n
"},{"location":"clojure-spec/generative-testing/example-projects/","title":"Example projects using Clojure Spec","text":"Project How the project uses Spec seancorfield/next-jdbc Data specifications using predicates, function definition argument specifications More examples welcome
Other example projects that use interesting features of Spec are most welcome. Raise an issue on the project issue tracker with details.
"},{"location":"clojure-spec/generative-testing/example-projects/next-jdbc/","title":"Projects using Clojure spec - next-jdbc","text":"The next-jdbc project is a modern low-level Clojure wrapper for JDBC-based access to databases.
The project defines data specifications using predicates and
"},{"location":"clojure-spec/generative-testing/example-projects/next-jdbc/#defining-specifications","title":"Defining specifications","text":"Specifications are defined within a single file src/next/jdbc/specs.clj
.
Specifications start with clojure.spec.alpha/def
expressions, using predicate functions as specifications. There is also a custom predicate function called
Function definition specifications follow, using the clojure.spec.alpha/fdef
function. The fdef
functions define the specification for the arguments of each function. The fdef
function name is the same as the function definition it is defining a specification for.
"},{"location":"clojure-spec/generative-testing/example-projects/next-jdbc/#instrumenting-specifications","title":"Instrumenting specifications","text":"Instrumenting functions provides automatic checking that argument in a function call conforms to the specification.
Rather than write individual expressions to instrument each function, a var called fns-with-specs
contains a collection of names for all the fdef
function definition specifications.
(def ^:private fns-with-specs\n [`jdbc/get-datasource\n `jdbc/get-connection\n `jdbc/prepare\n `jdbc/plan\n `jdbc/execute!\n `jdbc/execute-one!\n `jdbc/transact\n `jdbc/with-transaction\n `connection/->pool\n `prepare/execute-batch!\n `prepare/set-parameters\n `prepare/statement\n `sql/insert!\n `sql/insert-multi!\n `sql/query\n `sql/find-by-keys\n `sql/get-by-id\n `sql/update!\n `sql/delete!])\n
Instrument all the functions by passing fns-with-specs
as an argument to the clojure.spec.test.alpha/instrument
function.
This call is wrapped in a simple handler function for convenience.
(defn instrument []\n (clojure.spec.test.alpha/instrument fns-with-specs))\n
To remove the checking of argument specifications, clojure.spec.test.alpha/unstrument
is passed fns-with-specs
, again wrapped in a convinced function.
(defn unstrument []\n (clojure.spec.test.alpha/unstrument fns-with-specs))\n
"},{"location":"clojure-spec/projects/","title":"Clojure Spec Projects","text":"A series of projects showing approaches to using specifications and generating data for testing.
Project Description card game writing specifications and generating data from those specifications banking-on-clojure simplified online bank account using TDD, data and functional specifications and generative testing Bonbon card game A flavorful card game with clojure spec Genetic Programming With clojure.spec https://www.youtube.com/watch?v=xvk-Gnydn54 ClojureScript game with spec"},{"location":"clojure-spec/projects/#references","title":"References","text":" - Practicalli - Clojure Spec playlist - live coding to define specifications and generative testing
"},{"location":"clojure-spec/projects/bank-account/","title":"Spec project: Bank Account","text":"A relatively simple bank account application with data and function specifications, including generative testing data and function instrumentation.
"},{"location":"clojure-spec/projects/bank-account/#hintunder-active-development","title":"Hint::Under active development","text":"Developed as part of the Practicalli study guide live broadcasts
"},{"location":"clojure-spec/projects/bank-account/#create-depsedn-project","title":"Create deps.edn project","text":"Use Clojure CLI and clj-new
clojure -M:new app practicalli/banking-on-clojure\n
"},{"location":"clojure-spec/projects/bank-account/#hintpracticallibanking-on-clojure-repository","title":"Hint::practicalli/banking-on-clojure repository","text":"practicalli/banking-on-clojure repository contains the latest code to date for this project.
"},{"location":"clojure-spec/projects/bank-account/#outline-design-of-project","title":"Outline design of project","text":"Data Specifications are created for
- Customer Details \u2714
- Account holder \u2714
- Bank account
- Multiple Bank accounts
- Credit Card
- Mortgage
Functions and specifications are created for
- register-account-holder \u2714
- open-credit-account
- open-savings-account
- open-credit-card-account
- open-mortgage-account
- Make a payment
- Send account notification
- Check for overdraft
"},{"location":"clojure-spec/projects/bank-account/#development-workflow","title":"Development Workflow","text":" - Write a failing test \u2714
- write mock data \u2714
- write an function definition that returns the argument \u2714
- run tests - tests should fail \u2714
- write a spec for the functions argument - customer \u2714
- write a spec for the return value \u2714
- write a spec for relationship between args and return value
- replace the mock data with generated values from specification \u2714
- update functions and make tests pass \u2714
- instrument functions
- run specification checks
\u2714
Images to add
Running tests that fail on a spec in CIDER spacemacs-cider-test-spec-fail-banking-on-clojure-project.png
Running tests that fail on a spec on CircleCI circle-ci-banking-on-clojure-spec-test-runner-fail-register-account-holder-did-not-conform-to-spec.png
"},{"location":"clojure-spec/projects/bank-account/account-holder-specification/","title":"Account holder specification","text":"The account holder has the same information as custom details with the addition of an account-id
In the register-account-holder a uuid is generated for the account id, So a spec can be defined for this type
(spec/def ::account-id uuid?)\n
"},{"location":"clojure-spec/projects/bank-account/account-holder-specification/#design-decision-hierarchical-or-composite","title":"Design decision: hierarchical or composite","text":"There are several approaches to combining, depending on the shape of the data used
The account holder is a hash-map, so spec/keys
will create the map from specification keys
Including the customer-details specification in spec/keys
would include the customer details as a nested hash-map
(spec/def ::account-holder-hierarchy\n (spec/keys\n :req [::account-id ::customer-details]))\n
A valid data structure for this specification is a map with two keys, account-id
and customer-details
. account-id
is a uuid value, customer-details is a hash-map of values that conform to the customer-details specification
(spec/valid? ::account-holder-hierarchy\n #::{:account-id (java.util.UUID/randomUUID)\n :customer-details #:: {:first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street, Earth\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"}})\n;; => true\n
Flat data structures are usually preferred in Clojure over a nested hierarchy. Rather than use the ::customer-details specification as a key in the spec/keys
expression. The individual specifications that make up ::customer-details can be used.
(spec/def ::account-holder-composition\n (spec/keys\n :req [::account-id ::first-name ::last-name ::email-address ::residential-address ::social-security-id]))\n
(spec/valid? ::account-holder-composition\n #::{:account-id (java.util.UUID/randomUUID)\n :first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street, Earth\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"})\n
"},{"location":"clojure-spec/projects/bank-account/customer-details-specification/","title":"Customer details specification","text":"Create a new file for the specifications, src/practicalli/banking_specifications.cljc
, with the namespace practicalli.banking-specifications
.
Require the clojure.spec.alpha
library with an alias of spec
.
(ns practicalli.banking-specifications\n (:require [clojure.spec.alpha :as spec]))\n
"},{"location":"clojure-spec/projects/bank-account/customer-details-specification/#define-basic-customer-details","title":"Define basic customer details","text":"Define a specification for the customer-details map, composed of all the required keys that define a customer.
The bank legally requires specific information about a customer in order to add them as an account holder
(spec/def ::first-name string?)\n(spec/def ::last-name string?)\n(spec/def ::email-address string?)\n
(spec/def ::email-address\n (spec/and string?\n #(re-matches #\"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,63}$\"\n %)))\n
This specification will require a custom generator
A residential address is made from several pieces of information and is defined as a composite specification, from several specifications.
(spec/def ::house-name-number (spec/or :string string?\n :number int?))\n(spec/def ::street-name string?)\n(spec/def ::post-code string?)\n(spec/def ::county string?)\n(spec/def ::country string?)\n
(spec/def ::residential-address (spec/keys :req [::house-name-number ::street-name ::post-code]\n :opt [::county ::country]))\n
A social security number specification is also a candidate for a composite specification. Social security numbers may take different forms and even have different names in different countries, eg. the USA SSN is a nine-digit number in the format \"AAA-GG-SSSS\"
In the UK the social security number is called the National Insurance number and is of the form QQ123456C
(spec/def ::social-security-id-uk string?)\n(spec/def ::social-security-id-usa string?)\n
(defn social-security-number-usa? [value] (= 9 (count value)))\n(defn social-security-number-uk? [value] (= 11 (count value)))\n(spec/def ::social-security-id-usa (spec/and string? social-security-number-usa?))\n(spec/def ::social-security-id-uk (spec/and string? social-security-number-uk?))\n
These specifications required a custom generator in order to produce correct data each time.
A general social security specification can now be defined, with one of any of the country specific specifications
(spec/def ::social-security-id (spec/or ::social-security-id-uk\n ::social-security-id-usa))\n
"},{"location":"clojure-spec/projects/bank-account/customer-details-specification/#infoa-more-detailed-email-specification","title":"INFO::A more detailed email specification","text":"Use a regular expression to define the syntax of an email address, eg. jenny@jetpack.org
"},{"location":"clojure-spec/projects/bank-account/customer-details-specification/#infodetailed-social-security-numbers-would-check-the-different-forms","title":"INFO::Detailed social security numbers would check the different forms","text":"Predicate functions can be defined to check for the size of the different social security forms.
"},{"location":"clojure-spec/projects/bank-account/customer-details-specification/#composing-the-customer-details-specification","title":"Composing the customer details specification","text":"A customer details specification is a hash-map of key value pairs. The keys are the specifications that have just been defined.
spec/keys
creates a specification for a hash-map with required and optional keys. spec/keys
also includes a check for a map, so no explicit check for a map is required.
(spec/def ::customer-details\n (spec/keys\n :req [::first-name ::last-name ::email-address ::residential-address ::social-security-id]))\n
"},{"location":"clojure-spec/projects/bank-account/function-specifications/","title":"Specifications for function definitions - fdef
","text":"Create a spec/fdef
for the register-account-holder function
clojure.spec.alpha/fdef
defines a specification for a function definition, defn
. Specifications can attached to the arguments using :args
, the return value using :ret
and the relationship between the two using fn
.
:args
, :ret
and fn
are optional, although args
and ret
are required if you want to use :fn
"},{"location":"clojure-spec/projects/bank-account/function-specifications/#add-a-spec-to-cover-the-function-arguments","title":"Add a spec to cover the function arguments","text":":args
is a compound specification that covers all the function arguments. The :args
spec is invoked with the arguments in a list, so working with them is like using apply.
Using regular expressions we can find the right arguments to give to the specification. Regular expression spec functions include
The register-account-holder only takes one argument, so spec/cat
is used to bind a local key to the specification.
The function is defined in the practicalli.banking-on-clojure
namespace. Require that namespace in the current ns
form.
(ns practicalli.banking-specifications\n (:require [clojure.spec.alpha :as spec]\n [clojure.spec.gen.alpha :as spec-gen]\n [clojure.spec.test.alpha :as spec-test]\n\n [practicalli.banking-on-clojure :as SUT]))\n
The SUT
alias is used for the banking-on-clojure namespace, as is done with clojure.test
unit test namespaces.
(spec/fdef SUT/register-account-holder\n :args (spec/cat :customer\n :practicalli.bank-account-spec/customer-details))\n
"},{"location":"clojure-spec/projects/bank-account/function-specifications/#checking-function-calls-against-the-spec-instrument","title":"Checking function calls against the spec - instrument","text":"spec/fdef
by itself does not run checks against the specs
(register-account-holder {})\n;; => {:account-id #uuid \"3a6dddb7-dd87-485e-90f8-8c8975302845\"}\n
Require the Clojure spec test library
(require '[clojure.spec.test.alpha :as spec-test])\n
spec/instrument
will add a run time check for the specification
(spec-test/instrument `SUT/register-account-holder)\n
No the function is instrumented, data used as arguments of a function call will be checked against the specification.
(register-account-holder {::bad \"data\"})\n
This function call throws an exception because of the specification attached to the :args
section of the fdef
specification.
The error report provides detailed and quite clear information to help diagnose the issue
1. Unhandled clojure.lang.ExceptionInfo\n Spec assertion failed.\n\n Spec: #object[clojure.spec.alpha$regex_spec_impl$reify__2509 0x12b66a86 \"clojure.spec.alpha$regex_spec_impl$reify__2509@12b66a86\"]\n Value: (#:practicalli.bank-account-design-journal{:bad \"data\"})\n\n Problems:\n\n val: #:practicalli.bank-account-design-journal{:bad \"data\"}\n in: [0]\n failed: (contains? % :practicalli.bank-account-spec/first-name)\n spec: :practicalli.bank-account-spec/customer-details\n at: [:customer]\n\n val: #:practicalli.bank-account-design-journal{:bad \"data\"}\n in: [0]\n failed: (contains? % :practicalli.bank-account-spec/last-name)\n spec: :practicalli.bank-account-spec/customer-details\n at: [:customer]\n\n val: #:practicalli.bank-account-design-journal{:bad \"data\"}\n in: [0]\n failed: (contains? % :practicalli.bank-account-spec/email-address)\n spec: :practicalli.bank-account-spec/customer-details\n at: [:customer]\n\n val: #:practicalli.bank-account-design-journal{:bad \"data\"}\n in: [0]\n failed: (contains? % :practicalli.bank-account-spec/residential-address)\n spec: :practicalli.bank-account-spec/customer-details\n at: [:customer]\n\n val: #:practicalli.bank-account-design-journal{:bad \"data\"}\n in: [0]\n failed: (contains? % :practicalli.bank-account-spec/social-security-id)\n spec: :practicalli.bank-account-spec/customer-details\n at: [:customer]\n
Calling the register-account-holder with a value that conforms to the bank-account-spec for customer details returns the new value for account-holder
(register-account-holder\n #:practicalli.bank-account-spec\n {:first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street, Earth\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"})\n\n;; => {:practicalli.bank-account-spec/first-name \"Jenny\", :practicalli.bank-account-spec/last-name \"Jetpack\", :practicalli.bank-account-spec/email-address \"jenny@jetpack.org\", :practicalli.bank-account-spec/residential-address \"42 meaning of life street, Earth\", :practicalli.bank-account-spec/postal-code \"AB3 0EF\", :practicalli.bank-account-spec/social-security-id \"123456789\", :account-id #uuid \"e0f327de-4e92-479e-a9de-468e2c7c0e6d\"}\n
"},{"location":"clojure-spec/projects/bank-account/function-specifications/#add-a-specification-to-the-return-value","title":"Add a specification to the return value","text":"Attach the account-holder details specification to :ret
(spec/fdef register-account-holder\n :args (spec/cat :customer\n :practicalli.bank-account-spec/customer-details)\n :ret :practicalli.bank-account-spec/account-holder)\n
If the register-account-holder
logic changes to return a different value that the return spec, then an exception is raised
Returns an integer rather than a uuid
(defn register-account-holder\n \"Register a new customer with the bank\n Arguments:\n - hash-map of customer-details\n Return:\n - hash-map of an account-holder (adds account id)\"\n [customer-details]\n\n (assoc customer-details\n :practicalli.bank-account-spec/account-id\n (rand-int 100000)\n #_(java.util.UUID/randomUUID)))\n
So this should fail
(register-account-holder\n #:practicalli.bank-account-spec\n {:first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street, Earth\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"})\n
It still works as spec-test/instrument
only checks the args value.
spec-test/check
will test the return value with generated tests
(require '[clojure.spec.gen.alpha :as spec-gen])\n
(spec-test/check `SUT/register-account-holder)\n
The result is 100 generated tests that all fail, because the function was changed to return integers, not uuids
1. Caused by clojure.lang.ExceptionInfo\n Couldn't satisfy such-that predicate after 100 tries.\n {:pred #function[clojure.spec.alpha/gensub/fn--1876],\n :gen {:gen #function[clojure.test.check.generators/such-that/fn--8322]},\n :max-tries 100}\n
"},{"location":"clojure-spec/projects/bank-account/function-specifications/#change-the-function-back-again","title":"Change the function back again","text":"(defn register-account-holder\n \"Register a new customer with the bank\n Arguments:\n - hash-map of customer-details\n Return:\n - hash-map of an account-holder (adds account id)\"\n [customer-details]\n\n (assoc customer-details\n :practicalli.bank-account-spec/account-id\n (java.util.UUID/randomUUID)))\n
"},{"location":"clojure-spec/projects/bank-account/function-specifications/#instrument-the-function","title":"Instrument the function","text":"Testing function calls against the specification
Requires the spec test namespace
(require '[clojure.spec.test.alpha :as spec-test])\n
Instrument the spec to add checking, this only checks the arguments are correct.
(spec-test/instrument `practicalli.bank-account/register-account-holder)\n
(register-account-holder {:first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"})\n
"},{"location":"clojure-spec/projects/bank-account/generate-test-data/","title":"Generate test data","text":""},{"location":"clojure-spec/projects/bank-account/generate-test-data/#generate-test-data-from-specifications","title":"Generate test data from Specifications","text":"Now there are specifications for the customer-details and account-details, spec can generate random data for use with tests.
"},{"location":"clojure-spec/projects/bank-account/generate-test-data/#add-requires-to-the-test-namespace","title":"Add requires to the test namespace","text":"Edit the src/test/practicalli/banking_on_clojure.clj
and add requires for the banking-specifications
, clojure.spec.alpha
and clojure.spec.test.alpha
.
(ns practicalli.banking-on-clojure-test\n (:require [clojure.test :refer [deftest is testing]]\n [clojure.spec.alpha :as spec]\n [clojure.spec.test.alpha :as spec-test]\n [clojure.spec.gen.alpha :as spec-gen]\n [practicalli.banking-on-clojure :as SUT]\n [practicalli.banking-specifications]))\n
"},{"location":"clojure-spec/projects/bank-account/generate-test-data/#using-generators-to-generate-data","title":"Using Generators to generate data","text":"Test data can now be generated from this specification, creating values for each key in the hash-map.
The ::email-address specification has been simplified, as the regular expression version requires a custom generator (no built in generator to support this specification). The simplified email specification is:
(spec/def ::email-address string?)\n
With the simplified email specification, the customer-details specification can be used to generate all the data using the built in clojure.spec.alpha generators.
(spec-gen/generate (spec/gen :practicalli.banking-specifications/customer-details))\n\n;; => #:practicalli.banking-specifications\n{:first-name \"r7q9RFB202v7a69z\",\n :last-name \"6N5\",\n :email-address \"L6dd946p680P0pIYZ33CGZd0\",\n :residential-address\n #:practicalli.banking-specifications{\n :house-name-number \"gCuRMe0C8\",\n :street-name \"5\",\n :post-code \"VN\"},\n :social-security-id \"a7P0xfBNPv6\"}\n
Bind the result of this function to a name and it can be used as mock data throughout the unit tests defined.
(defn customer-details-mock-data\n (spec-gen/generate (spec/gen :practicalli.banking-specifications/customer-details)))\n
The generated data can also be used with function definitions and clojure.spec.test.alpha/check
function.
"},{"location":"clojure-spec/projects/bank-account/generate-test-data/#hintlibraries-for-custom-generators","title":"Hint::Libraries for custom generators","text":"gfredericks/test.chuck is a utility library for test.check and will work with clojure spec as its a wrapper around test.check.
lambdaisland/regal also has test.check generators that can be used for regular expressions defined with the regal (hiccup style) syntax.
"},{"location":"clojure-spec/projects/bank-account/generate-test-data/#generating-more-than-one-value-for-a-specification","title":"Generating more than one value for a specification","text":"clojure.spec.gen.alpha/sample
will generate 10 random values from the specification
(spec-gen/sample (spec/gen :practicalli.banking-specifications/customer-details))\n\n;; => (#:practicalli.banking-specifications{:first-name \"\", :last-name \"\", :email-address \"\", :residential-address #:practicalli.banking-specifications{:country \"\", :county \"\", :house-name-number \"\", :street-name \"\", :post-code \"\"}, :social-security-id \"2P902qTJCP6\"}\n\n#:practicalli.banking-specifications{:first-name \"\", :last-name \"z\", :email-address \"\", :residential-address #:practicalli.banking-specifications{:house-name-number 0, :street-name \"\", :post-code \"R\"}, :social-security-id \"3dDBA7pa98r\"}\n\n#:practicalli.banking-specifications{:first-name \"nQ\", :last-name \"w\", :email-address \"h6\", :residential-address #:practicalli.banking-specifications{:country \"\", :county \"7u\", :house-name-number \"\", :street-name \"87\", :post-code \"\"}, :social-security-id \"x57pf2H2i16\"}\n\n#:practicalli.banking-specifications{:first-name \"ac\", :last-name \"L0x\", :email-address \"S\", :residential-address #:practicalli.banking-specifications{:country \"Xd\", :county \"\", :house-name-number \"P\", :street-name \"\", :post-code \"\"}, :social-security-id \"j5iTA70j9FW\"}\n\n#:practicalli.banking-specifications{:first-name \"e\", :last-name \"ic\", :email-address \"15G\", :residential-address #:practicalli.banking-specifications{:house-name-number \"\", :street-name \"Nj\", :post-code \"f\"}, :social-security-id \"I83rx1wUj07\"}\n\n#:practicalli.banking-specifications{:first-name \"zPr\", :last-name \"r\", :email-address \"hsVz\", :residential-address #:practicalli.banking-specifications{:country \"W\", :house-name-number \"S\", :street-name \"64\", :post-code \"85s25\"}, :social-security-id \"8EEDiy28SX7\"}\n\n#:practicalli.banking-specifications{:first-name \"QzoV\", :last-name \"\", :email-address \"iS\", :residential-address #:practicalli.banking-specifications{:county \"OaMj9\", :house-name-number 1, :street-name \"pzc0ji\", :post-code \"tv1\"}, :social-security-id \"9z88KM5TLKK\"}\n\n#:practicalli.banking-specifications{:first-name \"w73AA\", :last-name \"\", :email-address \"\", :residential-address #:practicalli.banking-specifications{:county \"sUj\", :house-name-number 4, :street-name \"jw\", :post-code \"652Z\"}, :social-security-id \"rZMUTPK72N6\"}\n\n#:practicalli.banking-specifications{:first-name \"j09f\", :last-name \"EoU\", :email-address \"sA82q\", :residential-address #:practicalli.banking-specifications{:country \"28nyq3\", :county \"5PURE\", :house-name-number \"1NzKwe\", :street-name \"28Y\", :post-code \"t\"}, :social-security-id \"yNBdc7M29Io\"}\n\n#:practicalli.banking-specifications{:first-name \"Xa38iX8FP\", :last-name \"u4G\", :email-address \"Ne1w25nJ\", :residential-address #:practicalli.banking-specifications{:country \"H07\", :house-name-number -17, :street-name \"jWRhfrrz9\", :post-code \"sF9\"}, :social-security-id \"IX2w8Xx8u0n\"})\n
Generating multiple result is useful if a collection of customer details is required for testing purposes.
"},{"location":"clojure-spec/projects/bank-account/generate-test-data/#exercising-a-specification","title":"Exercising a specification","text":"clojure.spec.test.alpha/exercise
returns pairs of generated and conformed values for a spec. exercise by default produces 10 samples (like sample) but you can pass both functions a number indicating the number of samples to produce.
(spec/exercise (spec/cat :practicalli.banking-specifications/first-name :practicalli.banking-specifications/last-name))\n\n;; => ([(\"\") #:practicalli.banking-specifications{:first-name \"\"}]\n [(\"6\") #:practicalli.banking-specifications{:first-name \"6\"}]\n [(\"\") #:practicalli.banking-specifications{:first-name \"\"}]\n [(\"6\") #:practicalli.banking-specifications{:first-name \"6\"}]\n [(\"W\") #:practicalli.banking-specifications{:first-name \"W\"}]\n [(\"ljooD\") #:practicalli.banking-specifications{:first-name \"ljooD\"}]\n [(\"704d5x\") #:practicalli.banking-specifications{:first-name \"704d5x\"}]\n [(\"EZyBT\") #:practicalli.banking-specifications{:first-name \"EZyBT\"}]\n [(\"1e6\") #:practicalli.banking-specifications{:first-name \"1e6\"}]\n [(\"v\") #:practicalli.banking-specifications{:first-name \"v\"}])\n
clojure.spec.test.alpha/exercise-fn
provides the same service but for function specifications (fdef
).
"},{"location":"clojure-spec/projects/bank-account/rebel-readline/","title":"Using the project in rebel readline","text":"Start the rebel REPL
clojure -M:repl/rebel\n
Once rebel has started a prompt will be displayed.
First required the main namespace, containing the functions of the application. This loads the code in that namespace into the REPL.
(require 'practicalli.banking-on-clojure)\n
Now add the specifications for the project
(require 'practicalli.banking-on-clojure)\n
? When does the TAB completion start to work ?
Testing the specifications
First change into the specifications namespace so the fully qualified names of the specs are not required.
(in-ns 'practicalli.banking-specifications)\n
Generate sample data from the specifications
(spec-gen/sample (spec/gen ::account-id))\n
The function specifications and the instrument functions are loaded from the requires, so test by calling the instrumented functions, first with bad data and then with correct data.
(register-account-holder {})\n
Use the specifications to generate good data
(register-account-holder ::customer-details)\n
Run generative tests on functions to check the return and fn values
(spec-test/check `register-account-holder)\n
"},{"location":"clojure-spec/projects/bank-account/rebel-readline/#hinttodo-add-screenshots-using-rebel-readline-repl","title":"Hint::TODO: add screenshots using Rebel readline REPL","text":""},{"location":"clojure-spec/projects/bank-account/test-functions-against-spec/","title":"Test functions against spec","text":""},{"location":"clojure-spec/projects/bank-account/test-functions-against-spec/#generative-testing-with-check","title":"Generative testing with check
","text":"clojure.spec.test.alpha/check
generates 1000 values from the argument section of a function definition specification.
Pass the name of the function definition that has a specification to the check
function.
(spec-test/check ``register-account-holder`)\n
"},{"location":"clojure-spec/projects/bank-account/test-functions-against-spec/#limiting-the-generated-data","title":"Limiting the generated data","text":"1000 tests can take a noticeable time to run, so check is not as often used during active development, as it would slow down the normal fast feedback cycle with Clojure.
check
takes an optional second argument which configures how the function operates. Passing a hash-map as a second argument will set the number of data values generated {:clojure.spec.test.check/opts {:num-tests 100}}
(spec-test/check\n `register-account-holder\n {:clojure.spec.test.check/opts {:num-tests 100}})\n
Configuring check
to run fewer tests provides a simple way to test multiple values without slowing down the development workflow.
"},{"location":"clojure-spec/projects/bank-account/test-functions-against-spec/#reporting-on-generative-testing","title":"Reporting on Generative testing","text":"clojure.spec.test.alpha/summarize-results
will return a brief summary including the total number of results and a count for how many results passed and failed.
(spec-test/summarize-results\n (spec-test/check `register-customer\n {:clojure.spec.test.check/opts {:num-tests 10}}))\n
Use the threading macro to summarize the results of multiple check operations
(->> (spec-test/check `register-account-holder)\n (spec-test/check `open-current-bank-account)\n (spec-test/summarize-results))\n
If this expression is bound to a name then it can be called when ever the full suite of check
generative testing is required.
"},{"location":"clojure-spec/projects/bank-account/unit-tests-with-spec/","title":"Unit tests with specs","text":"Now that customer data and account-holder data has a specification, we can use the clojure.spec.alpha/valid?
in the unity test code, as that function returns true or false.
In this example the result of a call to register-account-holder
is checked to see if it is valid against the ::account-holder
specification. This simplifies the code needed in unit test assertions, as Clojure spec is doing the work.
(deftest register-account-holder-test\n (testing \"Basic registration - happy path\"\n (is (= (set (keys (SUT/register-account-holder customer-mock)))\n (set (keys account-holder))))\n\n (is (spec/valid? :practicalli.bank-account-spec/account-holder\n (SUT/register-account-holder customer-mock) ) )\n ))\n
"},{"location":"clojure-spec/projects/bank-account/validate-customer-details-specification/","title":"Testing data Specifications","text":"The specifications defined so far can be tested with specific data, using the conform
or valid?
functions.
Generating sample data from the specifications also provides useful feedback on how well the specifications are defined.
"},{"location":"clojure-spec/projects/bank-account/validate-customer-details-specification/#generating-data-from-specifications","title":"Generating data from specifications","text":"Test data specifications by generating sample data from those specifications.
Evaluating these functions several times is a quick way to identifies specifications that may require custom generators. If individual specifications do not generate consistent data, then incorrect results may occur during composite data specifications or function specifications.
(spec-gen/sample (spec/gen ::first-name))\n(spec-gen/sample (spec/gen ::last-name))\n(spec-gen/sample (spec/gen ::email-address))\n(spec-gen/sample (spec/gen ::house-name-number))\n(spec-gen/sample (spec/gen ::street-name))\n(spec-gen/sample (spec/gen ::post-code))\n(spec-gen/sample (spec/gen ::county))\n(spec-gen/sample (spec/gen ::country))\n(spec-gen/sample (spec/gen ::residential-address))\n(spec-gen/sample (spec/gen ::social-security-id-uk))\n(spec-gen/sample (spec/gen ::social-security-id-usa))\n(spec-gen/sample (spec/gen ::social-security-id))\n(spec-gen/sample (spec/gen ::customer-details))\n(spec-gen/sample (spec/gen ::account-holder))\n
"},{"location":"clojure-spec/projects/bank-account/validate-customer-details-specification/#validating-the-customer-details-specifications","title":"Validating the customer details specifications","text":"The specifications can be checked using the conform or valid? functions with example data.
Check an example hash-map from our test conforms to the specification
(spec/conform ::customer-details\n {:first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"})\n;; => :clojure.spec.alpha/invalid\n
The mock test data does not confirm to the specification, even though it has all the same keys as the map in the specification
(spec/valid? ::customer-details\n {:first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"})\n;; => false\n
spec/explain
will provide more information to help diagnose the issue
(spec/explain ::customer-details\n {:first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"})\n\n;; {:first-name \"Jenny\", :last-name \"Jetpack\", :email-address \"jenny@jetpack.org\", :residential-address \"42 meaning of life street\", :postal-code \"AB3 0EF\", :social-security-id \"123456789\"}\n;; - failed: (contains? % :practicalli.bank-account-design-journal/first-name) spec: :practicalli.bank-account-design-journal/customer-details\n;; {:first-name \"Jenny\", :last-name \"Jetpack\", :email-address \"jenny@jetpack.org\", :residential-address \"42 meaning of life street\", :postal-code \"AB3 0EF\", :social-security-id \"123456789\"}\n;; - failed: (contains? % :practicalli.bank-account-design-journal/last-name) spec: :practicalli.bank-account-design-journal/customer-details\n;; {:first-name \"Jenny\", :last-name \"Jetpack\", :email-address \"jenny@jetpack.org\", :residential-address \"42 meaning of life street\", :postal-code \"AB3 0EF\", :social-security-id \"123456789\"}\n;; - failed: (contains? % :practicalli.bank-account-design-journal/email-address) spec: :practicalli.bank-account-design-journal/customer-details\n;; {:first-name \"Jenny\", :last-name \"Jetpack\", :email-address \"jenny@jetpack.org\", :residential-address \"42 meaning of life street\", :postal-code \"AB3 0EF\", :social-security-id \"123456789\"}\n;; - failed: (contains? % :practicalli.bank-account-design-journal/residential-address) spec: :practicalli.bank-account-design-journal/customer-details\n;; {:first-name \"Jenny\", :last-name \"Jetpack\", :email-address \"jenny@jetpack.org\", :residential-address \"42 meaning of life street\", :postal-code \"AB3 0EF\", :social-security-id \"123456789\"}\n;; - failed: (contains? % :practicalli.bank-account-design-journal/social-security-id) spec: :practicalli.bank-account-design-journal/customer-details\n
The ::customer-details
spec is given a map with unqualified keys and is failing the :req
part of the spec/keys
part of the specification
"},{"location":"clojure-spec/projects/bank-account/validate-customer-details-specification/#qualifying-keys-with-auto-resolve-macro","title":"Qualifying keys with auto-resolve macro","text":"The auto-resolve macro, #::
will add the current namespace to all the keys in a hash-map
Change the test data to use qualified keys by adding the
(spec/conform ::customer-details\n #::{:first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"} )\n;; => #:practicalli.bank-account-design-journal{:first-name \"Jenny\", :last-name \"Jetpack\", :email-address \"jenny@jetpack.org\", :residential-address \"42 meaning of life street\", :postal-code \"AB3 0EF\", :social-security-id \"123456789\"}\n
"},{"location":"clojure-spec/projects/bank-account/write-failing-tests/","title":"Write failing tests","text":"In Test Driven Development style, first write unit tests for the banking functions.
Edit the src/practicalli/banking_on_clojure_test.clj
and add deftest
tests
(deftest register-account-holder-test\n (testing \"Basic registration - happy path\"\n (is (= (set (keys (register-account-holder {})))\n (set (keys {:account-id \"123\" :customer-name \"Jenny Jetpack\"}))))))\n
"},{"location":"clojure-spec/projects/bank-account/write-failing-tests/#write-a-function-stub-to-run-the-tests","title":"Write a function stub to run the tests","text":"The tests cannot run unless they call the function to be tested. A common approach it to write a function that returns the argument.
(defn register-account-holder\n \"Register a new customer with the bank\n Arguments:\n - hash-map of customer-details\n Return:\n - hash-map of an account-holder (adds account id)\"\n\n [customer-details]\n\n customer-details)\n
"},{"location":"clojure-spec/projects/bank-account/write-failing-tests/#add-mock-data","title":"Add mock data","text":"Define some initial mock data to use with the unit tests
(def customer-mock\n {:first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street, Earth\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"})\n
account is a customer with a bank account id added\n\n(def account-holder-mock\n {:account-id #uuid \"97bda55b-6175-4c39-9e04-7c0205c709dc\"\n :first-name \"Jenny\"\n :last-name \"Jetpack\"\n :email-address \"jenny@jetpack.org\"\n :residential-address \"42 meaning of life street, Earth\"\n :postal-code \"AB3 0EF\"\n :social-security-id \"123456789\"})\n
Update the test to use the mock data.
(deftest register-account-holder-test\n (testing \"Basic registration - happy path\"\n (is (= (set (keys (register-account-holder customer-mock)))\n (set (keys account-holder-mock))))))\n
"},{"location":"clojure-spec/projects/card-game/","title":"Card game: spec and generative testing","text":"Define a data specification that represent a deck of playing cards, adding functional specifictations to check the values passed to the functions use to play a card game.
spec generators are used to return varied sample data from those specifications. Function definitions are instrumented and check for correct arguments when those functions are called.
"},{"location":"clojure-spec/projects/card-game/#create-a-new-project","title":"Create a new project","text":"Create a new Clojure project using :project/create
from Practicalli Clojure CLI Config or add an alias definition of your choosing to the Clojure CLI user configuration.
clojure -T:project/create :template app :name practicalli/card-game\n
Open the src/practicalli/card_game.clj
file and require the clojure.spec.alpha
namespace
(ns practicalli.card-game.clj\n (:require [clojure.spec.alpha :as spec]))\n
"},{"location":"clojure-spec/projects/card-game/#playing-card-specifications","title":"Playing card specifications","text":"A playing card has a face value and a suit. There are 4 suits in a card deck.
A specification for the possible suits can be defined using literal values
(spec/def ::suits #{:clubs :diamonds :hearts :spades})\n
Define a predicate function to check a value conforms to the spec using the pattern matching that is build-in to the Clojure set
data type.
(def suits? #{:clubs :diamonds :hearts :spades})\n
"},{"location":"clojure-spec/projects/card-game/#card-game-decks","title":"Card game decks","text":"Suits from different regions are called by different names. Each of these suits can be their own spec.
(spec/def ::suits-french #{:hearts :tiles :clovers :pikes})\n(spec/def ::suits-german #{:hearts :bells :acorns :leaves})\n(spec/def ::suits-spanish #{:cups :coins :clubs :swords})\n(spec/def ::suits-italian #{:cups :coins :clubs :swords})\n(spec/def ::suits-swiss-german #{:roses :bells :acorns :shields})\n
A composite specification called ::card-suits
provides a simple abstraction over all the variations of suits. Using ::card-suits
will be satisfied with any region specific suits.
(spec/def ::card-suits\n (spec/or :french ::suits-french\n :german ::suits-german\n :spanish ::suits-spanish\n :italian ::suits-italian\n :swiss-german ::suits-swiss-german\n :international ::suits-international))\n
"},{"location":"clojure-spec/projects/card-game/#define-an-alias","title":"Define an alias","text":"Jack queen king are called face cards in the USA and occasionally referred to as court cards in the UK.
Define a spec for ::face-cards
and then define :court-cards
and alias
(spec/def ::face-cards #{:jack :queen :king :ace})\n(spec/def ::court-cards ::face-cards)\n
Any value that conforms to the ::face-card
specification also conforms to the ::court-cards
specification.
(spec/conform ::court-cards :ace)\n
"},{"location":"clojure-spec/projects/card-game/#playing-card-rank","title":"Playing card rank","text":"Each suit in the deck has the same rank of cards explicitly defining a rank
(spec/def ::rank #{:ace 2 3 4 5 6 7 8 9 10 :jack :queen :king})\n
Rank can be defined more succinctly with the clojure.core/range
function. The expression (range 2 11)
will generates a sequence of integer numbers from 2 to 10 (the end number is exclusive, so 11 is not in the sequence).
Using clojure.core/into
this range of numbers can be added to the face card values.
(into #{:ace :jack :queen :king} (range 2 11))\n
The ::rank
specification now generates all the possible values for playing cards.
(spec/def ::rank (into #{:ace :jack :queen :king} (range 2 11)))\n
The specification only checks to see if a value is in the set, the order of the values in the set is irrelevant.
"},{"location":"clojure-spec/projects/card-game/#playing-card","title":"Playing Card","text":"A playing card is a combination of suit and face value, a pair of values, referred to as a tuple.
Clojure spec has a tuple
function, however, we need to define some predicates first
(spec/def ::playing-card (spec/tuple ::rank ::suits ))\n
Use the spec with values to see if they conform. Try you own values for a playing card.
(spec/conform ::playing-card [:ace :spades])\n
"},{"location":"clojure-spec/projects/card-game/#game-specs","title":"Game specs","text":"Define specifications for data used to represent players and the overall card game.
The player name is a very simple spec.
(spec/def ::name string?)\n
Score will keep a running total of a player score across games, again a simple integer value.
(spec/def ::score int?)\n
A player is represented by a hash-map that contains their name, score and the hand they are currently dealt. The hand is a collection of tuples representing a playing card.
(spec/def ::player\n (spec/keys\n :req [::name ::score ::dealt-hand]))\n
"},{"location":"clojure-spec/projects/card-game/#game-deck-specs","title":"Game deck specs","text":"A card game has a deck of 52 cards, one card for each combination of suit and rank.
The size of the card deck changes over the course of a game, so the deck can contain any number of cards. The deck must contain only cards to be valid.
(spec/def ::card-deck (spec/* ::playing-card))\n
At this stage in the design, a card game can have any number of players
(spec/def ::players (spec/* ::player))\n
A game is represented by a hash-map with a collection of players and a card deck
(spec/def ::game (spec/keys :req [::players ::card-deck]))\n
"},{"location":"clojure-spec/projects/card-game/#generative-data-from-specifications","title":"Generative data from Specifications","text":"Clojure spec can generate random data which conforms to a specification, highly useful in testing Clojure code with a wide variety of values.
clojure.spec.alpha/gen
returns a generator for the given specification. clojure.spec.gen.alpha/generate
takes that generator and creates a random value that conforms to the specification. clojure.spec.gen.alpha/sample
will generate a collection of random values that each conform to the specification.
Require the clojure spec namespaces to make use of their functions.
(ns practicalli.card-game.clj\n (:require [clojure.spec.alpha :as spec]\n [clojure.spec.gen.alpha :as spec-gen]\n [clojure.spec.test.alpha :as spec-test]))\n\n(spec/def ::suits #{:clubs :diamonds :hearts :spades})\n(spec/def ::rank #{:ace 2 3 4 5 6 7 8 9 10 :jack :queen :king})\n
To generated data based on a specification, first get a generator for a given spec,
(spec/gen ::suits)\n
generate
will return a value using the specific generator for the specification.
(spec-gen/generate (spec/gen ::suits))\n
sample
will generate a number of values from the given specification
(spec-gen/sample (spec/gen ::rank))\n
"},{"location":"clojure-spec/projects/card-game/#card-game-data","title":"Card Game data","text":"Generate a random value for the ::player
specification
(spec-gen/generate (spec/gen ::player))\n
Example Expected output from generate
```clojure
```
Generate a random value for the ::game
specification
(spec-gen/generate (spec/gen ::game))\n
Generate a collection of random values that each conform to the specification.
(spec-gen/sample (spec/gen ::game))\n
"},{"location":"clojure-spec/projects/card-game/#practicallispec-generative-testing","title":":practicalli.spec-generative-testing","text":"{:name \"Yp34KE63vAL1eriKN4cBt\", :score 225, :dealt-hand ([9 :hearts] [4 :clubs] [8 :hearts] [10 :clubs] [:queen :spades] [3 :clubs] [6 :hearts] [8 :hearts] [7 :diamonds] [:king :spades] [:ace :diamonds] [2 :hearts] [4 :spades] [2 :clubs] [6 :clubs] [8 :diamonds] [6 :spades] [5 :spades] [:queen :clubs] [:queen :hearts] [6 :spades])}
"},{"location":"clojure-spec/projects/card-game/#function-specifications","title":"Function Specifications","text":"A function specification can contain a specification for the arguments, the return values and the relationship between the two.
The specifications for the function may be composed from previously defined data specifications.
(ns practicalli.card-game\n (:require [clojure.spec.alpha :as spec]\n [clojure.spec.gen.alpha :as spec-gen]\n [clojure.spec.test.alpha :as spec-test]))\n\n(spec/def ::suit #{:clubs :diamonds :hearts :spades})\n(spec/def ::rank (into #{:jack :queen :king :ace} (range 2 11)))\n(spec/def ::playing-card (spec/tuple ::rank ::suit))\n(spec/def ::dealt-hand (spec/* ::playing-card))\n\n(spec/def ::name string?)\n(spec/def ::score int?)\n(spec/def ::player (spec/keys :req [::name ::score ::dealt-hand]))\n(spec/def ::card-deck (spec/* ::playing-card))\n(spec/def ::players (spec/* ::player))\n(spec/def ::game (spec/keys :req [::players ::card-deck]))\n
"},{"location":"clojure-spec/projects/card-game/#function-definition","title":"Function definition","text":"The card game application has three functions to start with.
(defn regulation-card-deck\n \"Generate a complete deck of playing cards\"\n [{:keys [::deck ::players] :as game}]\n (apply + (count deck)\n (map #(-> % ::delt-hand count) players)))\n
At the start of function design, the algorithm may still be undefined. Using the specifications and generators mock data can be returned as a placeholder.
(defn deal-cards\n \"Deal cards to each of the players\n Returns updated game hash-map\"\n [game]\n (spec-gen/generate (spec/gen ::game)))\n
(defn winning-player\n \"Calculate winning hand by comparing each players hand\n Return winning player\"\n [players]\n (spec-gen/generate (spec/gen ::player)))\n
Example The expected form of a player won game:
#:practicalli.player-won\n {:name \"Jenny Nada\",\n :score 225,\n :dealt-hand [[9 :hearts] [4 :clubs] [8 :hearts] [10 :clubs] [:queen :spades]]}\n
"},{"location":"clojure-spec/projects/card-game/#spec-definitions","title":"Spec definitions","text":"Define a function specification for the deal-cards
function
- argument must be of type
::game
- return type is
::game
- function applies arguments to a game and returns the game
(spec/fdef deal-cards\n :args (spec/cat :game ::game)\n :ret ::game\n :fn #(= (regulation-card-deck (-> % :args :game))\n (regulation-card-deck (-> % :ret))))\n
Define a function specification for the winning-player
function
- argument must be of type
::players
- return type is
::players
(spec/fdef winning-player\n :args (spec/cat :players ::players)\n :ret ::player)\n
"},{"location":"clojure-spec/projects/card-game/#instrument-functions","title":"Instrument functions","text":"Instrumenting functions will wrap a function definition and check the arguments of any call to the instrumented function.
(spec-test/instrument `deal-cards)\n
Calling the deal-cards
function with an incorrect argument returns an error that describes where in the specification the error occurred.
(deal-cards \"fake game data\")\n
Error in an easier to read format
ERROR: #error\n {:message \"Call to #'practicalli.card-game/deal-cards did not conform to spec:\\n\\\n \"fake game data\\\" - failed:\n map? in: [0] at: [:args :game] spec: :practicalli.card-game/game\\n\",\n :data {:cljs.spec.alpha/problems\n [{:path [:args :game],\n :pred cljs.core/map?,\n :val \"fake game data\",\n :via [:practicalli.card-game/game :practicalli.card-game/game],\n :in [0]}],\n :cljs.spec.alpha/spec #object[cljs.spec.alpha.t_cljs$spec$alpha17968],\n :cljs.spec.alpha/value (\"fake game data\"),\n :cljs.spec.alpha/args (\"fake game data\"),\n :cljs.spec.alpha/failure :instrument}}\n
"},{"location":"clojure-spec/projects/card-game/#organizing-function-instrumentation","title":"Organizing function instrumentation","text":"Instrumenting functions creates a wrapper around the original function definition.
When you change the function definition and evaluate the new code, it replaces the instrumentation of the function. Therefore each time a function is redefined it should be instrumented.
There is no specific way to manage instrumenting a function, however, a common approach is to define a collection of functions to instrument, then use a helper function to instrument all the functions at once.
Bind a name to the collection of function specifications.
(def ^:private function-specifications\n [`card-game/deal-cards\n `card-game/winning-player])\n
Define a simple helper function to instrument all the functions in the collection.
(defn instrument-all-functions\n []\n (spec-test/instrument function-specifications))\n
Refactoring the code may involve a number of changes benefit from instrumentation being switched off until its complete. The unstrument
function will remove instrumentation from all the functions in the collection.
(defn unstrument-all-functions\n []\n (spec-test/unstrument function-specifications))\n
Koacha Test Runner can include functional specifications
Koacha test runner can manage the testing of function specifications and is especially useful for managing unit level testing with specifications.
"},{"location":"clojure-spec/testing/","title":"Testing with Specifications","text":""},{"location":"clojure-spec/testing/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"clojure-spec/testing/#during-development","title":"During development","text":"Create specifications for data and functions
Selectively instrument function definitions to check function call arguments against the function specification.
- clojure.spec.test.alpha/instrument - check fdef :args
"},{"location":"clojure-spec/testing/#unit-and-integration-testing","title":"Unit and integration testing","text":"Add specification checks along with unit testing and integration testing to provide a very wide range of data values to be tested (with a minimal amount of code).
- clojure.spec.test.alpha/check - use :args to generate tests to check fdef :ret and :fn
run a suite of spec-generative tests on an entire ns with check
. Just one namespace per check
expression?
control the number of values check creates for each check expression. As the default is 1000 the checks can take a noticeable time to run (see practicalli/spec-generative-testing)
Many built-in generators for clojure.core
data predicates
composite specifications can build generators upon predicate generators.
Pass generator-returning functions to spec, supplying generators for things spec does not know about. Pass an override map to gen
in order to supply alternative generators for one or more sub-paths of a spec.
Define your own generators
"},{"location":"clojure-spec/testing/#at-run-time","title":"At run time","text":"Use specifications for run time checking, typically using conform
and valid?
functions.
Specification are typically the minimal checks required for the system, compared to more extensive checks during test and system integration.
Create lightweight private specifications for tests that run in the production environment.
"},{"location":"clojure-spec/testing/checking%20arguments/","title":"Checking arguments in function calls with specifications","text":""},{"location":"clojure-spec/testing/checking%20arguments/#instrument-functions-during-development","title":"Instrument functions during development","text":"Instrumenting a function enables the checking of arguments in a function call against the specification defined in an fdef
definition of the same name.
(clojure.spec.test.alpha/instrument `function-name)\n
Instrumenting a function swaps the function definition var with a wrapped version of the function definition which includes tests the :args
spec from the fdef
expression.
unstrument
returns the function definition to the original form and tests for that function are no longer run.
"},{"location":"clojure-spec/testing/checking%20arguments/#unit-specification-testing","title":"Unit (Specification) testing","text":"You can generate data for interactive testing with gen/sample.
"},{"location":"coding-challenges/","title":"Coding Challenges for Clojure","text":"Coding challenges are an excellent way to start learning a new language. The challenges allow you to focus on the language and not be concerned about the more general engineering aspects of software development.
Challenges are there to explore a language and practice your understanding of how to assemble working code. It is recommended to try different approaches to solving a challenges and even repeat the same challenges at a later date and see what additional approaches you have learned.
Exercism.io and 4Ever-Clojure are highly recommended starting point for learning Clojure and does not require any installation or setup. 4Ever-Clojure is a new implementation of 4Clojure.com.
"},{"location":"coding-challenges/#practicalli-challenges","title":"Practicalli Challenges","text":"Challenge that can be solved in a few hours (or less). Use a Clojure REPL connected editor to help solve these challenges.
- Simple Projects
- TDD Code Kata
"},{"location":"coding-challenges/#approach-to-solving-challenges","title":"Approach to solving challenges","text":"Take a few minutes to digest the description of the challenge and make notes.
Identify the simplest possible thing to do, solving the problem in many small pieces which encourages experimentation
- experiment, write code and evaluate to see what it does (optionally create a comment with the result)
- use a rich comment
(comment ,,,)
to capture multiple ideas and designs, even failed experiments can be useful (separates experiments from working code) - continually evaluate code as expressions are written, to ensure their behaviour is understood (try different argument values for functions)
- try different shapes of data
- transform data shapes to keep function definitions simpler
Once there is a working solution, refactor or try different approaches and evaluate the merit of alternative solutions
"},{"location":"coding-challenges/#challenge-websites","title":"Challenge websites","text":"A local Clojure development environment supports solving challenge websites, e.g Clojure CLI and a Clojure REPl connected editor
Challenge website Description Requirements 4Ever-Clojure Learning the core functions of the Clojure language Web Browser Exercism Coding exercises with mentor support Web Browser & Exercim CLI ClojureScript Koans Interactive exercises in a web browser Web Browser Advent of Code Yearly coding challenge with a seasonal theme Clojure aware editor CodeWars Mostly math-based coding challenges with Clojure variants Web Browser"},{"location":"coding-challenges/advent-of-code/","title":"Advent Of Code","text":""},{"location":"coding-challenges/advent-of-code/#advent-of-code","title":"Advent Of Code","text":"Advent of Code is the annual coding challenge with a festive theme. Each day there is a new challenge in two parts, the first fairly easy the second a little more involved. The challenges are an investment of your time to complete them all, although even trying just a few is enough to help you think in different ways.
Every programming language requires regular practice to maintain your skills. A full time developer role gives lots of opportunities to practice every day, however, its often focused in around solving problems within a specific business domain, with little time to explore others. The Advent of Code puts you in a different domain, so its great for extending your coding experiences.
Solving challenges in a different language is another great way to extend your experiences, so here are some tips and examples for solving the advent of code in Clojure.
"},{"location":"coding-challenges/advent-of-code/#solving-challenges","title":"Solving challenges","text":" - Keep the solution as simple as possible. Its very easy to over-complicate the solution and end up simply confusing yourself.
- Don't try and make the perfect solution. Write something that works, this will give you a nice ego boost. Then you can experiment with the code and see if you can improve your approach.
- Break down the problem into the simplest thing you can solve first. Trying to solve a problem all at once will quickly have you going around in circles.
- Keep all the code and make notes. I use a a design journal in my projects to document my thinking process, capture decisions that worked and those that didn't work for this project. The journal is a great way to cement learning from solving the challenge.
- Challenges are only accessible from their day of the month onwards. There is a count-down clock displayed on the next challenge to open, so you know when it will be available. Don't feel pressured to keep up with the challenges though, enjoy the experience and have fun, you will learn more that way.
"},{"location":"coding-challenges/advent-of-code/#coding-video","title":"Coding video","text":"A video guide to solving the first challenge of Advent of Code from 2018, trying out different solutions at increasing levels of abstraction. With each level of abstraction it helps to think in a more functional way.
"},{"location":"coding-challenges/advent-of-code/#creating-a-project-for-the-challenge","title":"Creating a project for the challenge","text":"clojure -T:project/create :template lib practicalli.advent-of-clojure-code/2019\n
Create a new Clojure file for each of the daily challenges. It makes sense to keep both parts of each day in the same file.
Practicalli Advent Of Code solutions repository
practicalli/advent-of-clojure-code-2019
"},{"location":"coding-challenges/advent-of-code/#useful-resources-and-examples","title":"Useful Resources And Examples","text":"Videos and code solutions to many challenges from 2019 and past years.
- fdlk/advent-2019 - example Clojure solutions to the advent of code
- Awesome Advent Of Code - a collection of solutions in various languages
- Advent of Code 2018 video walk-through of Clojure solutions by Tim Pote and GitHub repository
#adventofcode channel in the Clojurians slack channel discusses challenges and solutions, especially during December when the challenge takes place.
"},{"location":"coding-challenges/koans/","title":"ClojureScript Koans","text":"Koans are a collection of small challenges that slowly increase in complexity. They are similar to the 4Clojure challenges in scope.
"},{"location":"coding-challenges/koans/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"coding-challenges/4clojure/","title":"Coding Challenges: 4Clojure","text":"4Ever-Clojure Challenges Website
4Ever-Clojure is a simple website with 150 challenges to help discover the functions built-in to the Clojure language, the Clojure API.
The website is self-contained with nothing to install, simply paste in the missing code and run the tests. One piece of code should solve all the tests for that challenge.
The Problem List shows the challenges categorized by experience level required, (Elementary, Easy, Medium, Hard) to solve them. Start with the easiest problem or work your way through the challenges in any order you wish. The Status column tracks your progress thorugh the challenges.
Select the name of a challenge to see the description and one or more code tests that must pass.
Enter the code that should be inserted where the __
double underscore characters are.
Press the Run button to see if the code satisfies the tests
A dialog box is displayed showing how many tests have passed and failed
Start learning the Clojure API
There are over 600 functions in the clojure.core
namespace alone, with additional functions in many other namespaces that make up the https://clojure.github.io/clojure/. It is not required to learn all these functions to be productive in Clojure.
4ever-clojure replaces 4Clojure 4Ever-Clojure is a new implementation of 4Clojure.com which has now been decommissioned
"},{"location":"coding-challenges/4clojure/#help-completing-the-challenges","title":"Help completing the challenges","text":"Look at the Clojure Cheatsheet and Clojure API for an understanding of what functions are available in the core of the Clojure language.
Search directly in ClojureDocs for functions. Each function has a page that describes the function, shows the arguments it takes and provides many examples of its use. At the end of the page are related functions too.
Practicalli Code walk-through and solution journal
practicalli/four-clojure code journals for the first 60 challenges contains a design journal showing how each challenge was solved and additional refactor or alternative approaches to the solution.
Practicalli 4Clojure guides playlist provides video walk-through of the first 64 challenges, again with alternative solutions where relevant.
An Internet search of clojure topic
, where topic
is a name of the thing you want to do, should return many examples of functions that could be useful to solving the challenge. Or
Help from the community
Clojure community - getting help covers several sources of help from the Clojure community.
"},{"location":"coding-challenges/4clojure/#using-let-and-anonymous-functions","title":"Using let and anonymous functions","text":"The solution submitted should be a single form, which is inserted in the test code where the __
underscore placeholder is. It is therefore not possible to define data with def
or a separate function with defn
to support the submitted solution.
Use the anonymous function, (fn [])
, to define behaviour.
(fn [value1 value2]\n (* value1 value2))\n
Use let to bind a name to a value, so that value can be re-used throughout the expression. let
is also useful for breaking the algorithm into smaller pieces, making it easier to solve the challenge.
(let [name value]\n (* 2 value (/ value 4) (+ value 3)))\n
It is common to combine fn
and let
to solve the challenges as they grow in complexity
(fn fibonacci [length-of-series]\n (let [fib [1 1]]\n (if (< (count fib) length-of-series)\n \"iterate... to implement\"\n fib)))\n
- fn - ClojureDocs
- let - ClojureDocs
- Fibonacci sequence guide - practicalli
"},{"location":"coding-challenges/4clojure/#my-function-is-not-working","title":"My function is not working","text":"4Ever Clojure uses babashka/sci project to evaluate code on a JavaScript host. Whist this should cover 99.9% of the Clojure API there may be some code that works in a Clojure (JVM) REPL that is not supported.
Try the code in a Clojure REPL or create a Clojure project using the latest version of Clojure (1.11.x).
"},{"location":"coding-challenges/4clojure/#references","title":"References","text":" - 4Ever-Clojure
- Clojure Cheatsheet - Clojure.org
- Clojure API - Clojure.org
- practicalli/four-clojure code journals for the first 60 challenges
- 4Clojure video guides by Practicalli
- Clojure Core Library - ClojureDocs
- Clojure, The Essential Reference - Renzo Bogatti - Manning book published in 2020
"},{"location":"coding-challenges/codewars/","title":"CodeWars","text":"Coding challenges in various languages with ranking scoreboard, experience levels and voting on solutions. Many of the challenges tend toward mathematics, so may require some background research before solving them.
"},{"location":"coding-challenges/codewars/#requirements","title":"Requirements","text":"Codewars is a web browser based system in which you can write code and run tests. Sample unit tests are provided with each challenge, so its all self-contained.
Create a free account and select the language you wish to attempt challenges in. Two simple coding tests will need to be completed in order to access that specific language.
"},{"location":"coding-challenges/codewars/#challenges-dashboard","title":"Challenges Dashboard","text":"After logging in, the dashboard suggests a challenge for you at a suitable level. 8 kyu is the easiest level, the smaller the number the harder the challenge.
"},{"location":"coding-challenges/codewars/#tackling-a-challenge","title":"Tackling a challenge","text":"Read the instructions and take a look at the sample tests.
Many of the challenges have only a very basic explanation, so always review the sample unit tests to help with the understanding. The sample tests are not necessarily the full suite of tests run when testing your solution, so there may be undocumented edge cases to solve
The source and test code can be copied into a new project, as has been done with the practicalli/codewars-guides solutions
clojure -M:new lib practicalli/readability-is-king\n
Update the solution window with your solution and use the TEST button to run the sample unit tests.
The ATTEMPT button will run all the unit tests for the challenge, which may be more than the sample tests. If the attempt passes all the tests then the solution can be submitted an other solutions reviewed.
"},{"location":"coding-challenges/codewars/#tracking-progress","title":"Tracking progress","text":"View your profile page to track your progress and revisit kata challenges already completed.
"},{"location":"coding-challenges/codewars/#references","title":"References","text":"practicalli/codewars-guide - a repository of code solutions to CodeWars challenges, each challenge is its own Clojure CLI (deps.edn) project.
YouTube: CodeWars video guides Unofficial Free Code Camp Clojure Challenges
"},{"location":"coding-challenges/exercism/","title":"Exercism Challenges","text":" Exercism Clojure Track
Exercism is a learning platform for multiple programming languates (currently 67) which combines carefully crafted coding challenges and mentors who review and advise on solutions.
Solve challenges via the built-in Exercism editor.
Or download each exercise locally using the Exercism CLI, providing a Clojure CLI configured project with a test runner.
Use the Exercism CLI to submit a solution for metor feedback.
Exercism embdedded Clojure editor The Exercisim Clojure editor is powered by babashka/sci
"},{"location":"coding-challenges/exercism/#clojure-track","title":"Clojure Track","text":"All the challenges are groups into specific language tracks, including the Clojure track
Join the language track to be presented with available challenges and progress through that specific track.
"},{"location":"coding-challenges/exercism/#working-locally","title":"Working Locally","text":" Exercism Guide to working locally
Follow the Practicalli Clojure CLI Install steps (Exercism includes a similar Clojure CLI install guide)
The Exercism CLI can download a Clojure project containing the code for a specific challeng and submit the code back to exercism to confirm if the tests have passed and complete the challenge (or get feedback from a mentor).
Each challenge shows the download and submit commands Each Exercise page shows the command to download the code for that specific exercise, which is of the form
exercism download --exercise=exercise-name --track=clojure\n
Open the project source code downloaded from Exercism in a preferred Clojure editor and write a solution to solve the exercise.
clojure -X:test
command in the root of the downloaded project will run the tests supplied by the exercise
Practicalli Test Runner aliases clojure -X:test/run
runs the Kaocha test runner from the Practicalli Clojure CLI Config
clojure -X:test/watch
will automatically re-run tests when file changes are detected.
Clojure test runner covers test runner options in more detail.
Once the tests pass and you are happy with the solution, submit it to the Exercism website
exercism submit /path/to/src-file\n
"},{"location":"coding-challenges/exercism/#repl-workflow","title":"REPL Workflow","text":"Use a REPL workflow to get instant feedback on code written to make the unit test assersions pass.
Terminal UIEditor connected REPL Start a REPL via a Terminal UI in the root of the Exercism project
clojure -M:repl/rebel\n
Open the project in a Clojure aware editor and connect to the REPL process.
Open the project in a Clojure aware editor and start a Clojure REPL, e.g. jack-in
Use a rich comment
to experiment with clojure expressions that help move towards a solution, typically solving one unit test at a time. This separates experimental code from finished designs.
(comment \n ;; experiment with clojure code \n ;; evaluate expressions in the Clojure editor\n ;; and see the evalaution results inline\n)\n
Disable Linter rules
Disable Linter rules within the comment
expression that are not useful for REPL experiments.
It is common to have several implmentations of a function with the same name, so :redefined-var
is disabled.
Functions defined in the REPL experments are not ment to be used publicly (until they are copied/moved out of the comment form), so :clojure-lsp/unused-public-var
lint rule is disabled
#_{:clj-kondo/ignore [:redefined-var :clojure-lsp/unused-public-var]}\n(comment\n ,,,\n)\n
"},{"location":"coding-challenges/exercism/#support","title":"Support","text":"Mentors on the Exercism website will provide a review of your submissions and you can switch between mentor and practice modes as you prefer.
practicalli/exercism-clojure-guides contains a design journal of solutions to several Clojure exercises.
Ask for advice in the #exercism or #beginners channels of the Clojurians Slack community.
"},{"location":"coding-challenges/exercism/nucleotide-count/","title":"Nucleotide Count","text":" Clojure Track: Nucleotide Count
Given a string representing a DNA sequence, count how many of each nucleotide is present.
If the string contains characters other than A, C, G, or T then an error should be throw.
Represent a DNA sequence as an ordered collection of nucleotides, e.g. a string of characters such as \"ATTACG\".
\"GATTACA\" -> 'A': 3, 'C': 1, 'G': 1, 'T': 2\n\"INVALID\" -> error\n
DNA Nucleotide names
A
is Adenine, C
is Cytosine, G
is Guanine and T
is Thymine
Code for this solution on GitHub
practicalli/exercism-clojure-guides contains the design journal and solution to this exercise and many others.
"},{"location":"coding-challenges/exercism/nucleotide-count/#create-the-project","title":"Create the project","text":"Download the Nucleotide Count exercise using the exercism CLI tool
exercism download --exercise=nucleotide-count --track=clojure\n
Use the REPL workflow to explore solutions locally
Open the project in a Clojure aware editor and start a REPL, using a rich comment form to experiment with code to solve the challenge.
"},{"location":"coding-challenges/exercism/nucleotide-count/#starting-point","title":"Starting point","text":"Unit test code calls functions from the src
tree which must exist with the correct argument signature for the unit test code to compile successfully.
Reviewing each assertion in the unit test code identifies the function definitions required.
Exercism Unit Tests (ns nucleotide-count-test\n (:require [clojure.test :refer [deftest is]]\n nucleotide-count))\n\n(deftest empty-dna-strand-has-no-adenosine\n (is (= 0 (nucleotide-count/count-of-nucleotide-in-strand \\A, \"\"))))\n\n(deftest empty-dna-strand-has-no-nucleotides\n (is (= {\\A 0, \\T 0, \\C 0, \\G 0}\n (nucleotide-count/nucleotide-counts \"\"))))\n\n(deftest repetitive-cytidine-gets-counted\n (is (= 5 (nucleotide-count/count-of-nucleotide-in-strand \\C \"CCCCC\"))))\n\n(deftest repetitive-sequence-has-only-guanosine\n (is (= {\\A 0, \\T 0, \\C 0, \\G 8}\n (nucleotide-count/nucleotide-counts \"GGGGGGGG\"))))\n\n(deftest counts-only-thymidine\n (is (= 1 (nucleotide-count/count-of-nucleotide-in-strand \\T \"GGGGGTAACCCGG\"))))\n\n(deftest validates-nucleotides\n (is (thrown? Throwable (nucleotide-count/count-of-nucleotide-in-strand \\X \"GACT\"))))\n\n(deftest counts-all-nucleotides\n (let [s \"AGCTTTTCATTCTGACTGCAACGGGCAATATGTCTCTGTGTGGATTAAAAAAAGAGTGTCTGATAGCAGC\"]\n (is (= {\\A 20, \\T 21, \\G 17, \\C 12}\n (nucleotide-count/nucleotide-counts s)))))\n
Function definitions required to compile unit test code
src/nucleotide_count.clj(ns nucleotide-count)\n\n(defn count-of-nucleotide-in-strand\n \"Count how many of a given nucleotide is in a strand\"\n [nucleotide strand])\n\n(defn nucleotide-counts\n \"Count all nucleotide in a strand\"\n [strand])\n
"},{"location":"coding-challenges/exercism/nucleotide-count/#making-the-tests-pass","title":"Making the tests pass","text":"Select one assertion from the unit tests and write code to make the test pass.
Experiment with solutions in the comment
form and add the chosen approach to the respective function definition.
"},{"location":"coding-challenges/exercism/nucleotide-count/#counting-nucleotides","title":"Counting nucleotides","text":"Use test data from the unit test code, e.g. \"GGGGGTAACCCGG\"
How often does a nucleotide appear
Example
(map\n #(if (= % \\A) 1 0)\n \"GGGGGTAACCCGG\")\n
Add the result to get the total count
Example
(count\n (map\n #(if (= % \\A) 1 0)\n \"GGGGGTAACCCGG\"))\n
Is there a more elegant way?
When only the matching nucleotide is in the strand, then all the elements of the strand can be counted.
filter
the DNA strand with a predicate function (returns true/false) that returns only the matching nucleotide.
Example
(filter #(= % \\A) valid-nucleotides))\n
;; Count the elements in the returned sequence for the total
Example
(count\n (filter #(= % \\A) valid-nucleotides))\n
Add this code into the starting function
"},{"location":"coding-challenges/exercism/nucleotide-count/#run-unit-tests","title":"Run unit tests","text":"Run the unit tests to see if they pass. x should pass, x should fail.
"},{"location":"coding-challenges/exercism/nucleotide-count/#nucleotide-occurances","title":"Nucleotide occurances","text":"Count the occurances
\"GGGGGTAACCCGG\"
(count\n (filter (fn [nucleotide] (= nucleotide \\A))\n \"GGGGGTAACCCGG\"))\n
Define the data
(def valid-nucleotides\n \"Characters representing valid nucleotides\"\n [\\A \\C \\G \\T])\n
Exception handling required
(throw (Throwable.)) if nucleotide is \\X\n
Or use a predicate with some (some element? in the sequence)
(some #(= \\G %) valid-nucleotides)\n\n (some #{\\G} valid-nucleotides)\n
(defn count-of-nucleotide-in-strand\n [nucleotide strand]\n (if (some #(= nucleotide %) valid-nucleotides)\n (count\n (filter #(= nucleotide %)\n strand))\n (throw (Throwable.))))\n\n (count-of-nucleotide-in-strand \\T \"GGGGGTAACCCGG\")\n
Design the second function
How often does a nucleotide appear
(map\n #(if (= % \\A) 1 0)\n valid-nucleotides)\n
Add the result to get the total count
Is there a more elegant way?
(filter #(= % \\A) valid-nucleotides)\n
Count the elements in the returned sequence for the total
Design the second function
How often does a nucleotide appear
NOTE: zero must be returned when there are no appearences
Return value always in the form
{\\A 20, \\T 21, \\G 17, \\C 12}\n
"},{"location":"coding-challenges/exercism/nucleotide-count/#hammock-time","title":"Hammock time...","text":" - How often does something appear,
- how frequenct is it?
- Is there a clojure standard library for that (approx 700 functions), review https://clojure-docs.org/
(frequencies \"GGGGGAACCCGG\")\n
If there are missing nucleotides then there is no answer
What if there is a starting point
{\\A 0 \\C 0 \\G 0 \\T 0}\n
;; Then merge the result of frequencies
(merge {\\A 0 \\C 0 \\G 0 \\T 0}\n (frequencies \"GGGGGAACCCGG\"))\n
Update the function definition and run tests
"},{"location":"coding-challenges/exercism/nucleotide-count/#solutions","title":"Solutions","text":"There are many ways to solve a challenge and there is value trying different approaches to help learn more about the Clojure language.
The following solution includes filter
and frequencies
functions which are commonly used functions from the Clojure standard library.
Example Solution
src/nucleotide_count.clj(ns nucleotide-count)\n\n(def valid-nucleotides\n \"Characters representing valid nucleotides\"\n [\\A \\C \\G \\T])\n\n(defn count-of-nucleotide-in-strand\n [nucleotide strand]\n (if (some #(= nucleotide %) valid-nucleotides)\n (count\n (filter #(= nucleotide %)\n strand))\n (throw (Throwable.))))\n\n(defn nucleotide-counts\n \"Count all nucleotide in a strand\"\n [strand]\n (merge {\\A 0 \\C 0 \\G 0 \\T 0}\n (frequencies \"GGGGGAACCCGG\")))\n
"},{"location":"coding-challenges/exercism/rna-transcription/","title":"Exercise: RNA Transcription","text":" Clojure Track: Nucleotide Count
Given a DNA strand, return its RNA complement (per RNA transcription).
Both DNA and RNA strands are a sequence of nucleotides.
The four nucleotides found in DNA are adenine (A), cytosine (C), guanine (G) and thymine (T).
The four nucleotides found in RNA are adenine (A), cytosine (C), guanine (G) and uracil (U).
Given a DNA strand, its transcribed RNA strand is formed by replacing each nucleotide with its complement:
- G -> C
- C -> G
- T -> A
- A -> U
Code for this solution on GitHub
practicalli/exercism-clojure-guides contains the design journal and solution to this exercise and many others.
"},{"location":"coding-challenges/exercism/rna-transcription/#create-the-project","title":"Create the project","text":"Download the RNA transcription exercise using the exercism CLI tool
exercism download --exercise=rna-transcription --track=clojure\n
Use the REPL workflow to explore solutions locally
Open the project in a Clojure aware editor and start a REPL, using a rich comment form to experiment with code to solve the challenge.
"},{"location":"coding-challenges/exercism/rna-transcription/#designing-the-solution","title":"Designing the solution","text":"To convert a collection of values, define a hash-map where the keys are the initial DNA values and the hash-map values are the transformed RNA values. Using a hash-map in this way is often termed as a dictionary.
A string is used as a collection of character values by many of the functions in clojure.core
. The dictionary uses characters for its keys and values.
{\\G \\C \\C \\G \\T \\A \\A \\U}\n
Use the map
function to pass the dictionary over the dna string (collection of characters) to create the RNA transcription.
Use an anonymous function to wrap the dictionary and pass each a character (nucleotide) from the DNA string in turn.
(defn to-rna\n [dna]\n (map (fn [nucleotide] (get {\\G \\C \\C \\G \\T \\A \\A \\U} nucleotide))\n dna))\n
(to-rna \"GCTA\")\n
The result is returned as a sequence of characters.
Refactor the to-rna
function and add clojure.string/join
to return the RNA value as a string
(defn to-rna\n [dna]\n (clojure.string/join\n (map (fn [nucleotide] (get {\\G \\C \\C \\G \\T \\A \\A \\U} nucleotide))\n dna)))\n
Now the function returns a string rather than a collection of characters.
(to-rna \"GCTA\")\n
"},{"location":"coding-challenges/exercism/rna-transcription/#throwing-an-assertion-error-for-incorrect-nucleotide","title":"Throwing an assertion error for incorrect nucleotide","text":"In the Exercism test suite, one test checks for an AssertionError when an incorrect nucleotide is passed as part of the DNA string.
(deftest it-validates-dna-strands\n (is (thrown? AssertionError (rna-transcription/to-rna \"XCGFGGTDTTAA\"))))\n
The throw
function can be use to return any of the Java errors. An assertion error would be thrown using the following code
(throw (AssertionError. \"Unknown nucleotide\"))\n
Refactor the to-rna
function to throw an assertion error if a nucleotide if found that is not part of the dictionary.
An if
function could be used with a conditional to check if each nucleotide is one of the keys in the dictionary and throw an AssertionError if not found. This would mean consulting the dictionary twice, once for the conditional check and once for the conversion.
Is there a way to consult the dictionary once for each nucleotide?
The get
function can return a specific not-found value when a key is not found in a map.
What if the throw
function is used as the not-found value in the get
function?
(defn to-rna\n [dna]\n (clojure.string/join\n (map (fn [nucleotide ](get {\\G \\C \\C \\G \\T \\A \\A \\U} nucleotide\n (throw (AssertionError. \"Unknown nucleotide\")) ))\n dna)))\n
Unfortunately this approach will evaluate the throw expression regardless of if the nucleotide is found in the dictionary, so calling this version of the function always fails.
The or
function evaluate the first expression and if a true value is returned then any additional expressions are skipped over.
If the first expression returns false or a falsey value, i.e. nil
, then the next expression is evaluated.
Proposed Solution
(defn to-rna\n [dna]\n (clojure.string/join\n (map (fn [nucleotide](or (get {\\G \\C \\C \\G \\T \\A \\A \\U} nucleotide)\n (throw (AssertionError. \"Unknown nucleotide\"))))\n dna)))\n
Call the to-rna
function with a DNA string from the unit test code
(to-rna \"GCTA\")\n
The function should return \"CGAU\"
Call the to-rna
function with a DNA string that contains an invalid nucleotide.
(to-rna \"GCXA\")\n
An AssertionError
is thrown as the X
character does not exist in the dictionary hash-map, so the get
expression returns nil
.
"},{"location":"coding-challenges/exercism/rna-transcription/#refactor","title":"Refactor","text":"Now the function is solving unit tests, minor adjustments can be made to streamline the code.
"},{"location":"coding-challenges/exercism/rna-transcription/#hash-map-as-function","title":"Hash map as function","text":"A hash-map can be called as a function and takes a key as an argument. This acts the same as the get
function, returning the value associated to a matching key, otherwise returning nil
or the not-found value if specified.
(defn to-rna\n [dna]\n (clojure.string/join\n (map (fn [nucleotide] (or ({\\G \\C \\C \\G \\T \\A \\A \\U} nucleotide)\n (throw (AssertionError. \"Unknown nucleotide\"))))\n dna)))\n
"},{"location":"coding-challenges/exercism/rna-transcription/#anonymous-function","title":"Anonymous function","text":"The anonymous function, fn
, has a terse form.
#(* %1 %2)
is the same as (fn [value1 value2] (+ value1 value2))
This syntax sugar is often use with map
, reduce
, apply
functions as the behaviour tends to be compact and of single use.
If the function definition is more complex or used elsewhere in the namespace, then the defn
function should be used to define shared behavior.
Solution with anonymous function
(defn to-rna\n [dna]\n (clojure.string/join\n (map #(or ({\\G \\C \\C \\G \\T \\A \\A \\U} %)\n (throw (AssertionError. \"Unknown nucleotide\")))\n dna )))\n
"},{"location":"coding-challenges/exercism/rna-transcription/#named-dictionary-data","title":"Named dictionary data","text":"Replace the hard-coded hash-map by defining a name for the dictionary.
Define a name to represent the dictionary data
(def dictionary-dna-rna {\\G \\C \\C \\G \\T \\A \\A \\U})\n
Refactor the to-rna
function to use the dictionary by name.
Solution using named dictionary data
(defn to-rna\n [dna]\n (clojure.string/join\n (map #(or (dictionary-dna-rna %)\n (throw (AssertionError. \"Unknown nucleotide\")))\n dna)))\n
"},{"location":"coding-challenges/exercism/rna-transcription/#making-the-function-pure","title":"Making the function pure","text":"Its beyond the scope of the Exercism challenge, however, its recommended to use pure functions where possible.
A pure function only uses data from its arguments.
Adding a dictionary as an argument to the to-rna
function would be simple.
Pure function approach
(defn to-rna\n [dictionary dna]\n (clojure.string/join\n (map #(or (dictionary %)\n (throw (AssertionError. \"Unknown nucleotide\")))\n dna )))\n
With a dictionary as an argument the function is also more usable, as other dictionaries could be used with the function.
The function would now be called as follows
(to-rna dictionary-dna-rna \"GTGAC\")\n
"},{"location":"coding-challenges/exercism/space-age/","title":"Space Age","text":""},{"location":"coding-challenges/exercism/space-age/#topics-covered","title":"Topics covered","text":"Code for this solution on GitHub
practicalli/exercism-clojure-guides contains the design journal and solution to this exercise and many others.
"},{"location":"coding-challenges/exercism/space-age/#create-the-project","title":"Create the project","text":"Download the RNA transcription exercise using the exercism CLI tool
exercism download --exercise=rna-transcription --track=clojure\n
Use the REPL workflow to explore solutions locally
Open the project in a Clojure aware editor and start a REPL, using a rich comment form to experiment with code to solve the challenge.
"},{"location":"coding-challenges/exercism/space-age/#challenge-introduction","title":"Challenge introduction","text":"Given an age in seconds, calculate how old someone would be on:
- Earth: orbital period 365.25 Earth days, or 31557600 seconds
- Mercury: orbital period 0.2408467 Earth years
- Venus: orbital period 0.61519726 Earth years
- Mars: orbital period 1.8808158 Earth years
- Jupiter: orbital period 11.862615 Earth years
- Saturn: orbital period 29.447498 Earth years
- Uranus: orbital period 84.016846 Earth years
- Neptune: orbital period 164.79132 Earth years
So if you were told someone were 1,000,000,000 seconds old, you should be able to say that they're 31.69 Earth-years old.
"},{"location":"coding-challenges/exercism/bob/","title":"Index","text":""},{"location":"coding-challenges/exercism/bob/#bob","title":"Bob","text":"The Bob challenge involves writing a very basics example of a text parser, something that would be used for a text based adventure game.
Bob is described as a lackadaisical teenager, so responses are very limited. To create the Bob text parser we need to identify the rules that determine Bob's response.
The instructions provide some basic rules:
- Bob answers 'Sure.' if you ask him a question.
- He answers 'Whoa, chill out!' if you yell at him.
- He answers 'Calm down, I know what I'm doing!' if you yell a question at him.
- He says 'Fine. Be that way!' if you address him without actually saying anything.
- He answers 'Whatever.' to anything else.
It is important to also read through the supplied unit tests to elaborate on these rules.
"},{"location":"coding-challenges/exercism/bob/#create-the-project","title":"Create the project","text":"Download the Bob transcription exercise using the exercism CLI tool
exercism download --exercise=bob --track=clojure\n
To use the Clojure CLI tool instead of Leiningen, create a deps.edn
file containing an empty hash-map, {}
and clone Practicalli Clojure CLI Config to ~/.clojure/
.
"},{"location":"coding-challenges/exercism/bob/#rules-derived-from-the-unit-tests","title":"Rules derived from the Unit tests","text":"Reviewing all the examples from the unit tests, there are 5 rules for the Bob parser
These rules were discovered by searching through the unit test code for each reply that Bob should return, showing the tests for each reply.
Each rule also had to ensure it did not create any false positives by being true for any other reply that Bob could make, especially the whatever reply.
Name Rule description question The phrase has a ? as the last alphanumeric character, not including whitespace shouting The phrase has uppercase alphabetic characters, but no lower case alphabetic characters shouting question A combination of question and shouting silence The phrase is empty or contains characters that are not alphanumeric whatever Any phrase that does not match any of the other rules"},{"location":"coding-challenges/exercism/bob/#design-approach","title":"Design approach","text":"There are two main approaches to solving this challenge. The first is to use the clojure.string
functions to check or transform the phrase given to Bob. The second approach is to use regular expressions with functions such as re-seq
, re-find
and re-matches
.
Start by defining the rules as an expression that returns either true or false, using some of the example strings from the unit tests.
Use a let
expression to bind a name to each rule, e.g. shouting?
, question?
, silence?
. Then these names can be used in a simple cond
expression to return the appropriate phrase. Regardless of if using clojure.string
or regular expressions, the cond
code should be similar
Once you have tried this challenge for yourself, take a look at the design journal for the clojure.string approach and the regular expression approach.
Bob - clojure.string approach Bob - regular expression approach
"},{"location":"coding-challenges/exercism/bob/bob-regular-expression-approach/","title":"Bob solution - regex","text":"Solution to Bob challenge using regular expressions and the re-matches
function.
Using re-matchers
, if the string matches the pattern, then the string is returned. Otherwise nil
is returned
The regular expressions cheatsheet from Mozilla Developer Network was very helpful in understanding regular expressions
"},{"location":"coding-challenges/exercism/bob/bob-regular-expression-approach/#asking-bob-a-question","title":"Asking Bob a question?","text":"The phrase passed to Bob is a question if the last alphanumeric character is a question mark. Using a simple regular expression we can check if the last character in the string a ?
#\"\\?\"
is a literal regular expression pattern that will match a single ?
character
So the regular expression pattern will match a single ? character
(re-matches #\"\\?\" \"?\")\n
With other characters present though the pattern doesn't match.
(re-matches #\"\\?\" \"Ready?\")\n
To match ?
with other characters,
.
matches any single character except line terminators (new line, carriage return)
(re-matches #\".\\?\" \"R?\")\n
.*
matches any number of single characters one or more times,
(re-matches #\".*\\?\" \"?Ready\")\n
\\s
matches a single whitespace character and \\s*
matches multiple whitespace characters
(re-matches #\".*\\?$\" \"Okay if like my spacebar quite a bit?\")\n ;; => \"Okay if like my spacebar quite a bit?\"\n
$
is a boundary assertion so the pattern only matches the ? at the end of a string and not in the middle. However, this is not required as the re-matches
uses groups and that manages the boundary assertion.
re-matches
does not require the $
as there is an implicit boundary
(re-matches #\".*\\?\" \"Okay if like my ? spacebar quite a bit\")\n
Match if there is a single space or space type character after the ?
(re-matches #\".*\\?\\s\" \"Okay if like my spacebar quite a bit? \")\n ;; => \"Okay if like my spacebar quite a bit? \"\n
Match if there are multiple space type characters after the ?
(re-matches #\".*\\?\\s*\" \"Okay if like my spacebar quite a bit? \")\n ;; => \"Okay if like my spacebar quite a bit? \"\n
Don't match if a question mark character is not at the end of the string
(re-matches #\".*\\?\" \"Okay if like my ? spacebar quite a bit\")\n
"},{"location":"coding-challenges/exercism/bob/bob-regular-expression-approach/#shouting-a-question-at-bob","title":"Shouting a question at Bob","text":"[^a-z]
matches if there are no lower case alphabetic characters. The ^
at the start of the pattern negated the pattern.
*
any number of the proceeding pattern
[A-Z]+
any number of upper case alphabetic characters
When a phrase has all uppercase characters then we have a match
(re-matches #\"[^a-z]*[A-Z]+[^a-z]*\" \"HELLO\")\n
If there are lower case characters, even if there are uppercase characters, the pattern does not match.
(re-matches #\"[^a-z]*[A-Z]+[^a-z]*\" \"Hello\")\n
If the characters are all uppercase then the pattern matches, even if there are other non-alphabetic characters
(re-matches #\"[^a-z]*[A-Z]+[^a-z]*\" \"ABC 1 2 3\")\n
"},{"location":"coding-challenges/exercism/bob/bob-regular-expression-approach/#silence-of-the-bob","title":"Silence of the Bob","text":"\\s
matches any single whitespace character, including space, tab, form feed, line feed, and other Unicode spaces.
(re-matches #\"\\s*\" \" \\t\\t\\t\")\n
"},{"location":"coding-challenges/exercism/bob/bob-regular-expression-approach/#solution-using-regular-expressions","title":"Solution using regular expressions","text":"The re-matches
expressions with regular expressions patterns can be put into a let expression. The names are bound to the re-matches expressions which evaluated to either true
or false
The names from the let are used with a cond
function as conditions, returning the relevant reply from Bob.
For the shouting question, the and
is used to check if two names are both true.
(defn response-for\n [phrase]\n (let [;; A ? at the end of the phrase, not counting whitespace\n question (re-matches #\".*\\?\\s*\" phrase)\n\n ;; No lower case characters, at least one upper case character\n yelling (re-matches #\"[^a-z]*[A-Z]+[^a-z]*\" phrase)\n\n ;; The entire string is whitespace\n silence (re-matches #\"\\s*\" phrase)]\n\n (cond\n silence \"Fine. Be that way!\"\n (and question yelling) \"Calm down, I know what I'm doing!\"\n question \"Sure.\"\n yelling \"Whoa, chill out!\"\n :whatever \"Whatever.\")))\n
"},{"location":"coding-challenges/exercism/bob/bob-string-approach/","title":"Bob string approach","text":"Solution to Bob challenge using clojure.string
functions and Character class from Java.
"},{"location":"coding-challenges/exercism/bob/bob-string-approach/#asking-bob-a-question","title":"Asking Bob a question?","text":"The phrase passed to Bob is a question if the last alphanumeric character is a question mark.
Using a simple comparison we can check if the last character in the string a ?
(= \\? (last \"this is a question?\"))\n
However if there is whitespace after the question mark then the last
character is a whitespace and so the expression returns false
(= \\? (last \"this is still a question? \"))\n
clojure.string/trimr
will remove all the trailing whitespace from the right side of a string. Once trimmed, then our initial comparison code will work again.
(= \\? (last (clojure.string/trimr \"this is still a question? \")))\n
"},{"location":"coding-challenges/exercism/bob/bob-string-approach/#shouting-at-bob","title":"Shouting at Bob","text":"Unfortunately the clojure.string API does not have a function to check if a string is in capital letters. There is an upper-case
function, so a comparison can be made with the original string and the string returned from clojure.string/upper-case
.
Convert the string to uppercase
(clojure.string/upper-case \"watch out!\")\n
compare the uppercase version of the string with the original, if they are equal, then the original string must have been in upper case
(= \"WATCH OUT!\"\n (clojure.string/upper-case \"WATCH OUT!\"))\n
(= \"watch out!\"\n (clojure.string/upper-case \"watch out!\"))\n
There is a flaw in this approach thought, as it will give false positives for strings that should return the 'Whatever' response
(= \"1, 2, 3\"\n (clojure.string/upper-case \"1, 2, 3\"))\n
Refined rule to check that the phrase contains alphabetic characters, otherwise it is not shouting.
The java.lang.Character class has a method called isLetter that determines if a character is a letter.
The Classes and methods in java.lang
are always available within a Clojure project, without the need for specifically importing the library.
Character/isLetter
can be called as a function in Clojure, passing in a character type.
(Character/isLetter \\a)\n
To support all Unicode characters there is an isLetter method that takes an integer type. As there could be any kind of characters in the phrase, we will use the int version. This required conversing the character to an int first before calling Character/isLetter
(Character/isLetter (int \\a))\n
the some
function is used to iterate over all the characters in the phrase. As soon as a letter is found it returns true, so does not need to process the whole phrase unless no letter is found.
(some #(Character/isLetter (int %)) phrase)\n
"},{"location":"coding-challenges/exercism/bob/bob-string-approach/#silence-of-the-bob","title":"Silence of the Bob","text":"clojure.string/blank?
is a predicate function that returns true if a string is empty or contains only whitespace. It also returns true for a nil
value.
"},{"location":"coding-challenges/exercism/bob/bob-string-approach/#final-solution","title":"Final solution","text":"Each of the rules is bound to a name that represents either a true or false value returned from each expression.
The cond
expression then evaluates the local names to see if they are true or false. The first true value found returns the string associated with the name.
For the shouting question, the and
is used to check if two names are both true.
(defn response-for [phrase]\n (let [phrase (string/trimr phrase)\n silence? (string/blank? phrase)\n question? (= \\? (last phrase))\n letters? (some #(Character/isLetter (int %)) phrase)\n shouting? (and (= phrase (string/upper-case phrase))\n letters?)]\n (cond\n (and shouting? question?) \"Calm down, I know what I'm doing!\"\n silence? \"Fine. Be that way!\"\n shouting? \"Whoa, chill out!\"\n question? \"Sure.\"\n :else \"Whatever.\")))\n
The first let binding, phrase
over-rides the name of the argument to the function. This is not that common an approach as over-riding can lead to confusion. However, in this relatively simple example it feels okay to do. The over-ride is the first let binding and it is preparing the string for all the other let bindings to use.
Over-riding names of functions from the Clojure standard library is not recommended as this does lead to much confusion.
"},{"location":"coding-challenges/palindrome/","title":"Project Palindrome","text":"In this section we will create a simple Clojure project using Leiningen and build up a palindrome checker step by step.
We will start with the simplest possible thing we can create and steadily add
"},{"location":"coding-challenges/palindrome/#what-is-a-palindrome","title":"What is a Palindrome","text":"For this project it is assumed that a palindrome is a string of characters from the English alphabet and not any other language or an alphanumeric sequence.
It is assumed that a palindrome is at least 3 characters long, meaning a single character cannot be a palindrome. If a single character was a palindrome, then any valid sequence would contain at least as many palindromes as characters in that sequence.
"},{"location":"coding-challenges/palindrome/#challenge","title":"Challenge","text":"Write an algorithm to find the 3 longest unique palindromes in a string. For the 3 longest palindromes, report the palindrome, start index and length in descending order of length. Any tests should be included with the submission.
For example, the output for string, \"sqrrqabccbatudefggfedvwhijkllkjihxymnnmzpop\"
should be:
Text: hijkllkjih, Index: 23, Length: 10\nText: defggfed, Index: 13, Length: 8\nText: abccba, Index: 5 Length: 6\n
- Check for a palindrome
- Generate a series of palindromes
"},{"location":"coding-challenges/palindrome/simple-palindrome-test/","title":"Simple palindrome test","text":""},{"location":"continuous-integration/","title":"Continuous Integration","text":"Topics to be covered in this section include:
- Continuous Integration services
- Circle CI
- GitHub Workflow
- GitLab CI
- Configure deployment pipelines
- Manage environment variables
- Security & Secrets
- Deployment
- Amazon AWS
- Render.com
CircleCI example in Practicalli Clojure Web Services
Banking on Clojure is an example of Continuous Integration using CircleCI, with LambdaIsland/Kaocha as the test runner and Heroku as the deployment pipeline.
"},{"location":"continuous-integration/#12-factor-approach","title":"12 Factor approach","text":"Following the 12 factor principles, the deployment is driven by source code to multiple environments.
"},{"location":"continuous-integration/#circleci-service","title":"CircleCI service","text":"Use Yaml language to write CI workflows and tasks, using Docker images as a consistent run-time environment
A commercial service with a generous free Cloud plan - (6,000 minutes), providing highly optomises container images to run tasks efficiently. The CircleCI Clojure images contain Clojure CLI, Leiningen and Babashka pre-installed.
CircleCI Orbs package up common configuration and tools, greatly simplifying the configuration and maintenance required.
CircleCI Clojure language guide
"},{"location":"continuous-integration/#github-workflow","title":"GitHub Workflow","text":"Use Yaml language to write CI workflows and tasks.
A commercial service with a modest free plan (2,000 minutes) for open source projects. GitHub Marketplace contains a wide range of Actions, including Clojure related actions, simplifying the configuration of CI.
Setup Clojure provides Clojure CLI, Leinigen and boot tools for use within the CI workflow
GitHub Actions overview
"},{"location":"continuous-integration/circle-ci/","title":"Circle CI continuous integration service","text":"Circle CI is a service to build, test and deploy projects. CircleCI uses docker images to run its workflow, either in the cloud or locally.
Projects can be build, tests run, artifacts (uberjars) created and applications deployed to services such as AWS, Render.com, etc.
Integration will be supported by Git version control, a continuous integration service (CircleCI, GitLabs, GitHub Actions) and a deployment platform (Heroku).
"},{"location":"continuous-integration/circle-ci/#getting-started","title":"Getting Started","text":"Sign up using a GitHub or Bitbucket account and login to the CircleCI dashboard.
Add Project in the CircleCI dashboard to configure a shared Git repository and run build pipelines using a .circleci/config.yml
file in the root of the Clojure project.
Every time changes are pushed to the shared code repository (GitHub, Bitbucket), CirceCI will run the pipeline for the project and show the results.
"},{"location":"continuous-integration/circle-ci/#clojure-images","title":"Clojure images","text":"Clojure specific container images are available for several versions of Java and Clojure. Pre-configured images are typically faster than installing software on top a more generic image.
cimg/clojure:1.10
is the recommended image for Clojure projects. The image contains OpenJDK 17 and the latest version of Clojure CLI, Leiningen and Babashka
Add the following under the docker:
key in the config.yml
- image: cimg/clojure:1.10\n
The CircleCI Clojure Language guide walks through the sections of the yaml configuration in detail.
Check Clojure version clojure -Sdescribe
shows configuration information for the Clojure CLI tool as a hash-map, with the :version key associated with the exact install version.
lein version
shows the current version of Leiningen install on your development environment.
java -version
shows the current version of the Java installation.
"},{"location":"continuous-integration/circle-ci/#references","title":"References","text":"CircleCI Clojure Language guide CircleCI Clojure image tags - json CircleCI Clojure images CircleCI dockerfiles repository
"},{"location":"continuous-integration/circle-ci/circle-ci-sample-project/","title":"Circle CI example project","text":"The Circle CI language guide for Clojure provides an example project that is managed by the Leiningen build automation tool and based on the Luminus micro-framework template.
The project runs on the Undertow web server (wrapped by immutant), using ring to manage web requests and responses, with compojure for server-side routing. The application uses mount to manage the application lifecycle.
Fork the CircleCI-Public/circleci-demo-clojure-luminus project on your GitHub or GitLab account or organisation.
Go to the CircleCI dashboard and login. Select the GitHub / GitLab organisation you want to work with, this will list the repositories in that organisation.
Find the project in the list of repositories for that organisation
Click on the \"Set Up Project\" button and select the Clojure configuration from the drop-down menu.
This template seems to be older than the sample configuration on the Clojure language page. Copy the sample configuration and paste it into the editor. Then press Add Config to automatically add it to your code repository.
This will start a build and show the pipelines dashboard, with the project running the tasks defined in the configuration
Oh, it failed...
Clicking on the FAILED button shows details of that particular pipeline. This opens the build report for the pipeline.
The build report shows all the steps that have passed and the details of the step which has failed, in this case lein do test, uberjar
Edit the .circleci/config.yml
file in your fork and change the images used to openjdk-8-lein-2.9.3
.
Then commit the change to the code in the code repository. Another build will run automatically.
The dashboard shows the second build of this pipeline, showing the new commit just pushed
Success. Now development can continue knowing the configuration of the pipeline works.
"},{"location":"continuous-integration/circle-ci/circle-ci-sample-project/#hintfailing-on-java-11","title":"Hint::Failing on Java 11","text":"The example project only seems to run on Java 8. Running the project locally with either lein run
or lein test
"},{"location":"continuous-integration/circle-ci/circle-ci-sample-project/#hintcannot-edit-configuration-via-dashboard","title":"Hint::Cannot edit configuration via dashboard","text":"Apart from the initial creation of the configuration, its not possible to edit the configuration via the dashboard.
"},{"location":"continuous-integration/circle-ci/detailed-config/","title":"Circle CI detailed configuration","text":""},{"location":"continuous-integration/circle-ci/detailed-config/#orbs","title":"Orbs","text":"CircleCI Orbs are pre-packaged configurations for specific tasks, reducing the amount of configuration to maintain.
Orbs Description h-matsuo/github-release@0.1.3 GitHub release - package up releases ? Deploy to Heroku Kaocha test runner - unit and generative testing, junit-xml reports and test coverage"},{"location":"continuous-integration/circle-ci/detailed-config/#executors","title":"Executors","text":"Define an executor for the environment used to run the CircleCI jobs.
Executor environment Description machine Linux virtual machine docker Configuration for a Clojure CLI project
---\nversion: 2.1\n\norbs:\n github-release: h-matsuo/github-release@0.1.3\n\nexecutors:\n tools-deps-executor:\n docker:\n - image: circleci/clojure:openjdk-11-tools-deps-1.10.1.697\n working_directory: ~/repo\n environment:\n JVM_OPTS: -Xmx3200m\n\ncommands:\n setup:\n steps:\n - checkout\n - restore_cache:\n keys:\n - v1-dependencies-{{ checksum \"deps.edn\" }}\n - v1-dependencies-\n - save_cache:\n paths:\n - ~/.m2\n key: v1-dependencies-{{ checksum \"deps.edn\" }}\n\n acceptance-tests:\n steps:\n - run:\n name: check coverage\n command: clojure -M:test:coverage\n\n deploy-version:\n steps:\n - run:\n name: Update pom.xml\n command: clojure -Spom\n - run:\n name: Build\n command: clojure -M:jar\n - run:\n name: Deploy\n command: clojure -M:deploy\n\n store-artifact:\n steps:\n - run:\n name: Create jar\n command: clojure -M:jar\n - run:\n name: Zip up jar file\n command: zip --junk-paths github-api-lib github-api-lib.jar\n - run:\n name: install mvn\n command: |\n sudo apt-get update\n sudo apt-get -y install maven\n - run:\n name: extract version from pom\n command: |\n mvn help:evaluate -Dexpression=project.version -q -DforceStdout > current_version\n - persist_to_workspace:\n root: ~/repo\n paths:\n - github-api-lib.zip\n - current_version\n\n create-release:\n steps:\n - attach_workspace:\n at: ~/repo\n - github-release/create:\n tag: \"v$(cat ~/repo/current_version)\"\n title: \"Version v$(cat ~/repo/current_version)\"\n description: \"Github-related API calls.\"\n file-path: ~/repo/github-api-lib.zip\n\n\n\njobs:\n test:\n executor: tools-deps-executor\n steps:\n - setup\n - acceptance-tests\n deploy:\n executor: tools-deps-executor\n steps:\n - setup\n - acceptance-tests\n - deploy-version\n artifact:\n executor: tools-deps-executor\n steps:\n - setup\n - store-artifact\n release:\n executor: github-release/default\n steps:\n - setup\n - create-release\n\n\nworkflows:\n build-test-release:\n jobs:\n - test\n - deploy:\n context: clojars\n requires:\n - test\n filters:\n branches:\n only: main\n - artifact:\n requires:\n - test\n filters:\n branches:\n only: main\n - release:\n context: github\n requires:\n - test\n - deploy\n - artifact\n filters:\n branches:\n only: main\n
"},{"location":"continuous-integration/circle-ci/random-clojure-function/","title":"Random Clojure Function","text":"A Clojure command line application that shows a random function from the namespaces available in the Clojure Standard library, or a specific namespace from that library.
Random Clojure Function repository
This guide shows how to develop this project alongside CircleCI as the continuous integration service.
- Create a new project - using the Random Clojure Function guide
- Create a repository on GitHub
- Commit the project early and push changes to GitHub
- Add a .circleci/config.yml file and push to GitHub, choosing the relevant image
- Login to CircleCI dashboard and add project, choosing manual configuration
- Continue developing the random clojure function project, including tests
- After each push to GitHub, check the build status
- Add a CircleCI badge to the project readme
Video uses an older command to create projects
:project/create
alias from Practicalli Clojure CLI Config creates a new project
Arguments are key value pairs and can specify the :template
, project :name
and outpug directory :output-dir
.
clojure -T:project/new :template app :name practicalli/playground`\n
"},{"location":"continuous-integration/circle-ci/random-clojure-function/#create-a-new-project","title":"Create a new project","text":"Start following the guide to create the random clojure function project, using a deps.edn for the Clojure project configuration
clojure -T:project/new :template app :name practicalli/random-clojure-function\n
Version control the Clojure project using Git (or magit in Spacemacs)
"},{"location":"continuous-integration/circle-ci/random-clojure-function/#add-a-test-run-alias","title":"Add a test run alias","text":"Edit the deps.edn
file in the root of the project and add a :test/run
alias, to run the kaocha test runner which will stop if a failing test is detected. Stopping on a failed test saves running the full test suite and make the CI workflow more effective.
Project deps.edn:test/run\n{:extra-paths [\"test\"]\n :extra-deps {lambdaisland/kaocha {:mvn/version \"1.60.977\"}}\n :exec-fn kaocha.runner/exec-fn\n :exec-args {:randomize? false\n :fail-fast? true}}\n
"},{"location":"continuous-integration/circle-ci/random-clojure-function/#create-a-remote-repository","title":"Create a remote repository","text":"Add the remote repository URL to the local Git repository.
git remote add practicalli git@github.com:practicalli/random-clojure-function.git\n
"},{"location":"continuous-integration/circle-ci/random-clojure-function/#add-circleci-configuration","title":"Add CircleCI configuration","text":"Adding CircleCI early in the project development cycle ensures testing from the saved source code is successful and testing is consistently repeatable.
Create a new file called .circleci/config.yaml
in the root of the project.
Edit the file and add the following configuration.
Circe CI configuration for Clojure project
``yaml title=\".circleci/config.yaml\" version: 2.1 jobs: # basic units of work in a run build: # runs without Workflows must have a
buildjob as entry point working_directory: ~/random-clojure-function # directory where steps will run docker: # run the steps with Docker - image: cimg/clojure:1.10 # image is primary container where
stepsare run environment: # environment variables for primary container JVM_OPTS: -Xmx3200m # limit maximum JVM heap size to prevent out of memory errors steps: # commands that comprise the
build` job - checkout # check out source code to working directory - restore_cache: # restores cache if checksum unchanged from last run key: random-clojure-function-{{ checksum \"deps.edn\" }} - run: clojure -P - save_cache: # generate / update cache in the .m2 directory using a key template paths: - ~/.m2 - ~/.gitlibs key: random-clojure-function-{{ checksum \"deps.edn\" }} - run: clojure -X:test/run
```
run: clojure -P
step downloads dependencies for the project, including the :extra-deps
if aliases are also included.
run: clojure -X:test/run
adds the test directory to the class path and runs the Kaocha runner defined in the alias.
"},{"location":"continuous-integration/circle-ci/random-clojure-function/#connect-circle-ci-to-the-project","title":"Connect Circle CI to the project","text":"Commit and push the .circleci/config.yml
file to the GitHub repository.
Open the CircleCI dashboard and select Add Project. If your GitHub account has multiple organizations, choose the appropriate organization first.
Search the repository list for the GitHub repository and select ,,,
Select the Manual configuration as a .circleci/config.yml
file has already been added to the Git repository.
Press Start Building button to confirm that a config.yml
file has already been added and the build should start.
Now the first build runs with the config.yml
file.
Its failed. Okay lets investigate...
Thats okay, we have failing tests locally, so we know that the CircleCI build is working the same as on our local development environment.
The continuous integration is now working and tests are automatically run as soon as you push changes to the remote repository.
So the development of the project can continue with greater confidence
"},{"location":"continuous-integration/circle-ci/random-clojure-function/#adding-a-build-status-badge","title":"Adding a Build Status badge","text":"Generating a status badge documentation describes how to add a build status badge for your project, usually at the top of the README.md file in the project
[![CircleCI](https://circleci.com/gh/circleci/circleci-docs.svg?style=svg)](https://circleci.com/gh/practicalli/random-clojure-function)\n
Add this markdown to the top of the README.md file, underneath the title. Then commit and push the change to the GitHub repository.
NOTE: you might want to fix the unit tests first :)
"},{"location":"continuous-integration/circle-ci/status-monitor/","title":"Status Monitor Circle CI Continuous Integration","text":"practicalli/webapp-status-monitor is a Leiningen project create in October 2018.
The project uses ring and compojure as the basis of a web application.
Configured with a project.clj file.
(defproject status-monitor \"0.1.0-SNAPSHOT\"\n :description \"A server side website dashboard to collate monitoring information\"\n :url \"https://github.com/jr0cket/status-monitor\"\n :min-lein-version \"2.0.0\"\n :dependencies [[org.clojure/clojure \"1.9.0\"]\n [compojure \"1.6.1\"]\n [ring/ring-defaults \"0.3.2\"]\n [hiccup \"1.0.5\"]]\n :plugins [[lein-ring \"0.12.4\"]\n [lein-eftest \"0.5.3\"]]\n :ring {:handler status-monitor.handler/app}\n :profiles\n {:dev {:dependencies [[javax.servlet/servlet-api \"2.5\"]\n [ring/ring-mock \"0.3.2\"]]}})\n
"},{"location":"continuous-integration/circle-ci/status-monitor/#circleci-configuration","title":"CircleCI Configuration","text":"This configuration uses the CircleCI specific docker image with Java 17 and the latest version of Leiningen.
The configuration defines that the code will be check out, Leiningen will download the dependencies and then run unit tests.
version: 2\njobs:\n build:\n docker:\n - image: cimg/clojure:1.10\n\n working_directory: ~/repo\n\n environment:\n LEIN_ROOT: \"true\"\n # Customize the JVM maximum heap limit\n JVM_OPTS: -Xmx3200m\n\n steps:\n - checkout\n\n # Download and cache dependencies\n - restore_cache:\n keys:\n - v1-dependencies-{{ checksum \"project.clj\" }}\n # fallback to using the latest cache if no exact match is found\n - v1-dependencies-\n\n - run: lein deps\n\n - save_cache:\n paths:\n - ~/.m2\n key: v1-dependencies-{{ checksum \"project.clj\" }}\n\n # run tests!\n - run: lein test\n
"},{"location":"continuous-integration/circle-ci/status-monitor/#caching-dependencies","title":"Caching dependencies","text":"CircleCI create a cache of downloaded dependencies, to help speed up the running of the project.
The config.yml defines the path where the dependencies are saved. A unique key is used to identify the dependencies cache.
"},{"location":"continuous-integration/github-workflow/","title":"GitHub Workflows","text":"Automate tasks, such as running unit tests or lint code, whenever code is committed to a GitHub repository.
GitHub Actions can run one or more tasks after specific events, such as commits, raising issues or pull requests.
An event triggers a configured workflow which contains one or more jobs. A job contains a one or more steps which defines actions to run.
Practicalli GitHub Workflow Examples Practicalli recommended GitHub Actions
Introduction to GitHub Actions Understanding the workflow file
"},{"location":"continuous-integration/github-workflow/#anatomy-of-a-workflow","title":"Anatomy of a workflow","text":"Term Description Event Triggers a workflow, e.g. Create pull request, push commit, etc. Workflow Top level configuration containing one or more jobs, triggered by a specific event Job Set of steps executed in the same runner, multiple jobs execute in parallel within their own instance of a runner Step Individual task that runs commands (actions), sharing data with other steps Action Standalone commands defined within a step, custom commands or GitHub community Runner A GitHub Actions server, listening for available jobs"},{"location":"continuous-integration/github-workflow/#example-github-action","title":"Example GitHub Action","text":".github/workflows/workflow-name.yaml
is a file that contains the workflow definition.
Setup Java adds an OpenJDK distribution, i.e. Eclipse Temurin, at a specified version (Java 17 recommended).
Setup Clojure provides Clojure via Clojure CLI, Leiningen or Boot. Clojure CLI is recommended.
Cache is used to cache Clojure and Java libraries
- The example workflow runs on Ubuntu.
- The project code is checked out from the Git repository.
- Java and Clojure run-times are added to the environment
- Unit tests are run using the
:test/run
alias (this alias should run Kaocha or similar test runner) - Source code format and idioms are checked with cljstyle and clj-kondo
- The Clojure project is packaged into an Uberjar for deployment
Example GitHub workflow for Clojure CLI project
name: Test and Package project\non:\n pull_request:\n push:\n branches:\n - main\njobs:\n clojure:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Cache Clojure Dependencies\n uses: actions/cache@v3\n with:\n path:\n - ~/.m2\n - ~/.gitlibs\n key: cache-${{ hashFiles('**/deps.edn') }}\n restore-keys: clojure-deps-\n\n - name: Prepare java\n uses: actions/setup-java@v3\n with:\n distribution: 'temurin'\n java-version: '17'\n\n - name: Install clojure tools\n uses: DeLaGuardo/setup-clojure@9.5\n with:\n cli: 1.11.1.1165 # Clojure CLI based on tools.deps\n cljstyle: 0.15.0 # cljstyle\n clj-kondo: 2022.10.05 # Clj-kondo\n\n - name: Run Unit tests\n run: clojure -X:test/run\n\n - name: \"Lint with clj-kondo\"\n run: clj-kondo --lint deps.edn src resources test --config .clj-kondo/config-ci.edn\n\n - name: \"Check Clojure Style\"\n run: cljstyle check --report\n\n - name: Package Clojure project\n run: clojure -X:project/uberjar\n
"},{"location":"continuous-integration/github-workflow/#references","title":"References","text":" - Practicalli Blog - publish blog workflow - build publish a Cryogen project with Clojure CLI and publish the generated website with GitHub pages (also a Staging workflow that runs on pull requests)
- Practicalli Landing Page GitHub workflow - build a ClojureScript & Figwheel project with Clojure CLI and publish the generated site to GitHub pages
- Practicalli Clojure CLI config - lint with clj-kondo workflow - lint the
deps.edn
file with clj-kondo
"},{"location":"control-flow/cond/","title":"cond","text":""},{"location":"control-flow/if/","title":"if","text":""},{"location":"control-flow/when/","title":"when","text":""},{"location":"core.async/","title":"Introducing core.async","text":"The core.async
library provides a way to do concurrent programming using channels (eg. queues).
It minimises the need to use complex concurrent constructs and worry less about thread management.
core.async
is written in Clojure and can be used with both Clojure and ClojureScript.
- core.async getting started
- Introduction to asynchronous programming
- ClojureScript core.async and todos - Bruce Haurman
- core.async 101
- Mastering concurrent processes
- LispCast Clojure core.async: Channels - first lesson only.
- core.async introduction in ClojureScrpt unravelled
- core.async: Concurrency without Callbacks - Stuart Halloway
- David Nolan - core.async for asynchronous programming
- Timothy Baldridge - Core.Async
- core.async in Use - Timothy Baldridge
- Timothy Baldridge - core.async - pipelines - free video
- Timothy Baldridge - core.async - garbage collected go blocks - free video
"},{"location":"core.async/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"Pull requests are welcome
"},{"location":"core.async/#hint-coreasync-resources","title":"Hint:: core.async resources","text":""},{"location":"core.async/#hintcommunicating-sequential-processes","title":"Hint::Communicating Sequential Processes","text":"Communicating Sequential Processes (CSP) is a formalism for describing concurrent systems pioneered by C. A. R. Hoare in 1978. It is a concurrency model based on message passing and synchronization through channels
"},{"location":"core.async/#coreasync-on-clojurescript","title":"core.async on ClojureScript","text":"core.async is very widely used within ClojureScript applications and many libraries are built on top of it.
It\u2019s a good example of the syntactic abstractions that can be achieved by transforming code with ClojureScript macros.
JavaScript is single-threaded so you do not get the benefit of safe communication between threads, as there is only one.
"},{"location":"core.async/#concepts","title":"Concepts","text":""},{"location":"core.async/#channels","title":"Channels","text":"A channel is a queue with one or more publishers and one or more consumers. Producers put data onto the queue, consumers take data from the queue.
As data in Clojure is immutable, channels provide a safe way to communicate between threads.
"},{"location":"core.async/#chanel-size","title":"Chanel size","text":"Channels do not include a buffer by default, they use a producer (put!
) and consumer (take!
) to transfer a value through the channel. A maximum of 1024 put!
functions can be queued onto a single channel.
Specify a channel using (chan)
or a channel with a fixed buffer using (chan 12)
.
"},{"location":"core.async/#processes","title":"Processes","text":"Processes are independently running pieces of code that use channels for communication and coordination.
Calling put!
and take!
inside a process will stop that process until the operation completes.
Processes are launched using the go macro and puts and takes use the <! and >! placeholders. The go macro rewrites your code to use callbacks but inside go everything looks like synchronous code, which makes understanding it straightforward:
In ClojureScript, stopping a process doesn\u2019t block the single-threaded JavaScript environment, instead, the process will be resumed at a later time when it is no longer blocked.
"},{"location":"core.async/#important-functions","title":"Important functions","text":""},{"location":"core.async/#chan","title":"chan
","text":"The chan
function creates a new channel.
You can give a name to a channel using the def
function, eg. (def my-channel (chan))
A single channel can take a maximum of 1024 put requests. Once it has reached the maximum, then it is considered full.
A buffer of a fixed size can be specified when defining a new channel: (def channel-with-fixed-buffer (chan 12))
. This buffer increases the number of puts that can be sent to the channel. A dropping or sliding buffer can be used to discard messages added when the buffer is full.
A channel can also include a transducer, to manipulate the value on the channel eg. adding a timestamp (chan 32 (map (Date. now)))
eg. filtering messages (chan 12 (map even?))
Channels can be explicitly closed using (close channel-name)
or by adding a timeout that closes the channel after the specified number of milliseconds (timeout 6000)
.
"},{"location":"core.async/#put","title":"put!
","text":"The put!
function puts a value (message) on the channel.
You can put messages on the channel even if nothing is listening (no waiting take!
functions).
Evaluating put!
will always add a message on to the channel as long as the channel is open and not full.
"},{"location":"core.async/#take","title":"take!
","text":"The take!
function will take a single message from the queue.
If there are no messages on the queue when you evaluate take!
, then the function will wait to execute as soon as something is put on the channel
The take!
function needs an argument that is the channel and a function that will receive any message taken from a channel.
"},{"location":"core.async/#time","title":"time
","text":"This is a macro that executes an expression and prints the time it takes
Criterium is an excellent library for performance testing your code
"},{"location":"core.async/#go","title":"go
","text":"Referred to as a go block, the go
function creates a lightweight process, not bound to threads. Thousands of go blocks can be created efficiently and they can all have their own channel.
"},{"location":"core.async/#blocking-and-non-blocking","title":"blocking and non-blocking","text":"core.async offers two ways to write to and read from channels: blocking and non-blocking. A blocking write blocks the thread until the channel has space to be written to (the buffer size of a channel is configurable), a blocking read blocks a thread until a value becomes available on the queue to be read.
More interesting, and the only type supported in ClojureScript, are asynchronous channel reads and writes to channels, which are only allowed in \"go blocks\". Go blocks are written in a synchronous style, and internally converted to a state machine that executes them asynchronously.
Consider the following core.async-based code:
(let [ch (chan)]\n (go (while true\n (let [v (<! ch)]\n (println \"Read: \" v))))\n (go (>! ch \"hi\")\n (<! (timeout 5000))\n (>! ch \"there\")))\n
In this example, let introduces a new local variable ch, which is a new channel. Within the let's scope two go blocks are defined, the first is an eternal loop that reads (<!) a new value from channel ch into variable v. It then prints \"Read: \" followed by the read value to the standard out. The second go block writes (>!) two values to channel ch: \"hi\", it then waits 5 seconds and then writes \"there\" to the channel. Waiting for 5 seconds is implemented by reading from a timeout channel, which is a channel that closes itself (returns nil) after a set timeout. When running this code in the Clojure REPL (for instance), it will return instantly. It will then print \"Read: hi\", and 5 seconds later it will print \"Read: there\".
"},{"location":"core.async/#hint","title":"Hint::","text":"In JavaScript you cannot do blocking loops like this: the browser will freeze up for 5 seconds. The \"magic\" of core.async is that internally it converts the body of each go block into a state machine and turns the synchronous-looking channel reads and writes into asynchronous calls.
"},{"location":"core.async/bike-assembly-line/","title":"core.async scenario: Bike assembly line","text":"In this example we are going to use a bicycle assembly line as the process we want to make concurrent. The tasks involved in making our bicycle are:
- Making the frame
- Painting the wheels
- Making the rims
- Making the wheels (adding hub and spokes to wheels - different hub for front and rear wheels)
- Making the handlebars
- Fitting tyres to the rims (solid rims, so no tubes)
- Attaching the handlebars to the frame
- Attaching wheels to the frame
- Attaching crank to frame
- Attaching peddles to the crank
- Connecting control system wires (gears, breaks)
"},{"location":"core.async/bike-assembly-line/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"Pull requests are welcome
"},{"location":"core.async/bike-assembly-line/#current-build-process","title":"Current build process","text":"At the moment, each person creates one bicycle all by themselves. This means they need all sorts of different tools and are switching tasks all the way through assembling the bike.
We want to move to a more parallel approach, so as we automate this process we will evaluate what actions can be done in parallel and which must be done sequentially (i.e. painting the frame must come after making the frame).
"},{"location":"core.async/clacks-messages/","title":"Clacks Messenger with core.async","text":"In a previous exercise we built a simple encoder/decoder to send messages via the Clacks message service (as designed by Sir Terry Pratchett, RIP).
Now we will use core.async to create a processing queue between each Clack towers, so we can then model, monitor and visualise a Clacks messenger system with multiple Clacks towers. For additional fun we will enable new Clacks towers to come on line and connect to the existing system.
"},{"location":"core.async/clacks-messages/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"Pull requests are welcome
"},{"location":"core.async/toy-car-assembly-line/","title":"core.async example - toy car assembly line","text":""},{"location":"core.async/toy-car-assembly-line/#noteget-the-example-project-and-open-it-in-a-clojure-repl","title":"Note::Get the example project and open it in a Clojure REPL","text":"Clone or download the Lispcast: Clojure core-async Factory example
Open that project in your Clojure editor or run lein repl
in the top level directory of the project.
"},{"location":"core.async/toy-car-assembly-line/#the-toy-car-factory","title":"The toy car factory","text":"The toy car factory assembles car parts before distributing them. How can we make this process faster and more scalable?
"},{"location":"core.async/toy-car-assembly-line/#the-current-process","title":"The current process","text":"One worker picks out random parts of a car from the parts box until all the parts are collected to assemble the car.
"},{"location":"core.async/toy-car-assembly-line/#the-time-macro","title":"The time
macro","text":"We will use the time macro to see how long parts of our code takes to run and help find parts to optimise.
A simple example would be:
(time\n (map inc (range 0 10000)))\n
"},{"location":"core.async/toy-car-assembly-line/#timing-assembly-functions","title":"Timing assembly functions","text":"Investigate the time it takes to carry out the different assembly line tasks
(time (take-part))\n\n(time (attach-wheel :body :wheel))\n\n(time (box-up :body))\n\n(time (put-in-truck :body))\n
And to total time it takes to get a a whole car through the assembly line
(time (build-car))\n
The total time can be longer than the sum of the tasks, as the take-part
function does not always give the required part needed.
"},{"location":"core.async/toy-car-assembly-line/#hiring-more-workers","title":"Hiring more workers","text":"Adding 10 more workers is equivalent to adding 10 processes that run the assembly tasks.
Lets use a go block for a worker
(do\n (go\n (dotimes [number 10]\n (println \"First go block processing:\" number)\n (Thread/sleep 1200)))\n (go\n (dotimes [number 10]\n (println \"Second go block processing:\" number)\n (Thread/sleep 1200))))\n
These are two separate go blocks, so their is no co-ordination between the two. You can see the println statement from each go block intertwined.
"},{"location":"data-inspector/","title":"Clojure Data Browsers","text":"Clojure has a strong focus on using the built in data structures (list, vector, hash-map, set) to represent information in the system. Tools to inspect data and browse through nested or large data sets is invaluable in understanding what the system is doing.
There are many clojure.core
functions that can be used to explore data structures and transform them to produce specific views on data.
tap>
and datafy
are recent additions to Clojure that provide a more elegant way of exploring data than the classic println
statement.
New tools are being created to capture and visualize results from evaluated expressions (REBL, Reveal) as well as tools to specifically visualize tap>
expressions (Reveal, Portal).
"},{"location":"data-inspector/#common-approaches","title":"Common approaches","text":" - Editor data browsers - e.g. cider-inspect
- Portal - tool to navigate and visualise data via
tap>
- Clojure inspector - built-in Java Swing based inspector
- Reveal repl with data browser, also a
tap>
source (semi-commercial project)
"},{"location":"data-inspector/clojure-inspector/","title":"Clojure Inspector","text":"A visual browser for Clojure data using the Java UI libraries.
Require the clojure.inspector
namespace in the REPL or project namespace definitions to use the functions
ReplProject (require '[clojure.inspector :as inspector])\n
(ns practicalli.application\n (:require [clojure.inspector :as inspector]))\n
inspect
for flat data structures inspect-tree
for deeply nested / hierarchically data inspect-table
a sequence of data structures with the same shape
"},{"location":"data-inspector/clojure-inspector/#inspect","title":"inspect
","text":"View flat structures especially with non-trivial size data sets.
This example generated 10,000 random numbers. The Clojure inspector shows the values along with their index in the collection.
(inspector/inspect\n (repeatedly 10000 #(rand-int 101)))\n
"},{"location":"data-inspector/clojure-inspector/#inspect-tree","title":"inspect-tree
","text":"(inspect\n {:star-wars\n {:characters\n {:jedi [\"obiwan kenobi\" \"Yoda\" \"Master Wendoo\"]\n :sith [\"Palpatine\" \"Count Dukoo\"]}}})\n
"},{"location":"data-inspector/clojure-inspector/#inspect-table","title":"inspect-table
","text":"Inspect a sequence of data structures that share the same form, often found in data sets for machine learning and wider data science, eg. daily weather records.
This example generates mock data for a 20 day period for one or more locations. Each day contains the day, location and cumulative number of cases reported.
(defn mock-data-set\n \"Generates a set of mock data for each name\n Arguments: names as strings, names used in keys\n Returns: Sequence of maps, each representing confirmed cases\"\n [& locations]\n (for [location locations\n day (range 20)]\n {:day day\n :location location\n :cases (+ (Math/pow (* day (count location)) 0.8)\n (rand-int (count location)))}))\n\n(inspector/inspect-table\n (mock-data-set \"England\" \"Scotland\" \"Wales\" \"Northern Ireland\"))\n
"},{"location":"data-inspector/portal/","title":"Portal - navigate your data","text":"Portal inspector is a tool for exploration of Clojure data using a browser window to visualise and inspect Clojure, JSON, Transit, Logs, Yaml, etc.
Registered Portal as a tap source and wrap code with (tap> ,,,)
to see the results in Portal, providing a more advanced approach to debuging than println.
Send all evaluation results to Portal for a complete history using the portal-wrap nREPL middleware
Add a custom Mulog publisher to send all logs to Portal to help with debugging.
Open Portal from the REPL or configure Portal to open on REPL startup.
Practicalli Project Templates
Clojure projects created with Practicalli Project Templates include Portal configuration to recieve all evaluation results and Mulog event logs.
A custom dev/user.clj
file loads dev/portal.clj
and dev/mulog-events.clj
configurations on REPL startup, when the dev
directory is included on the path.
Use the :repl/reloaded
for a complete REPL reloaded workflow and tooling on REPL startup
Clojure 1.10 onward required tap sources and tap> tap is a shared, globally accessible system for distributing values (log, debug, function results) to registered tap sources.
add-tap
to register a source and receive all values sent. remove-tap
to remove a source.
tap>
form sends its contents to all registered taps. If no taps are registered, the values sent by tap> are discarded.
(deref (deref #'clojure.core/tapset))
will show the tap sources. tapset
is a Clojure set defined as private var and not meant to be accessed directly.
Online Portal demo
"},{"location":"data-inspector/portal/#add-portal","title":"Add Portal","text":"Clojure CLI user configuration aliases enable Portal to be used with any Clojure or ClojureScript project.
Practicalli Clojure CLI ConfigAlias Definition Practicalli Clojure CLI Config contains several aliases that support Portal, either to start a REPL process that can send all Clojure evaluated code to Portal or simply adding Portal as a library for manual use.
Run a REPL with portal and portal.nrepl/wrap-portal
to send every REPL evaluation to Portal over an nREPL connection
:repl/reloaded
- starts a rich terminal UI REPL with Portal nREPL middleware, including REPL Reloaded tools :repl/inspect
- starts a basic REPL with Portal nREPL middleware.
Or include the portal library in clojure
commands or when starting a REPL via an editor
dev/reloaded
- Portal, including REPL Reloaded tools inspect/portal-cli
- Clojure CLI (simplest approach) inspect/portal-web
- Web ClojureScript REPL inspect/portal-node
- node ClojureScript REPL
Create portal aliases to include the portal libraries for the Clojure, ClojureScript Web browser and ClojureScript Node server libraries
Portal aliases in Clojure CLI user configuration
:inspect/portal-cli\n{:extra-deps {djblue/portal {:mvn/version \"0.34.2\"}\n clj-commons/clj-yaml {:mvn/version \"0.7.0\"}}}\n\n:inspect/portal-web\n{:extra-deps {djblue/portal {:mvn/version \"0.34.2\"}\n org.clojure/clojurescript {:mvn/version \"1.10.844\"}}\n :main-opts [\"-m\" \"cljs.main\"]}\n\n:inspect/portal-node\n{:extra-deps {djblue/portal {:mvn/version \"0.34.2\"}\n org.clojure/clojurescript {:mvn/version \"1.10.844\"}}\n :main-opts [\"-m\" \"cljs.main\" \"-re\" \"node\"]}\n\n:repl/inspect\n{:extra-deps\n {cider/cider-nrepl {:mvn/version \"0.28.5\"}\n djblue/portal {:mvn/version \"0.33.0\"}\n clj-commons/clj-yaml {:mvn/version \"0.7.0\"}}\n :main-opts [\"-m\" \"nrepl.cmdline\"\n \"--middleware\"\n \"[cider.nrepl/cider-middleware,portal.nrepl/wrap-portal]\"]}\n
Practicalli Clojure CLI Config contains several aliases that support Portal.
YAML support for Portal - Clojure only clj-commons/clj-yaml adds YAML support to Portal for Clojure on the JVM
REPL Reloaded Aliases
REPL Reloaded section includes the :repl/reloaded
and :dev/reloaded
ailas definitions
"},{"location":"data-inspector/portal/#start-repl-with-portal","title":"Start REPL with Portal","text":"Run a REPL in a terminal and include the Portal library, using the Clojure CLI tools
REPL StarupEmacs Project configurationEmacs variable Start a REPL with namespace reloading, hotload libraries and portal data inspector
clojure -M:repl/reloaded\n
Or start the REPL with only portal
clojure -M:inspect/portal:repl/rebel\n
Add cider-clojure-cli-aliases
to a .dir-locals.el
in the root of the Clojure project with an alias used to add portal
.dir-locals.el
((clojure-mode . ((cider-preferred-build-tool . clojure-cli)\n (cider-clojure-cli-aliases . \":dev/reloaded\"))))\n
Or include an alias with only portal data inspector
.dir-locals.el
((clojure-mode . ((cider-preferred-build-tool . clojure-cli)\n (cider-clojure-cli-aliases . \":inspect/portal-cli\"))))\n
Set cider-clojure-cli-aliases
to the alias used to add portal, e.g. inspect/portal
Example
(setq cider-clojure-cli-aliases \":inspect/portal\")\n
Spacemacs: add to dotspacemacs/user-config
in the Spacemacs configuration file. Doom Emacs: add to config.el
Doom configuration file.
"},{"location":"data-inspector/portal/#open-portal","title":"Open Portal","text":"(require '[portal.api :as inspect])
once the REPL starts.
For inspector-portal-web
use (require '[portal.web :as inspect])
instead
(inspect/open)
to open the Portal inspector window in a browser (see portal themes)
(add-tap #'portal/submit)
to add portal as a tap target
"},{"location":"data-inspector/portal/#use-portal-from-repl","title":"Use Portal from REPL","text":"Portal functions can be called from the REPL prompt. When using Portal regularly, include code in a file, e.g. dev/user.clj
namespace to start a portal and add a tap source. Use a rich comment form, (comment ,,,)
to wrap the portal function calls if Portal should be launched manually by the developer.
user namespace and REPL commands
(ns user\n (:require [portal.api :as inspect]))\n\n(comment\n ;; Open a portal inspector window\n (inspect/open)\n ;; Add portal as a tap> target over nREPL connection\n (add-tap portal.api/submit)\n ;; Clear all values in the portal inspector window\n (inspect/clear)\n ;; Close the inspector\n (inspect/close)\n ) ;; End of rich comment block\n
"},{"location":"data-inspector/portal/#open-portal-on-repl-startup","title":"Open Portal on REPL startup","text":"Start the Portal inspector as soon as the REPL is started. This works for a terminal REPL as well as clojure aware editors.
Create a dev/user.clj
source code file which requires the portal.api library, opens the inspector window and adds portal as a tap source.
REPL ReloadedBasic Portal config When using namespace reloading tools (clojure tools.namespace.repl, Integrant, etc.) it is advisable to exclude dev
directory from the path to avoid launching multiple instances of Portal.
Example
(ns user\n (:require\n [portal.api :as inspect]\n [clojure.tools.namespace.repl :as namespace]))\n\n(println \"Set REPL refresh directories to \" (namespace/set-refresh-dirs \"src\" \"resources\"))\n
As a further precaution, check the Portal API sessions
value to ensure Portal is not already running, preventing Portal running multiple times
Example
(def portal-instance\n (or (first (inspect/sessions))\n (inspect/open {:portal.colors/theme :portal.colors/gruvbox})))\n
Example
(ns user\n (:require [portal.api :as inspect]))\n\n;; ---------------------------------------------------------\n;; Open Portal window in browser with dark theme\n(inspect/open {:portal.colors/theme :portal.colors/gruvbox})\n;; Add portal as a tap> target over nREPL connection\n(add-tap #'portal.api/submit)\n;; ---------------------------------------------------------\n(comment\n (inspect/clear) ; Clear all values in the portal inspector window\n (inspect/close) ; Close the inspector\n ) ; End of rich comment block\n
Start a REPL using the :repl/reloaded
or :dev/reloaded
alias from Practicalli Clojure CLI Config to include the dev
directory on the path and the portal library.
"},{"location":"data-inspector/portal/#basic-use","title":"Basic use","text":"The tap>
function sends data to Portal to be shown on the inspector window.
(tap> {:accounts\n [{:name \"jen\" :email \"jen@jen.com\"}\n {:name \"sara\" :email \"sara@sara.com\"}]})\n
Use portal to navigate and inspect the details of the data sent to it via tap>
.
(inspect/clear)
to clear all values from the portal inspector window.
(inspect/close)
to close the inspector window.
"},{"location":"data-inspector/portal/#editor-commands","title":"Editor Commands","text":"Control Portal from a Clojure Editor by wrapping the portal commands.
Emacs Add helper functions to the Emacs configuration and add key bindings to call them.
Emacs Configuration
;; def portal to the dev namespace to allow dereferencing via @dev/portal\n(defun portal.api/open ()\n (interactive)\n (cider-nrepl-sync-request:eval\n \"(do (ns dev)\n (def portal ((requiring-resolve 'portal.api/open)))\n (add-tap (requiring-resolve 'portal.api/submit)))\"))\n\n(defun portal.api/clear ()\n (interactive)\n (cider-nrepl-sync-request:eval \"(portal.api/clear)\"))\n\n(defun portal.api/close ()\n (interactive)\n (cider-nrepl-sync-request:eval \"(portal.api/close)\"))\n
- Spacemacs: add to
dotspacemacs/user-config
in the Spacemacs configuration file. - Doom emacs: add to
config.el
Doom configuration file.
Add key bindings to call the helper functions, ideally from the Clojure major mode menu.
SpacemacsDoom Emacs Add key bindings specifically for Clojure mode, available via the , d p
debug portal menu when a Clojure file (clj, edn, cljc, cljs) is open in the current buffer.
Spacemacs Key bindings for Portal
Add key bindings to Clojure major mode, e.g. , d p c to clear values from Portal Spacemacs Configuration - dotspacemacs/user-config
(spacemacs/declare-prefix-for-mode 'clojure-mode \"dp\" \"Portal\")\n(spacemacs/set-leader-keys-for-major-mode 'clojure-mode \"dpp\" 'portal.api/open)\n(spacemacs/set-leader-keys-for-major-mode 'clojure-mode \"dpc\" 'portal.api/clear)\n(spacemacs/set-leader-keys-for-major-mode 'clojure-mode \"dpD\" 'portal.api/close)\n
Or add user key bindings to user menu, SPC o
avoiding potential clash with Spacemacs Clojure layer key bindings. e.g. Space o p c to clear values from Portal Spacemacs Configuration - dotspacemacs/user-config
(spacemacs/declare-prefix \"op\" \"Clojure Portal\")\n(spacemacs/set-leader-keys \"opp\" 'portal.api/open)\n(spacemacs/set-leader-keys \"opc\" 'portal.api/clear)\n(spacemacs/set-leader-keys \"opD\" 'portal.api/close)\n
Use the map!
macro to add keys to the clojure-mode-map
, using :after
to ensure cider is loaded before binding the keys
Doom Configuration
(map! :map clojure-mode-map\n :n \"s-o\" #'portal.api/open\n :n \"C-l\" #'portal.api/clear)\n
Practicalli Doom Emacs configuration
Practicalli Doom Emacs config includes Portal key bindings in the Clojure major mode menu, under the debug menu. * , d p o
to open portal * , d p c
to clear results from portal
(map! :after cider\n :map clojure-mode-map\n :localleader\n :desc \"REPL session\" \"'\" #'sesman-start\n\n ;; Debug Clojure\n (:prefix (\"d\" . \"debug/inspect\")\n :desc \"debug\" \"d\" #'cider-debug-defun-at-point\n (:prefix (\"i\" . \"inspect\")\n :desc \"last expression\" \"e\" #'cider-inspect-last-sexp\n :desc \"expression\" \"f\" #'cider-inspect-defun-at-point\n :desc \"inspector\" \"i\" #'cider-inspect\n :desc \"last result\" \"l\" #'cider-inspect-last-result\n (:prefix (\"p\" . \"portal\")\n :desc \"Clear\" \"c\" #'portal.api/clear\n :desc \"Open\" \"D\" #'portal.api/close\n :desc \"Open\" \"p\" #'portal.api/open)\n :desc \"value\" \"v\" #'cider-inspect-expr))\n\n ; truncated...\n )\n
Practicalli Doom Emacs Config - +clojure.el
Portal Documentation - Editors
"},{"location":"data-inspector/portal/#editor-nrepl-middleware","title":"Editor nREPL middleware","text":"portal.nrepl/wrap-portal
sends every REPL evaluation to Portal over an nREPL connection, avoiding the need to wrap expressions with tap>
.
Practicalli Clojure CLI ConfigAlias Definition Start a REPL that includes the Portal nREPL middleware to send the result of every evaluation to portal.
:repl/reloaded
- rich terminal UI with portal and REPL Reloaded tools :repl/inspect
- basic terminal UI with portal
:repl/inspect
to start a terminal REPL with nREPL support for Clojure editor connection and portal libraries and middleware that will send all evaluations to portal once added as a tap source. !!!! EXAMPLE \"User deps.edn\"
:repl/inspect\n{:extra-deps\n {nrepl/nrepl {:mvn/version \"1.0.0\"}\n cider/cider-nrepl {:mvn/version \"0.30.0\"}\n djblue/portal {:mvn/version \"0.40.0\"}\n clj-commons/clj-yaml {:mvn/version \"0.7.0\"}}\n :main-opts [\"-m\" \"nrepl.cmdline\"\n \"--middleware\"\n \"[cider.nrepl/cider-middleware,portal.nrepl/wrap-portal]\"]}\n
Start a REPL with :repl/reloaded
or 'repl/inspect'
clojure -M:repl/reloaded\n
Start Portal User Interface and add portal as a tap target using the portal.api/submit
function to send all evaluated code to Portal
Clear results to keep history manageable
Use the Portal API clear
function to remove all existing results in Portal
"},{"location":"data-inspector/portal/#tap-logs-to-portal","title":"Tap Logs to Portal","text":"Using a custom mulog publisher, all event logs can be automatically sent to portal.
mulog tap publisher
;; ---------------------------------------------------------\n;; Mulog Custom Publishers\n;; - tap publisher for use with Portal and other tap sources\n;; ---------------------------------------------------------\n(ns mulog-publisher\n (:require\n ;; [com.brunobonacci.mulog :as mulog]\n [com.brunobonacci.mulog.buffer :as mulog-buffer]\n [portal.api :as p]))\n\n(deftype TapPublisher [buffer transform]\n com.brunobonacci.mulog.publisher.PPublisher\n (agent-buffer [_] buffer)\n (publish-delay [_] 200)\n (publish [_ buffer]\n (doseq [item (transform (map second (mulog-buffer/items buffer)))]\n (tap> item))\n (mulog-buffer/clear buffer)))\n\n(defn tap\n [{:keys [transform] :as _config}]\n (TapPublisher. (mulog-buffer/agent-buffer 10000) (or transform identity)))\n
Require the mulog-publisher
namespace and mulog library in the user
ns expression
Require mulog-publisher
namespace
(ns user\n \"Tools for REPL Driven Development\"\n (:require\n [com.brunobonacci.mulog :as mulog]\n [mulog-publisher]))\n
Start the publisher, optionally setting a global context for events first
Set values for all mulog events and start custom mulog publisher
;; ---------------------------------------------------------\n;; Mulog events and publishing\n\n;; set event global context - information added to every event for REPL workflow\n(mulog/set-global-context! {:app-name \"Practicalli Service\",\n :version \"0.1.0\", :env \"dev\"})\n\n(def mulog-tap-publisher\n \"Start mulog custom tap publisher to send all events to Portal\n and other tap sources\n `mulog-tap-publisher` to stop publisher\"\n (mulog/start-publisher!\n {:type :custom, :fqn-function \"mulog-publisher/tap\"}))\n;; ---------------------------------------------------------\n
Mulog events are now sent to portal when evaluated
(mulog/log ::repl-state ::ns (ns-publics *ns*))\n
Stop the mulog publisher by calling the reference it returns, i.e. mulog-tap-publisher
Function to stop mulog tap publisher
(defn mulog-tap-stop\n \"Stop mulog tap publisher to ensure multiple publishers are not started\n Recommended before using `(restart)` or evaluating the `user` namespace\"\n [] (mulog-tap-publisher))\n
"},{"location":"data-inspector/portal/#references","title":"References","text":" Portal Documentation - clj-docs
"},{"location":"data-structures/","title":"Data structures","text":"Clojure is a very data-centric language. clojure.core
contains a great number of functions for manipulating data structures, especially the immutable built in data structures, referred to generically as collections.
Collections can take any types of elements and types can be mixed. Collections can even have other collections as an element.
Collections are passed as arguments to function (either in part or in full) and functions often return collections as a result.
"},{"location":"data-structures/#built-in-collections","title":"Built-in collections","text":"Values can be represented as a collection of discrete pieces of data: number, string, boolean value.
Clojure has great facilities for working with collections of data, providing many types of data structures and a uniform way to use all of these data structures.
The 4 commonly used built-in data structures
Name syntax Description list ()
A linked list, optomised for sequential access from the front (head) vector []
An indexed array optimised for random access hash-map {:key \"value\"}
Associative collection of key / value pairs, keys must be unique. Keys are the index set #{}
A unique set of values Vector and hash-map are the most commonly collections used to model information with Clojure.
Lists are not explicitly used to model data, although data may be returned by a function as a list (referred to as a sequence)
"},{"location":"data-structures/#collection-characteristics","title":"Collection Characteristics","text":"Clojure data structure share the following characteristics:
- Immutable - once a data structure is defined it cannot be changed
- Persistent - functions may return an altered copy of a data structure which will share common values with the original data structure for efficient memory use (structural sharing)
- Dynamically typed - a data structure can contain any value, including functions (as they evaluate to a value) and other data structures (nested data structures)
This section will cover the Clojure built in persistent data structures in more detail.
"},{"location":"data-structures/#common-data-structures","title":"Common Data Structures","text":"Simple data
(def name value)
Sequential data
(list ...) sequence - always processed sequentially
(vector) sequencw with randon access
Dictionary
(key value key1 value key2 value)
Connverting data, data decoder/encoder, state machine, etc
Data set
(def name\n [{:identical-keys \"with evolving values\"}\n {:identical-keys \"values differ from each other\"}\n {:identical-keys \"values collectively provide meaning\"}])\n
Weather monitoring data, bank transactions, stock exchange rates, etc
"},{"location":"data-structures/#hierarchical-data","title":"Hierarchical data","text":"(def name\n {:simple-key value\n :composite-key {:nested-key value}\n :deeper-composite-key {:nested-key {:deeper-nested-key value}}})\n
representing state, structure of a website Starwars example,
walk the hierarchy to get the appropriate values
extract only the values required by a function and pass as arguments
hierachiecy can become too complex to manage, the flatest possible structure is usually simpler to work with (transform)
"},{"location":"data-structures/alternatives/","title":"Alternative data structures","text":"Whist list, vector, hash-map and set are by far the most commonly used data structures, Clojure has many others of interest.
"},{"location":"data-structures/alternatives/#variations-on-hash-maps","title":"Variations on hash-maps","text":""},{"location":"data-structures/alternatives/#variations-on-sets","title":"Variations on sets","text":"ordered-set
"},{"location":"data-structures/list/","title":"List","text":"The list is used extensively in Clojure, it is a List (List Processing) language after all. The unique thing about lists is that the first element is always evaluated as a function call, therefore lists are most commonly used for defining and calling functions.
Lists are sometimes used as a data structure and have a sequential lookup time. A list can hold any valid types of data, from numbers and strings to other data structures such as vectors, maps and sets. Types can be mix as Clojure will dynamically type each element as its evaluated.
Its more common to use vectors and maps which typically offer quicker access as they can be looked up via an index or key.
Note Explore the list data structure and discover which line of code fails. Try work out why that line of code fails.
(list 1 2 3 4)\n(list -1 -0.234 0 1.3 8/5 3.1415926)\n(list \"cat\" \"dog\" \"rabbit\" \"fish\")\n(list :cat 1 \"fish\" 22/7 (str \"fish\" \"n\" \"chips\"))\n(list 1 2 \"three\" [4] five '(6 7 8 9))\n(list )\n\n( 1 2 3 4)\n\n(quote (1 2 3 4))\n'(1 2 3 4)\n\n;; Duplicate elements in a list ?\n(list 1 2 3 4 1)\n(list \"one\" \"two\" \"one\")\n(list :fred :barney :fred)\n
We can create a list using the list
function
(list 1 2 3 4)\n
This evaluates to (1 2 3 4)
We can give this result a name
(def my-list (list 1 2 3 4))\n
Then when we evaluate my-list
it will return the list as a result
However, if we create a list directly by using (1 2 3 4)
, this will fail when evaluated as 1
is not a function. So when we define a data structure as a list we need to use the quote
function or ' syntax
(quote (1 2 3 4))\n'(1 2 3 4)\n
"},{"location":"data-structures/list/#hintfirst-element-of-a-list-is-a-function-call","title":"Hint::First element of a list is a function call","text":"The first element of a list is evaluated as a function call, unless the list is wrapped in a quote function
"},{"location":"data-structures/list/#testing-for-a-list","title":"Testing for a List","text":"When is a list not a list?
. Lists are sometimes created as other types if they are created in ways other than using the list
function. If you want to know if something is list like, then you can use the seq?
function. If you test with the list?
function and that returns false, you can use the type
function to see what its real type is.
See more about the types that list-like structures actually are in the article: What is a list? The ultimate predicate showdown
"},{"location":"data-structures/naming/","title":"Naming data structures","text":"We have seen that defining things is as simple as giving a name to a value using the def
function. It is the same for the Clojure data structures and any other values.
(def people [\"Jane Doe\" \"Samuel Peeps\"])\n
Names are of course case sensitive, so Person is not the same as person
(def Person \"James Doh\" \"Sam Stare\")\n
Clojure uses dynamic typing, this means its trivial to mix and match different kinds of data. Here we are defining a name for a vector, which contains numbers, a string and name of another def.
(def my-data [1 2 3 \"frog\" person])\n\nmy-data\n
"},{"location":"data-structures/naming/#data-structures-are-immutable-names-are-mutable","title":"Data structures are immutable, names are mutable","text":"You can dynamically re-define a name to points to a different value.
Hint This re-definition (or rebinding) of names to new values is typically used only during the development of your code, especially in REPL driven development.
(def my-data [1 2 3 4 5 \"frog\" person])\n
The original value that defined my-data remains unchanged (its immutable), so anything using that value remains unaffected. Essentially we are re-mapping my-data to a new value.
Lets define a name to point to a list of numbers
(def my-list '(1 2 3))\n
We are returned that list of numbers when we evaluate the name
my-list\n
We can use the cons function to add a number to our list, however because lists are immutable, rather than changing the original list, a new one is returned. So if we want to keep on referring to our \"changed\" list, we need to give it a name
(def my-list-updated (cons 4 my-list))\n
As you can see we have not changed the original list
my-list\n
;; The new list does have the change though.
my-list-updated\n
You could therefore give the impression of mutable state by applying a function to data structure and redefining the original name to point to the resulting data structure.
Hint In practice, the ability to redefine functions and data structures live helps you develop your application quickly in the REPL.
In production you typical do not redefine functions or data structures in a live running application. That could be part of a new release of your application though.
(def my-list (cons 5 my-list))\n
So now when we evaluate the original name, we get the updated list
my-list\n
"},{"location":"data-structures/naming/#naming-scope","title":"Naming Scope","text":"All def names are publicly available via their namespace. As def values are immutable, then keeping things private is of less concern than languages built around Object Oriented design.
Private definitions syntax can be used to limit the access to def names to the namespace they are declared in.
To limit the scope of a def, add the :private true metadata key value pair.
(def ^{:private true} some-var :value)\n\n(def ^:private some-var :value)\n
The second form is syntax sugar for the first one.
You could also define a macro for def-
(defmacro def- [item value]\n `(def ^{:private true} ~item ~value)\n)\n
You would then use this macro as follows:
(def- private-definition \"This is only accessible in the namespace\")\n
"},{"location":"data-structures/pretty-printing/","title":"Pretty Printing data structures","text":"Data structures containing small amounts of data are quite human readable, although can benefit from pretty printing to make them very easy for humans to read.
The larger a data structure becomes, or if a data structure is nested, then there are tools to print out ascii views of the data structures.
"},{"location":"data-structures/pretty-printing/#pretty-print-hash-maps","title":"Pretty print hash-maps","text":"(clojure.pprint/pprint\n {:account-id 232443344 :account-name \"Jenny Jetpack\" :balance 9999 :last-update \"2021-12-12\" :credit-score :aa} )\n
Each key is printed on a new line, making the hash-map easier to read, especially when there are a large number of keys
{:account-id 232443344,\n :account-name \"Jenny Jetpack\",\n :balance 9999,\n :last-update \"2021-12-12\",\n :credit-score :aa}\n
Clojure aware editors can also have an align option when formatting hash-maps, making the results easier to read
{:account-id 232443344,\n :account-name \"Jenny Jetpack\",\n :balance 9999,\n :last-update \"2021-12-12\",\n :credit-score :aa}\n
"},{"location":"data-structures/pretty-printing/#hintpretty-print-evaluation-results","title":"Hint::Pretty Print evaluation results","text":"Clojure aware editors should allow the pretty printing of the evaluation results.
"},{"location":"data-structures/pretty-printing/#print-table-of-nested-data-structures","title":"Print Table of nested data structures","text":"Nested data structures can also be shown as a table, especially the common approach of using a vector of hash-maps where each map has the same keys
(clojure.pprint/print-table\n [{:location \"Scotland\" :total-cases 42826 :total-mortality 9202}\n {:location \"Wales\" :total-cases 50876 :total-mortality 1202}\n {:location \"England\" :total-cases 5440876 :total-mortality 200202}])\n
| :location | :total-cases | :total-mortality |\n|-----------+--------------+------------------|\n| Scotland | 42826 | 9202 |\n| Wales | 50876 | 1202 |\n| England | 5440876 | 200202 |\n
"},{"location":"data-structures/pretty-printing/#references","title":"References","text":"Data browsers (Cider Inspector, Portal, Reveal Free) are very useful for larger and nested data structures.
"},{"location":"data-structures/set/","title":"Set","text":"A Clojure set is a persistent data structure that holds a unique set of elements. Again the elements can be of any type, however each element must be unique for a valid set.
Note Explore creating sets from existing collections. Notice what happens if you have duplicate values in the collection. Define sets directly using the #{}
notation and see what happens if there are duplicate values.
(set `(1 2 3 4))\n(set `(1 2 1 2 3 4))\n\n#{1 2 3 4}\n#{:a :b :c :d}\n;; duplicate key error\n#{1 2 3 4 1}\n
"},{"location":"data-structures/set/#unique-but-not-ordered","title":"Unique but not ordered","text":"A set is not ordered by the values it contains. If you need a sorted set then you can use the sorted-set
function when creating a new set. Or you can run
(sorted-set 1 4 0 2 9 3 5 3 0 2 7 6 5 5 3 8)\n\n(sort [9 8 7 6 5])\n(sort-by )\n
"},{"location":"data-structures/set/#looking-up-values-in-a-set","title":"Looking up values in a set","text":"(#{:a :b :c} :c)\n(#{:a :b :c} :z)\n
Sets can also use the contains?
function to see if a value exists in a set
(contains?\n #{\"Palpatine\" \"Darth Vader\" \"Boba Fett\" \"Darth Tyranus\"}\n \"Darth Vader\")\n
"},{"location":"data-structures/shared-memory/","title":"Shared memory with Persistent data structures","text":"The Clojure data structures are immutable, so they initially seem similar to constants rather than variables. Once a collection is created, it cannot be changed. Any functions that run on a collection do not change the collection, instead they return a new collection with the respective changes.
Creating a new collection each time may seem inefficient, however, the persistent collections use a sharing model. When a new collection is created, it links to all the relevant elements of the original collection and adds any new elements.
Hint Read the InfoQ article on An In-Depth Look at Clojure Collections.
"},{"location":"data-structures/vector/","title":"Vector","text":"Vectors are an indexed sequential collections of data, basically the same as arrays in other languages. However, there are several differences. The index for a vector starts at 0, just like arrays in other languages.
Vectors are written using square brackets []
with any number of pieces of data inside them, separated by spaces.
Note Experiment with creating vectors for your data structures
(vector 1 2 3 4)\n[1 2 3 4 5]\n[56.9 60.2 61.8 63.1 54.3 66.4 66.5 68.1 70.2 69.2 63.1 57.1]\n[]\n\n(def pi 3.1435893)\n[1 2.4 pi 11/4 5.0 6 7]\n[:cat :dog :rabbit :fish]\n[{:cat 1} \"fish\" \"potatoes\" \"oil\" (str \"who ate my\" \"fish n chips\")]\n\n;; Include other data structures in vectors, in this example a list is an element of the vector\n[1 2 3 '(4 5 6)]\n\n;; Are duplicate elements allowed ?\n[1 2 3 4 1]\n
Note What can you do with vectors? Vectors are easy to add more items to, delete items from, or pull arbitrary items out of. Here are some functions that operate on vectors.
(vector? [5 10 15])\n(= [] [])\n(= [] [1])\n\n(first [5 10 15])\n(rest [5 10 15])\n(nth [5 10 15] 1)\n(count [5 10 15])\n\n(conj [5 10] 15)\n
Hint When a function is effectively asking if a value is true or false, its referred to as a predicate function. Its common practice in Clojure to place a ?
at the end of that functions name.
"},{"location":"data-structures/vector/#lookup-data-from-a-vector","title":"Lookup data from a Vector","text":"([1 2 3] 1)\n\n;; ([1 2 3] 1 2) ;; wrong number of arguments, vectors behaving as a function expect one parameter\n\n;; ((1 2 3) 1) ;; you cant treat lists in the same way, there is another approach - assoc\n
"},{"location":"data-structures/vector/#changing-vectors","title":"Changing vectors","text":"The next two functions are used to make new vectors. The vector
function takes any number of items and puts them in a new vector.
conj
takes a vector and an item and returns a new vector with that item added to the end. The function name is taken from the verb \"conjugate\", meaning \"to join together.
Remember that collections in Clojure are immutable, so when we say that a function \"adds to\" or \"removes from\" a collection, what we mean is that the function returns a new collection with an item added or removed.
Note Using one or more vectors, create a data structure of the high temperatures for the next 7 days in your area. Use the nth
function to get the high temperature for next Friday
"},{"location":"data-structures/hash-maps/","title":"Data Structures: Hash-maps","text":"Associative collection of key value pairs
Useful for defining self-describing structured data (assuming meaningful key names are used)
A map is a key / value pair data structure. Keys are usually defined using a keyword, although they can be strings or anything else.
Keywords point to themselves, so using them for the keys makes it very easy to get values out of the map, or for updating existing values in the map.
Note Explore creating maps
{:key \"value\"}\n{:key :value}\n{\"key\" \"value\"}\n(\"key\" :value)\n(:meaining-of-life 42)\n{:a 1 :b 2 :c 3}\n{:monday 1 :tuesday 2 :wednesday 3 :thursday 4 :friday 5 :saturday 6 :sunday 7}\n{1 \"Monday\" 2 \"Tuesday\" 3 \"Wednesday\" 4 \"Thursday\" 5 \"Friday\" 6 \"Saturday\" 7 \"Sunday\"}\n
"},{"location":"data-structures/hash-maps/#hintcomma-characters-are-treated-as-white-space","title":"Hint::Comma characters are treated as white-space","text":"The comma character is rarely used in Clojure hash-maps as it is ignored by Clojure. When coming from other languages, it may be initially comforting to include commas.
"},{"location":"data-structures/hash-maps/#nested-data-models","title":"Nested data models","text":"nested maps to create a hierarchy or path for data. This can add more context to the overall design
various types of data
"},{"location":"data-structures/hash-maps/#hintone-data-structure-to-rule-them-all","title":"Hint::One data structure to rule them all","text":"It is preferred to have a single data structure to model the data of a system, which is them used by all the functions of that system. An example is in the state used for an application, e.g. Practicalli website practicalli.data namespace
If there is no logical connection between data across a system, then data should be grouped into one structure per namespace as a minimal approach.
"},{"location":"data-structures/hash-maps/#example-use-data-sets","title":"Example use: Data sets","text":"A collection of maps which have the same form, e.g. a vector of hash-maps with the same keys
Example: meteorological recordings
(def recording-station-876WA\n [{:timestamp \"2021-12-01T12:00\" :location {:latitude 24.3453434 :longitude 10.348888} :temperature 12.4 :rainfail 0.1 :uv-level 0.4}\n {:timestamp \"2021-12-01T12:10\" :location {:latitude 24.3453434 :longitude 10.348888} :temperature 12.6 :rainfail 0.1 :uv-level 0.45}\n {:timestamp \"2021-12-01T12:00\" :location {:latitude 24.3453434 :longitude 10.348888} :temperature 12.9 :rainfail 0.1 :uv-level 0.5}])\n
Providing a collection of consistent hash-map data structures is very easy to work with in Clojure.
reduce
, filter
and map
functions can easily process this form of data as part of algorithms to interpret the meaning from a data set.
As each recording station creates the same types of data, then they can be merged by including the recording station id in the map
"},{"location":"data-structures/hash-maps/access/","title":"Accessing hash-maps","text":"The values in a hash-map can be accessed in multiple ways
get
get-in
contains?
- using hash-map as a function
- use :keyword as a function
- threading hash-map through one or more keys
Clojure provides a get function that returns the value mapped to a key in a set or map.
"},{"location":"data-structures/hash-maps/access/#get-and-get-in-functions","title":"get and get-in functions","text":"get
is a very explicitly named function that makes its purpose very clear. The get
function works regardless of the type of keys used in the hash-map.
(get map key)
get-in
has the same quality, for use with nested hash-maps.
(get-in nested-map [:keys :path])\n
(get-in {\"timestamp\" 1291578985220 \"scores\" {\"FSU\" 31 \"UF\" 7}} [\"scores\" \"FSU\"])\n;;=> 31\n\n(get-in {\"timestamp\" 1291578985220 \"scores\" {\"FSU\" 31 \"UF\" 7}} [\"scores\"])\n;;=> {\"FSU\" 31, \"UF\" 7}\n\n(get-in {\"timestamp\" 1291578985220 \"scores\" {\"FSU\" 31 \"UF\" 7}} [])\n;;=> {\"timestamp\" 1291578985220, \"scores\" {\"FSU\" 31, \"UF\" 7}}\n\n(get-in {\"timestamp\" 1291578985220 \"scores\" {\"FSU\" 31 \"UF\" 7}} nil)\n;;=> {\"timestamp\" 1291578985220, \"scores\" {\"FSU\" 31, \"UF\" 7}}\n
"},{"location":"data-structures/hash-maps/access/#hintmissing-or-incorrect-key","title":"Hint::missing or incorrect key","text":"If the key in the path is missing or the path is missing (or nil) then get-in
will return more of the hash-map than expected.
"},{"location":"data-structures/hash-maps/access/#using-hash-map-as-a-function","title":"Using hash-map as a function","text":"A hash-map (and list, vector, set) can be called as a function with a key as the argument. This provides a more terse expression than using get
and also works irrespective of key types used.
Passing the key :star-wars
to the hash-map returns the value associated with that key
({:star-wars {:characters {:jedi [\"Luke\" \"Obiwan\"]}}} :star-wars)\n
A nested hash-map (containing other hash-maps) can be accessed via multiple nested calls to the returned values.
((({:star-wars {:characters {:jedi [\"Luke\" \"Obiwan\"]}}} :star-wars) :characters) :jedi)\n
"},{"location":"data-structures/hash-maps/access/#keyword-key-as-a-function","title":"keyword key as a function","text":"A keyword can be called as a function, taking a hash-map as an argument
(:star-wars {:star-wars {:characters {:jedi [\"Luke\" \"Obiwan\"]}}})\n
A nested hash-map (containing other hash-maps) can be accessed via multiple nested calls to the returned values.
(:jedi (:characters (:star-wars {:star-wars {:characters {:jedi [\"Luke\" \"Obiwan\"]}}})))\n
"},{"location":"data-structures/hash-maps/access/#threading-macro","title":"Threading macro","text":"Using keyword keys as functions, the thread macros provide a consistent approach to accessing hash-map data
The hash-map is passed through one or more keyword keys, so obtaining values from a flat or nested hash-map is just the same.
(-> hash-map\n :keyword1\n ,,,)\n
If the keys are a type other than keywords, then a get function would be required for accessing the hash-map.
(-> hash-maps (get \"scores\") (get \"FSU\"))\n
As part of a processing pipeline, taking specific values from a JSON file of association football match statistics
(-> match-statistics.json\n (clojure.data.json/read-str :key-fn keyword)\n :totals\n :goals-home-team)\n
"},{"location":"data-structures/hash-maps/access/#checking-a-key-or-value-exists-in-a-hash-map","title":"Checking a key or value exists in a hash-map","text":"keys
function will return a collection of the keys contained in a map. vals
returns a collection of the values
is in a map or set. In general I use the value returned from a map or set to determine if a key exists - the following snippet uses that pattern.
Check if a key has a specific value
(if (star-wars-map :space-ships)\n (do-true-behaviours)\n (do-false-behaviours))\n
Check a key has a specific value and also use that value
TODO: is this a good case for if-lets
This pattern fails if the value of :key is nil.
"},{"location":"data-structures/hash-maps/access/#contains-and-some","title":"contains? and some","text":"contains?
checks for the index of a collection. The index of a hash-map is the keys it contains
some
will check for a value in a collection
(def recipe-map {:ingredients \"tofu\"})\n\n(contains? recipe-map :ingredients)\n;; => true\n\n(some #{\"tofu\"} recipe-map)\n;; => nil\n\n(vals recipe-map)\n;; => (\"tofu\")\n\n(some #{\"tofu\"} (vals recipe-map))\n;; => \"tofu\"\n
The key is contained as part of the hash-map index, irrespective of the value associated with that key (so long as there is a legal value associate with the key).
(contains? {:totals nil} :totals)\n
"},{"location":"data-structures/hash-maps/accessing-nested-hash-maps/","title":"Accessing Nested Hash-maps","text":"Its also quite common to have maps made up of other maps, maps of vectors or vectors of maps.
Now we can refer to the characters using keywords. Using the get function we return all the information about Luke
(get star-wars-characters :luke)\n(get (get star-wars-characters :luke) :fullname)\n
By wrapping the get function around our first, we can get a specific piece of information about Luke. There is also the get-in function that makes the syntax a little easier to read
(get-in star-wars-characters [:luke :fullname])\n(get-in star-wars-characters [:vader :fullname])\n
Or if you want the data driven approach, just talk to the map directly
(star-wars-characters :luke)\n(:fullname (:luke star-wars-characters))\n(:skill (:luke star-wars-characters))\n\n(star-wars-characters :vader)\n(:skill (:vader star-wars-characters))\n(:fullname (:vader star-wars-characters))\n
And finally we can also use the threading macro to minimise our code further
(-> star-wars-characters\n :luke)\n\n(-> star-wars-characters\n :luke\n :fullname)\n\n(-> star-wars-characters\n :luke\n :skill)\n
This technique is called destructuring. Find out more on Destructuring
Duplicate keys in a map are not allowed, so the following maps...
{\"fish\" \"battered\" \"chips\" \"fried\" \"fish\" \"battered and fried\"}\n{:fish \"battered\" :chips \"fried\" :fish \"battered & fried\"}\n\n;; ...throw duplicate key errors\n\n;; Duplicate values are okay though\n{:fish \"fried\" :chips \"fried\" :peas \"mushy\"}\n
"},{"location":"data-structures/hash-maps/create/","title":"Creating Hash-maps","text":"Hash-maps can be defined literally using {}
and including zero or more key / value pairs. Keys and values can be any legal Clojure type.
Keywords are very commonly used for keys as they provide a convenient way to look up values.
"},{"location":"data-structures/hash-maps/create/#literal-hash-map-examples","title":"Literal hash-map examples","text":"A hash-map defining Obi-wan, a character from the Star Wars universe.
{:name \"Obi-wan Kenobi\" :homeworld \"Stewjon\"}\n
A hash-map defining Leia, another character from the Star Wars with additional information
{:name \"Leia Skywalker\" :homeworld \"Alderaan\" :birthplace \"Polis Massa\"}\n
Use def
to bind a name to a hash-map, making it easier to pass the map to a function as an argument.
(def luke {:name \"Luke Skywalker\" :homeworld \"Tatooine\" :birthplace \"Polis Massa\"})\n
"},{"location":"data-structures/hash-maps/create/#data-set-of-maps","title":"Data set of maps","text":"Create a data set by defining a vector of hash-maps
[{:name \"Obi-wan Kenobi\" :homeworld \"Stewjon\" :occupation \"Jedi\"}\n {:name \"Jyn Erso\" :homeworld \"Vallt\" :occupation \"Soldier\"}\n {:name \"Leia Skywalker\" :homeworld \"Alderaan\" :occupation \"Senator\"}\n {:name \"Luke Skywalker\" :homeworld \"Tatooine\" :occupation \"Jedi\"}\n {:name \"Qui-Gon Jinn\" :homeworld \"Coruscant\" :occupation \"Jedi\"}\n {:name \"Padm\u00e9 Amidala\" :homeworld \"Naboo\" :occupation \"Senator\"}\n {:name \"Sheev Palpatine\" :homeworld \"Naboo\" :occupation \"Supreme Chancellor\"}]\n
"},{"location":"data-structures/hash-maps/create/#example-nested-hash-maps","title":"Example: nested hash-maps","text":"Create a map to represent the world of Star Wars, including various characters & ships, indicating the factions that characters and ships belong to.
Individual Star Wars characters can be defined using a map of maps
{:luke {:name \"Luke Skywalker\" :skill \"Targeting Swamp Rats\"}\n :vader {:name \"Darth Vader\" :skill \"Breaking the rules and peoples hearts\"}\n :jarjar {:name \"JarJar Binks\" :skill \"Failing upwards\"}}\n
Hash-maps can also use other collections as values
{:characters\n {:jedi [\"Luke Skywalker\" \"Obiwan Kenobi\"]\n :sith [\"Darth Vader\" \"Darth Sideous\"]\n :droids [\"C3P0\" \"R2D2\" \"BB8\"]}\n :ships\n {:rebel-alliance [\"Millennium Falcon\" \"X-wing fighter\"]\n :imperial-empire [\"Intergalactic Cruiser\" \"Destroyer\"\n \"Im just making these up now\"]}}\n
Use the def function to bind a name to the Star Wars character information, making it easier to pass to several functions
(def star-wars-characters\n {:luke {:fullname \"Luke Skywalker\" :skill \"Targeting Swamp Rats\"}\n :vader {:fullname \"Darth Vader\" :skill \"Breaking the rules and peoples hearts\"}\n :jarjar {:fullname \"JarJar Binks\" :skill \"Failing upwards\"}})\n
"},{"location":"data-structures/hash-maps/create/#generating-hash-maps","title":"Generating hash-maps","text":"hash-map
is a clojure.core function that returns a hash-map of the given arguments, or an empty hash-map, {}
, if no arguments are given.
Arguments should be key-value pairs, otherwise the function will return nil
"},{"location":"data-structures/hash-maps/create/#converting-collections-to-hash-maps","title":"Converting collections to hash-maps","text":"(apply hash-map [:a 1 :b 2])\n;;=> {:b 2 :a 1}\n
Order of keys in a hash-map is not guaranteed. However, order of keys should be irrelevant as the keys are unique within a map.
(into {} ,,,)\n
map
reduce
merge returns a hash-map that is a merging of the key value pairs from all maps, for any duplicate keys the value from the last key (left to right) is used
"},{"location":"data-structures/hash-maps/create/#setting-default-values","title":"Setting default values","text":"Calling a function with a hash-map as an argument is a flexible way to design the API of a namespace and Clojure application in general.
As functions are talking a map, a function call with fewer or more keys than needed will still result in a successful call (alhtough results could be interesting)
If fewer keys are passed then defaults can be set.
merge
can be used to ensure any required keys with default values are always present, and still over-ridden by keys passed in as an argument
merge
should passed the default key values first, with the argument map merged on top. This ensures all keys are present and that the argument values are used if duplicate keys exist between the maps.
(merge {:option1 \"default-value\" :option2 \"default-value\"}\n {:option1 \"custom-value\"})\n;;=> {:option1 \"custom-value\" :option2 \"default-value\"}\n
The merge
function can be used in a function to return the merged map of default and argument values When a function has a number of options with default values.
(defn parse-cli-tool-options\n \"Return the merged default options with any provided as a hash-map argument\"\n[arguments]\n (merge {:replace false :report true :paths [\".\"]}\n arguments))\n\n(parse-cli-tool-options {:paths [\"src\" \"test\"] :report false})\n;; => {:replace false, :report false, :paths [\"src\" \"test\"]}\n
If an empty hash-map is sent as an argument, the default values are returned
(parse-cli-tool-options {})\n;; => {:replace false, :report true, :paths [\".\"]}\n
zipmap
(zipmap [:a :b :c] [1 2 3])\n;; => {:a 1, :b 2, :c 3}\n
"},{"location":"data-structures/hash-maps/create/#custom-merging-with-a-function","title":"Custom merging with a function","text":""},{"location":"data-structures/hash-maps/create/#create-a-sub-set-of-existing-map","title":"Create a sub-set of existing map","text":""},{"location":"data-structures/hash-maps/create/#filter","title":"filter","text":"Create a sub-set of an existing map
;; #61 - Map Construction ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Difficulty: Easy ;; Topics: core-functions ;; Special Restrictions: zipmap
;; Write a function which takes a vector of keys and a vector of values and constructs a map from them.
;; Tests (= ([:a :b :c] [1 2 3]) {:a 1, :b 2, :c 3}) (= ( [1 2 3 4] [\"one\" \"two\" \"three\"]) {1 \"one\", 2 \"two\", 3 \"three\"}) (= (__ [:foo :bar] [\"foo\" \"bar\" \"baz\"]) {:foo \"foo\", :bar \"bar\"})
;; If we could use zipmap then the answer would be simple
(zipmap [:a :b :c] [1 2 3]) ;; => {:a 1, :b 2, :c 3}
(= (zipmap [:a :b :c] [1 2 3]) {:a 1, :b 2, :c 3}) ;; => true
;; So now we have to figure out the algorithm that zipmap uses
;; Analyse the problem ;; We want to create a paring of values from the first and second vectors ;; Then each pair should be made into a key value pair within a map data structure.
;; The map function will work over multiple collections, returning a single collection
;; A simple example of map function in action: (map str [:a :b :c] [1 2 3]) ;; => (\":a1\" \":b2\" \":c3\")
;; In stead of string, we could use hash-map
(map hash-map [:a :b :c] [1 2 3]) ;; => ({:a 1} {:b 2} {:c 3})
;; now we just need to put all the maps into one map, so perhaps merge will work
(merge (map hash-map [:a :b :c] [1 2 3])) ;; => ({:a 1} {:b 2} {:c 3})
(conj (map hash-map [:a :b :c] [1 2 3])) ;; => ({:a 1} {:b 2} {:c 3})
(reduce conj (map hash-map [:a :b :c] [1 2 3])) ;; => {:c 3, :b 2, :a 1}
;; (reduce conj (map vectork ks vs))
((fn [key-sequence value-sequence] (into {} (map vector key-sequence value-sequence))) [:a :b :c] [1 2 3]) ;; => {:a 1, :b 2, :c 3}
"},{"location":"data-structures/hash-maps/update/","title":"Update Hash-maps","text":""},{"location":"defining-behaviour-with-functions/","title":"Defining behaviours with functions","text":"Clojure has functions, rather than methods for defining behaviour / \"algorithms\"
Clojure design at its most basic comprises:
- one or more data structures
- functions that process those data-structures
There is a common saying in Clojure: \"Its better to have one data structure and many functions, than many data structures and many functions\"
"},{"location":"defining-behaviour-with-functions/anonymous-functions/","title":"Anonymous Functions","text":"clojure.core/fn
is a function for defining custom functions.
fn
is called the anonymous function as it has no external name by which it can be referred by. They are used within the scope of another function call, as having no name they cannot be called from another part of the code.
(map (fn [args] ,,,) [1 2 3])\n((fn [args] ,,,))\n
The value of using anonymous functions comes when there is a short, specific piece of behaviour required which is unlikely to be needed elsewhere in the code. An anonymous function can always be refactored into a defn
expression if used in multiple places.
"},{"location":"defining-behaviour-with-functions/anonymous-functions/#definition-of-an-anonymous-function","title":"Definition of an anonymous function","text":"(fn [argument] (str \"some behaviour, typically using the arguments passed:\" argument ))\n
This expression is a function call to fn
which has the arguments called argument
"},{"location":"defining-behaviour-with-functions/anonymous-functions/#calling-an-anonymous-function","title":"Calling an anonymous function","text":"To get a value from evaluating this function you need to pass it a value (or another function) as an argument, as well as calling it as a function by placing the anonymous function as the first element of a list.
((fn [arguments] (str \"behaviour, typically using the arguments passed: \" arguments )) \"Is this the right room for an argument\")\n
"},{"location":"defining-behaviour-with-functions/anonymous-functions/#binding-a-local-names","title":"Binding a local names","text":"fn
can have a local name which can be used to write a recursive function (a fn that calls itself).
Adding a name also helps with debugging code, as the name will be used to identify that function call if it appears in a stack trace of an exception.
A recursive function that counts the elements in a collection
(fn -count [xs]\n (if (empty? xs)\n 0\n (inc (-count (rest xs)))))\n
(fn meaningful-name\n []\n (str \"If I fail, you will know my name\"))\n
"},{"location":"defining-behaviour-with-functions/anonymous-functions/#anonymous-function-syntactic-sugar","title":"Anonymous function Syntactic Sugar","text":"There is a short form of the function definition using the #( ,,, )
syntax.
For example, if we want to increment an argument we could start to define an anonymous function as follows:
#(inc %)\n
The %
represents a placeholder for an argument that is passed into the anonymous function. This argument is anonymous as well as the value is simply swapped into the position of %
.
To evaluate this anonymous function we need to give it an argument to work on. Anything after the anonymous function is taken as its argument. So in the following expression we pass the value 1 as the argument and we should get the result of incrementing 1 as a result
( #(inc %) 1 )\n\n;; => 2\n
The %
placeholder can also refer to a specific argument by adding an index number. The index numbers refer to the position of the arguments supplied to the anonymous function.
Here we will add two different arguments together
( #(+ %1 %2) 20 22)\n
So %1
will represent the first argument and the %2
will represent the second argument passed to this function.
Sometimes position can be important as the following two versions of code demonstrate
( #(/ %1 %2) 24 6)\n\n( #(/ %2 %1) 24 6)\n
These two expressions give different values (and return different types, Integer and Ratio) as the positions of the arguments have been reversed.
"},{"location":"defining-behaviour-with-functions/calling-functions/","title":"Calling Functions","text":"To call a function in Clojure you use the name of the function as the first element of a list.
In this simple example, a function is defined that takes no arguments, then that function is called.
(defn my-function []\n (str \"I only return this string\"))\n\n(my-function)\n
Functions can be defined to take arguments.
"},{"location":"defining-behaviour-with-functions/calling-functions/#arity","title":"Arity","text":"This is the term to describe the number of arguments a function takes. This can be a fixed number or variable number of arguments.
Simple polymorphism can also be used to have one function take different numbers of arguments, as with the multi-arity
function in the examples below.
(defn single-arity [] \n (str \"I do not take any arguments\"))\n\n(defn single-arity [argument] \n (str \"I take 1 argument only\"))\n\n(defn triple-arity [argument1 argument2 argument3] \n (str \"I take 3 arguments only\"))\n\n(defn multi-arity \n ([argument] \n (str \"I match 1 argument only\"))\n ([argument1 argument2]\n (str \"I match when 2 arguments are used\")))\n\n(defn variable-arity [argument & more-arguments]\n (str \"I assign the first argument to argument, \n all other arguments to more-arguments\"))\n
"},{"location":"defining-behaviour-with-functions/examples/","title":"Examples","text":""},{"location":"defining-behaviour-with-functions/parameters/","title":"Parameters","text":""},{"location":"defining-behaviour-with-functions/syntax/","title":"Syntax","text":"Defining functions is done with the fn
function
We have already seen the def
function to assign names to values. We can also use the same function to give a name to our functions.
"},{"location":"designing-data-structures/","title":"Designing Data Structures","text":"Some common design guides for data structures in Clojure
"},{"location":"designing-data-structures/#the-basics-design-approach","title":"The Basics design approach","text":"Most data structures in Clojure seem to be created from either vectors or maps or a combination of both. Sets are used where uniqueness of values is important and lists are often used for their lazy properties.
Vectors are the most flexible data structure in Clojure and support none-sequential access as they are indexed.
Maps are really useful for defining semantic meaning to your data structures, helping you create data structures that express the context of the model they represent. Maps give you unordered, arbitrary index arrangement. Access is iteration of key/value pairs or getting a value for a given key.
Lists give you sequential, one-at-a-time arrangement. They allow for efficient iteration, lazy generation, and stack discipline.
Sets give you unordered, unique constraint arrangement. Access is iteration of elements or checking containment.
"},{"location":"designing-data-structures/modeling-alphabet-codes/","title":"Model alphabet codes","text":"Maps in Clojure are used to model key and value pairs.
- Keys must be unique within a map.
- A key can be a number, string or keyword.
Vectors in Clojure are a general data structure that are good for handing any kind of information.
Note Define a data structure where each letter of the alphabet is represented by a 6 digit binary code
Lets define a name called alphabet
that is bound to a map. Each key in the map is a character of the alphabet and each value is a vector of numbers that represent a binary code.
The map also includes a binary code for a full stop and space character
(def alphabet {\"A\" [0 1 0 0 0 1]\n \"B\" [0 0 1 0 1 0]\n \"C\" [0 1 0 0 1 0]\n \"D\" [1 0 1 0 0 0]\n \"E\" [1 0 1 1 0 0]\n \"F\" [1 1 0 1 0 0]\n \"G\" [1 0 0 1 1 0]\n \"H\" [1 0 1 0 0 1]\n \"I\" [1 1 1 0 0 0]\n \"J\" [0 0 1 1 1 1]\n \"K\" [0 1 0 1 0 1]\n \"L\" [1 1 1 0 0 1]\n \"M\" [1 1 1 0 1 1]\n \"N\" [0 1 1 1 0 1]\n \"O\" [1 1 0 1 1 0]\n \"P\" [1 1 1 1 1 0]\n \"Q\" [1 0 1 1 1 0]\n \"R\" [1 1 1 1 0 0]\n \"S\" [0 1 1 1 1 0]\n \"T\" [1 0 0 1 1 1]\n \"U\" [0 0 1 0 1 1]\n \"V\" [0 1 1 0 0 1]\n \"W\" [1 1 0 1 0 1]\n \"X\" [1 0 1 0 1 0]\n \"Y\" [1 0 0 0 1 1]\n \"Z\" [1 1 0 0 1 1]\n \".\" [1 0 1 1 0 1]\n \" \" [0 0 1 0 0 0]})\n
"},{"location":"designing-data-structures/modeling-name-generation-map/","title":"Design a map for name generation","text":"Imagine you are writing a simple name generator that takes your name and creates an alternative version. For example this could be a generator of your \"Posh\" or \"Hipster\" name.
Note Define a data structure to model sloane names that has three names for every letter of the alphabet. For name suggestions, see the Tattler sloane name generator.
The following seems to be the simplest way to model the sloane names. This follows the representation in the original source material.
(def sloane-first-name\n {\"a\" \"Ally-Pally\"\n \"b\" \"Bongo\"\n \"c\" \"Chipper\"})\n\n(def slone-second-name\n {\"a\" \"Anstruther\"\n \"b\" \"Beaufort\"\n \"c\" \"Cholmondeley\"})\n\n(def slone-third-name\n {\"a\" \"Arbuthnot\"\n \"b\" \"Battenburg\"\n \"c\" \"Coutts\"})\n
The following alternative data structure design is very simple and more concise, however it does loose some of the semantic meaning. The position of the names is not defined in terms of the context of the problem.
(def slone-names\n {:a [\"Ally-Pally\" \"Anstruther\" \"Arbuthnot\"]})\n
This next design removes some of the redundancy in defining each letter of the alphabet several times. Apart from less typing and therefore reading by the development team, it also explicitly defines the semantic meaning of each name within the context of this problem.
(def slone-names\n {:a {:first \"Ally-Pally\" :second \"Anstruther\" :third \"Arbuthnot\"}})\n
For extra points you could try and implement a function that generated your sloane name.
"},{"location":"designing-data-structures/modeling-name-generation-map/#creating-the-algorithm-to-construct-your-sloane-name","title":"Creating the algorithm to construct your sloane name","text":" - The first sloane name is chosen from the first character of the first name
- The second sloane name chosen from the first character of the second name
- The third sloane name is chosen from the second character of the second name
You can get the first element of a string by treating it just like a collection. However this returns a character
(first \"Strings also act as collections\")\n
A string can be converted to a keyword, a character cannot
(keyword \"a\")\n
A character can be converted to a string using the str function
(str (first \"Strings also act as collections\"))\n
The keywords need to be the same case, so convert the first character to lower case (which returns a string, so the explicit str function is no longer required.)
(clojure.string/lower-case (first \"Strings also act as collections\"))\n
Putting it all together.
(keyword (clojure.string/lower-case (first \"Strings also act as collections\")))\n
"},{"location":"designing-data-structures/modeling-name-generation-map/#create-a-function-to-calculate-your-sloane-name","title":"Create a function to calculate your sloane name","text":"Putting all this together in a function to generate your sloan name, given your a string with your first and last name.
(defn sloane-name\n \"Given a first and last name as a string, returns your equivalent Sloane name as a string\"\n [name]\n (let [first-name (keyword (clojure.string/lower-case (first (first (clojure.string/split name #\" \")))))\n middle-name (keyword (clojure.string/lower-case (first (second (clojure.string/split name #\" \")))))\n last-name (keyword (clojure.string/lower-case (second (second (clojure.string/split name #\" \")))))]\n (str (get-in slone-names [first-name :first])\n \" \"\n (get-in slone-names [middle-name :second])\n \" \"\n (get-in slone-names [last-name :third]))))\n
Supply a name that will test if the sloane-name
function works
(sloane-name \"Billy Abstainer\")\n;; => \"Bongo Anstruther Battenburg\"\n
"},{"location":"designing-data-structures/with-maps-of-maps/","title":"With Maps of Maps","text":"Note Define a collection of star-wars characters using a map of maps. Each character should have an name that they are typically referred to, along with their fullname and skill
(def star-wars-characters\n {:luke {:fullname \"Luke Skywalker\" :skill \"Targeting Swamp Rats\"}\n :vader {:fullname \"Darth Vader\" :skill \"Crank phone calls\"}\n :jarjar {:fullname \"JarJar Binks\" :skill \"Upsetting a generation of fans\"}})\n
Now we can refer to the characters using keywords. Using the get function we return all the information about Luke
(get star-wars-characters :luke)\n
By wrapping the get function around our first, we can get a specific piece of information about Luke
(get (get star-wars-characters :luke) :fullname)\n
There is also the get-in function that makes the syntax a little easier to read
(get-in star-wars-characters [:luke :fullname])\n(get-in star-wars-characters [:vader :fullname])\n
Or you can get really concise by just talking to the map directly
(star-wars-characters :luke)\n(:fullname (:luke star-wars-characters))\n(:skill (:luke star-wars-characters))\n\n(star-wars-characters :vader)\n(:skill (:vader star-wars-characters))\n(:fullname (:vader star-wars-characters))\n
And finally we can also use the threading macro to minimise our code further
(-> star-wars-characters\n :luke)\n\n(-> star-wars-characters\n :luke\n :fullname)\n\n(-> star-wars-characters\n :luke\n :skill)\n
Note* Create a slightly data structure holding data around several developer events. Each event should have a website address, event type, number of attendees, call for papers.
(def dev-event-details\n {:devoxxuk {:URL \"http://jaxlondon.co.uk\"\n :event-type \"Conference\"\n :number-of-attendees 700\n :call-for-papers \"open\"}\n :hackthetower {:URL \"http://hackthetower.co.uk\"\n :event-type \"hackday\"\n :number-of-attendees 99\n :call-for-papers \"closed\"}})\n
This data structure is just a map, with each key being the unique name of the developer event.
The details of each event (the value to go with the event name key) is itself a map as there are several pieces of data associated with each event name. So we have a map where each value is itself a map.
Call the data structure and see what it evaluates too, it should not be a surprise
dev-event-details\n
We can ask for the value of a specific key, and just that value is returned
(dev-event-details :devoxxuk)\n
In our example, the value returned from the :devoxxuk key is also a map, so we can ask for a specific part of that map value by again using its key
(:URL (dev-event-details :devoxxuk))\n
"},{"location":"designing-data-structures/with-maps/","title":"With Maps","text":"Maps allow you to model data with its contextual meaning. The keys of a map can give the context and the values are the specific data.
Note Define a shopping list of items you want, including how many of each item you want to buy
(def shopping-list\n {\"cat food\" 10\n \"soya milk\" 4\n \"bread\" 1\n \"cheese\" 2})\n
Note Define a star-wars characters, eg. luke skywalker, jarjar binks. The star-wars character should include a name and a skill (it doesn't matter what these are).
Use the 'get' function to return the value of a given key, eg. name. Use keywords to return a given value if you used keywords for the map keys.
In this answer we have defined three different star-wars characters, all using the same map keys.
(def luke {:name \"Luke Skywalker\" :skill \"Targeting Swamp Rats\"})\n(def darth {:name \"Darth Vader\" :skill \"Crank phone calls\"})\n(def jarjar {:name \"JarJar Binks\" :skill \"Upsetting a generation of fans\"})\n
Lets see what the specific skill luke has
(get luke :skill)\n
When you use a keyword, eg. :name, as the key in a map, then that keyword can be used as a function call on the map to return its associated value. Maps can also act as functions too.
(:name luke)\n(luke :name)\n
There are also specific functions that work on maps that give all the keys
of a map and all the values
of that map
(keys luke)\n(vals luke)\n
"},{"location":"designing-data-structures/with-vectors-of-maps/","title":"A Vector of Maps","text":"Vectors are good for holding any information whether that be simple values or other collections.
Maps are good for defining data with semantic meaning, using the keys to express the context of the values.
*Note Define a simple data structure for a collection of stocks in a portfolio. This would contain a collection of stock information, with each stock holding the ticker name, last trading monetary value and opening monetary value.
This is a vector of maps, as there will be one or more company stocks to track. Each map represents the stock information for a company.
(def portfolio [ { :ticker \"CRM\" :lastTrade 233.12 :open 230.66}\n { :ticker \"AAPL\" :lastTrade 203.25 :open 204.50}\n { :ticker \"MSFT\" :lastTrade 29.12 :open 29.08 }\n { :ticker \"ORCL\" :lastTrade 21.90 :open 21.83 }])\n
We can get the value of the whole data structure by referring to it by name
portfolio\n
As the data structure is a vector (ie. array like) then we can ask for a specific element by its position in the array using the nth
function
Lets get the map that is the first element (again as a vector has array-like properties, the first element is referenced by zero)
(nth portfolio 0)\n
The vector has 4 elements, so we can access the last element by referencing the vector using 3
(nth portfolio 3)\n
As portfolio is a collection, also known as a sequence, then we can use a number of functions that provide common ways of getting data from a data structure
(first portfolio)\n(rest portfolio)\n(last portfolio)\n
We can get specific information about the share in our portfolio, or as the keys in each map are defined with Clojure keywords, we can also use the keywords to return the specific values they pair with.
(get (second portfolio) :ticker)\n;; => \"AAPL\"\n\n(:ticker (first portfolio))\n;; => \"CRM\"\n
If we want to get specific share information across the whole portfolio, then we can simply map
the :ticker
keyword over each share in portfolio
(map :ticker portfolio)\n;; => (\"CRM\" \"AAPL\" \"MSFT\" \"ORCL\")\n\n(mapv :ticker portfolio)\n;; => [\"CRM\" \"AAPL\" \"MSFT\" \"ORCL\"]\n
"},{"location":"designing-data-structures/with-vectors-of-vectors/","title":"With Vectors of Vectors","text":"The most frequent use of you will see is in the project.clj
file, where a vector of vectors is used to model the library dependencies for a project
[[org.clojure/clojure \"1.8.0\"]\n [org.clojure/core.match \"0.3.0-alpha4\"]]\n\n[[org.clojure/clojure \"1.6.0\"]\n [ring \"1.4.0-beta2\"]\n [compojure \"1.3.4\"]\n [hiccup \"1.0.5\"]]\n
Fixme Think of an exercise to create a vector of vectors as a data model
"},{"location":"designing-data-structures/with-vectors/","title":"With Vectors","text":"Vectors as the simplest data structure in Clojure to work with. They are very similar to an array in other languages, although they have additional qualities in Clojure.
Vectors
- can be of any length
- are indexed so have fast random access
- can contain any types
- are immutable
Define a data structure for a simple shopping list with any items you would typically want to buy.
(def shopping-list [\"Cerial\" \"Baked Beans\" \"Cat food\" \"Quorn chicken pieces\" ])\n
"},{"location":"development-environments/","title":"Development Environments","text":"This workshop encourages LightTable & Leiningen as the development environment, as they are the easiest tools to set up.
Leiningen is the build automation tool used to manage Clojure projects. It will create projects from templates and run our Clojure environment (REPL).
LightTable is a Clojure aware editor that supports the dynamic workflow of Clojure development in a REPL. LightTable is also written in Clojure (and ClojureScript).
The following pages will show you how to set up LightTable and Leiningen.
"},{"location":"development-environments/java/","title":"Java","text":""},{"location":"development-environments/java/#java-a-host-platform-for-clojure","title":"Java - a host platform for Clojure","text":"You will need to have a Java Runtime Edition (usually installed on most computers by default) to run any Clojure applications. Version 8 is recommended (although version 6 & 7 should work).
To test if you have Java on your computer, open a command line window and run the command
java -version\n
"},{"location":"development-environments/java/#installing-the-java-runtime-edition","title":"Installing the Java Runtime Edition","text":"Download and install the latest Oracle Java SDK (version 1.8 at time of writing).
Alternatively, install OpenJDK or Zulu build of OpenJDK
"},{"location":"development-environments/java/#ubuntu","title":"Ubuntu","text":"The OpenJDK is available as a package on Ubuntu and can be installed via the Ubuntu software center or via the command line:
sudo apt-get install openjdk-8-jre\n
"},{"location":"development-environments/java/#why-is-java-required","title":"Why is Java Required","text":"Clojure was designed as a hosted language, which means it is developed and run on top of Java's Virtual Machine (JVM). However, its not necessary to learn the Java language to use Clojure.
Clojure is compiled into Java bytecode when you evaluate the code. This compilation happens in the background so you dont usually see it happening. For example, if you are using the Clojure REPL then each time you evaluate an expression it is compiled into Java bytecode and then injected into the running REPL and the results are then returned. This all happens pretty instantaneously.
Most of the current Clojure tooling was developed for Clojure on the JVM, for example Leiningen.
As Clojure runs on Java you can also use all the other libraries that run on the Java Virtual machine, regardless of whether those libraries were written in Java, Clojure, Scala, JRuby, jython, Groovy, etc.
"},{"location":"development-environments/leiningen/","title":"Leiningen Build tool","text":"leiningen.org (pronounced line-ing-en) is a very powerful build automation tool for automating Clojure projects. With Leiningen you can:
- Create Clojure Projects with templates
- Define and manage dependencies
- Run an interactive Clojure environment (REPL)
- Run unit tests using Clojure.test
- Run your Clojure application
- Create a deployable Clojure application, as Java Jar file
- Deploy a Clojure library to a remote repository
"},{"location":"development-environments/leiningen/#install-leiningen","title":"Install Leiningen","text":"Download the install script from leiningen.org and run the Leiningen script in a terminal
On Linux and MacOSX, make the script executable first
chmod a+x lein\n./lein\n
Hint I put the lein
script in ~/bin
directory which is part of my operating system execution path ($PATH). To include the ~/bin
directory in the system path, I add the following code to the ~/.profile
file
"},{"location":"development-environments/leiningen/#testing-leiningen-is-working","title":"Testing Leiningen is working","text":"Test that Leiningen is installed with the following command
lein version\n
Output should look similar to:
Leiningen 2.6.1 on Java 9-internal OpenJDK 64-Bit Server VM\n
"},{"location":"development-environments/lighttable/","title":"LightTable","text":"LightTable is a simple development tool that supports Clojure, ClojureScript, JavaScript and Python languages. The tool is open source and written in Clojure & ClojureScript (with a little JavaScript & CSS)
"},{"location":"development-environments/lighttable/#install-lighttable","title":"Install Lighttable","text":"Download lighttable.com and follow the suggested instructions:
MacOSX Install the lighttable.dmg
file just as any other MacOSX package
Linux Extract the contents of the downloaded lighttable file to a suitable directory (/usr/local
or ~/apps
). Add LightTable
to the system $PATH
, or add the following script to the system $PATH
.
Windows Download the windows zip file for LightTable and extract the installer, following the instructions inside the installer.
"},{"location":"development-environments/lighttable/#lighttable-configuration","title":"LightTable configuration","text":"Lighttable configuration is in the file user.behaviours
. Open the user behaviours file, Ctrl-space
and type user behaviors
. When you save the file, Ctrl-s
, changes are applied immediately.
Sample User Behaviours file
Here is a sample of user behaviours file for LightTable
"},{"location":"development-environments/lighttable/#using-lighttable","title":"Using LightTable","text":"LightTable has an online tutorial entitled Getting started with LightTable
I create a project first with Leiningen, open the project directory in the LightTable workspace and open any files I want to work with. I then connect the open editor window for the file by pressing Ctrl-Enter
at the end of an expression.
Hint my approach is documented in the quick demo section of my Clojure & LightTable slides from JAXLondon 2013.
"},{"location":"development-environments/other-tools/","title":"Other Development tools for Clojure","text":"There are several development tools you can use to support your Clojure development.
My current choice of development environment is Spacemacs, a feature rich configuration for Emacs. See my article on Spacemacs for Clojure development
Some common setups I have seen in use for Clojure development are:
- Modern - LightTable, Leiningen, Git
- Modern Classic - Spacemacs with Clojure layer, Leiningen, magit
- Classic - Emacs with Cider, Leiningen, magit
- Java (IntelliJ) - Cursive Clojure
- Java (Eclipse) - Counterclockwise documentation site
- Ubiquitous - Vim, nailgun, Leiningen, Git
- Simple - Nightcode, Leiningen, Git
- Lightweight - Atom, Protorepl, Leiningen, Git
There may be many more variations, however you should find a development environment with at minimum the following features:
- starting & using a REPL, with in-line evaluation
- syntax highlighting & coloured brackets (eg. rainbow-delimiters in Emacs)
- autocomplete of names (functions, symbols, keywords, etc)
- snippets / templates
"},{"location":"development-environments/other-tools/#tools-for-developers-with-a-java-background","title":"Tools for developers with a Java background","text":"Clojure runs on the Java Virtual Machine so its not surprising that there is good support for Clojure in the major Java IDEs.
"},{"location":"development-environments/other-tools/#eclipse","title":"Eclipse","text":"Counterclockwise is an Eclipse IDE plugin to provide an integrated development environment for Clojure. Take a look at the Counterclockwise documentation site for installation instructions
"},{"location":"development-environments/other-tools/#intellij","title":"IntelliJ","text":"Cursive is a Clojure IDE that aims to understands your code. Advanced structural editing, refactor, VCS integration and much more, all out of the box. It is currently a standalone tool, although will eventually become an IntelliJ plugin.
La Clojure is a plugin for IntelliJ IDEA. Provides Clojure language support: syntax and error highlighting, completion, navigation and refactor.
"},{"location":"development-environments/other-tools/#netbeans","title":"Netbeans","text":"Netbeans did have great support for Clojure, but unfortunately at the time of writing the Clojure plugin has been unmaintained for so long it is not a viable tool to use for Clojure development.
"},{"location":"games/","title":"Writing Games with Clojure","text":"Games are driven by events and require state to be managed, so are a good way to explore how to manage state with immutable values.
For games in Clojure the events are simply function calls and we prefer to pass the state around rather than have a central mutable container for our state.
This section will contain several games that have been built using a functional approach with immutable data structures.
- TicTacToe on the command line
"},{"location":"games/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"Pull requests are welcome
"},{"location":"games/#hintgames-in-clojurescript","title":"Hint::Games in ClojureScript","text":"There is a section on games in the Practicalli ClojureScript book, including a TicTacToe game using Reagent (react.js style library) and Scalable Vector Graphics (SVG).
"},{"location":"games/tictactoe-cli/","title":"TicTacToe on the command line","text":"Tic-tac-toe is a paper-and-pencil game for two players, X and O, who take turns marking the spaces in a 3\u00d73 grid. The player who succeeds in placing three of their marks in a horizontal, vertical, or diagonal row wins the game
The code for this section is published on GitHub at: practicalli/tictactoe-cli
A TicTacToe game that you run on the command line. The game takes input from a human player and the program is the second player.
Output from the game appears in the REPL
Current board:\n1 | 2 | 3\n---------\n4 | 5 | 6\n---------\n7 | 8 | 9\nX: Select your move (press a number between 1 and 9 then press enter)\nCurrent board:\nX | 2 | 3\n---------\n4 | 5 | 6\n---------\n7 | 8 | 9\nO: Select your move (press a number between 1 and 9 then press enter)\nCurrent board:\nX | O | 3\n---------\n4 | 5 | 6\n---------\n7 | 8 | 9\nX: Select your move (press a number between 1 and 9 then press enter)\nCurrent board:\nX | O | X\n---------\n4 | 5 | 6\n---------\n7 | 8 | 9\nO: Select your move (press a number between 1 and 9 then press enter)\nCurrent board:\nX | O | X\n---------\nO | 5 | 6\n---------\n7 | 8 | 9\nX: Select your move (press a number between 1 and 9 then press enter)\nCurrent board:\nX | O | X\n---------\nO | X | 6\n---------\n7 | 8 | 9\nO: Select your move (press a number between 1 and 9 then press enter)\nCurrent board:\nX | O | X\n---------\nO | X | O\n---------\n7 | 8 | 9\nX: Select your move (press a number between 1 and 9 then press enter)\nCurrent board:\nX | O | X\n---------\nO | X | O\n---------\nX | 8 | 9\nPlayer X wins!\n
"},{"location":"games/tictactoe-cli/#references","title":"References","text":" - TicTacToe game created by Brian Will.
"},{"location":"games/tictactoe-cli/create-project/","title":"Create a Clojure project","text":"Create a project for our game.
{% tabs deps=\"deps.edn projects\", lein=\"Leiningnen projects\" %}
{% content \"deps\" %} Create a new project using clj-new
alias, found in Practicalli Clojure CLI Config
clojure -M:new practicalli/tictactoe-cli\n
Open the project in a Clojure aware editor or run a rebel REPL
clojure -M:repl/rebel\n
Once the rebel REPL is running, load the project and change to the main namespace
(require 'practicalli/tictactoe-cli)\n\n(in-ns 'practicalli/tictactoe-cli)\n
{% content \"lein\" %} The default Leiningen template is suitable fine for the project as no additional libraries are used.
lein new tictactoe-cli\n
git clone https://github.com/practicalli/tictactoe-cli.git\n
"},{"location":"games/tictactoe-cli/create-project/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"games/tictactoe-cli/create-project/#hintalternatively-clone-the-github-repository","title":"Hint::Alternatively clone the github repository","text":"You can also clone the tictactoe-cli game from GitHub
"},{"location":"games/tictactoe-cli/create-project/#updating-clojure-version-and-licence","title":"Updating Clojure version and licence","text":"In the project.clj
file I have updated Clojure to version 1.10.0 and changed the licence to be the more open Creative Commons license.
(defproject tictactoe-cli \"0.1.0-SNAPSHOT\"\n :description \"TicTacToe game played on the command line\"\n :url \"https://github.com/practicalli/tictactoe-cli\"\n :license {:name \"Creative Commons Attribution Share-Alike 4.0 International\"\n :url \"https://creativecommons.org\"}\n :dependencies [[org.clojure/clojure \"1.10.0\"]])\n
I also removed the license
file and added a brief description of the project to the README.md
file
{% endtabs %}
"},{"location":"install/","title":"Install Clojure","text":"Clojure CLI provides the foundation for Clojure development, providing a declarative approach to:
- Run Clojure programs and tools
- Run a REPL process (Read-Eval-Print Loop) and provides a basic interactive terminal UI
- Manage packaged dependencies from Maven (jars) and use Git repositories as dependencies
Practicalli Clojure Config community tools
Practicalli Clojure CLI Config is a user configuration providing aliases for a wide range of community tools which extends the features of Clojure CLI. The aliases include tools to create, develop, build and deploy Clojure code. Aliases are used heavily in the Practicalli books.
If the Practicalli Clojure CLI config is not used, review the deps.edn
file from the GitHub repository and add relevant aliases definitions to your own Clojure CLI configuration.
"},{"location":"install/#pre-requisites","title":"\"Pre-requisites\"","text":"A Java Virtual Machine hosts Clojure. Java 21 is the current Long Term Support version providing a stable platform to run Clojure
"},{"location":"install/#additional-tools","title":"Additional tools","text":"Clojure connected editor
A Clojure connected editor provides the most effective way to write and maintain Clojure projects. The editor connects to (or starts) a Clojure REPL and code can be evaluated as its typed, showing the results instantly in line with the code.
Clojure LSP server generates static analysis of code which editors can surface as code diagnostics. Analysis supports effective code navigate and refactor tools. Practicalli Clojure LSP config configures
Data Inspectors
Data inspectors visualize results of Clojure code evaluation and allow navigation of nested data or paging through large data sets.
Portal is highly recommended data inspector and included in projects generated with Practicalli Project Templates.
Alternative development tools Leiningen is the long-standing development tool for Clojure. All the code examples in this book should work with Leiningen when a correctly configured project.clj
file is created which includes all the necessary library dependencies. Libraries included via aliases should be added as either :dev-dependencies
or :aliases
in the Leiningen project.clj
file.
"},{"location":"install/clojure-cli/","title":"Install Clojure CLI","text":"Clojure CLI is a command line tool for running a Clojure REPL, project or tool.
Clojure CLI automatically downloads required library dependencies, including the Clojure Standard library.
Clojure distributed as a library Clojure is distributed as a library (.jar
Java ARchive) via Maven Central.
A deps.edn
file specifies the version of Clojure to be used with a project.
:deps {org.clojure/clojure {:mvn/version \"1.12.0\"}}\n
The Clojure CLI tool provides a default Clojure library version if not specified in the project or user deps.edn
files.
Clojure releases
Practicalli Clojure CLI Config extends the Clojure CLI with a range of development tools as well as configuration for Clojure LSP and cljstyle code format tool.
LinuxHomebrewWindows Use the Linux script installer from Clojure.org - Getting Started to install or update to the latest stable release
curl -L -O https://github.com/clojure/brew-install/releases/latest/download/linux-install.sh && \\\nchmod +x linux-install.sh && \\\nsudo ./linux-install.sh\n
The installation creates /usr/local/bin/clojure
, /usr/local/bin/clj
wrapper and /usr/local/lib/clojure
directory.
Use alternative location - unattended install --prefix
option specifies an alternative lolcation for the Clojure CLI install.
When permissions are not available or for automating the install without password prompt, use a local user specific install, e.g.
curl -L -O https://github.com/clojure/brew-install/releases/latest/download/linux-install.sh && \\\nchmod +x linux-install.sh && \\\n./linux-install.sh --prefix $HOME/.local/\n
Include version number for specific release Each Clojure CLI version is a number that represents the version of Clojure used and the build version of the Clojure CLI tool, e.g. 1.11.1.1413
.
Clojure CLI Releases page
Include the version in the script name for repeatable environments, e.g. in Dockerfile configuration and Continuous Integraion workflows. Clojure CLI install specific version
curl -L -O https://github.com/clojure/brew-install/releases/1.11.1.1413/download/linux-install.sh && \\\nchmod +x linux-install-1.11.1.1413.sh\nsudo ./linux-install-1.11.1.1413.sh\n
Practically recommends setting XDG_CONFIG_HOME
to the .config
directory, to avoid creating another dot directory in the root of the user account. Add the following to ~/.bashrc
for the bash shell or ~/.zshenv
for Zsh.
export XDG_CONFIG_HOME=\"$HOME/.config\"\n
Use the Homebrew command with the clojure/tools tap, as defined in the Clojure.org Getting started guide
brew install clojure/tools/clojure\n
Use Homebrew to update an install of Clojure CLI to the latest release
brew upgrade clojure/tools/clojure\n
Homebrew on Linux or Windows with WSL
For Windows 10 use Windows Subsystem for Linux and Windows Terminal are recommended if you have administrative privileges and are comfortable using a Unix system on the command line.
Alternatively install scoop.sh, a command line installer for windows. Powershell 5 or greater is required. Follow the scoop-clojure getting started guide, summarized here:
Open \"Windows PowerShell\" and enter the following commands to configure the shell:
iwr -useb get.scoop.sh | iex\nSet-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force\n
Then in the same PowerShell window, install the Clojure related tools using the following commands: scoop bucket add extras\nscoop bucket add java\nscoop bucket add scoop-clojure https://github.com/littleli/scoop-clojure\nscoop install git 7zip pshazz temurin-lts-jdk clj-deps leiningen clj-kondo vscode coreutils windows-terminal\n
Reference: Clojure CLI Install - Clojure.org Getting Started - official guide
"},{"location":"install/clojure-cli/#practicalli-clojure-cli-config","title":"Practicalli Clojure CLI Config","text":"Add a wide range of community tools to extend the capabilities of Clojure CLI via the aliases.
Clone Practicalli Clojure CLI Config GitHub repository, first removing the $XDG_CONFIG_HOME/clojure
and $HOME/.clojure
directory if they exist.
User configuration locations If XDG_CONFIG_HOME
environment variable is set, then the user configuration is $XDG_CONFIG_HOME/clojure/deps.edn
Otherwise the user configuration is $HOME/.clojure/deps.edn
.
CLJ_CONFIG
environment variable can be used to set a custom location, overriding any other location.
Practicalli recommends FreeDesktop XDG location
Practically recommends setting XDG_CONFIG_HOME
to the .config
directory to simplify versioning of configuration.
Configure ~/.bashrc
for the bash shell Bash .bashrc file
export XDG_CONFIG_HOME=\"$HOME/.config\"\n
Configure ~/.zshenv
for Zsh
# Set XDG_CONFIG_HOME for clean management of configuration files\nexport XDG_CONFIG_HOME=\"${XDG_CONFIG_HOME:=$HOME/.config}\"\nexport XDG_DATA_HOME=\"${XDG_DATA_HOME:=$HOME/.local/share}\"\nexport XDG_CACHE_HOME=\"${XDG_CACHE_HOME:=$HOME/.cache}\"\nexport ZDOTDIR=\"${ZDOTDIR:=$XDG_CONFIG_HOME/zsh}\"\n
Free Desktop XDG CONFIGClassic Config If XDG_CONFIG_HOME
environment variable is set, clone the repository to $XDG_CONFIG_HOME/clojure
Via SSH
git clone git@github.com:practicalli/clojure-cli-config.git $XDG_CONFIG_HOME/clojure\n
Via HTTPS:
git clone https://github.com/practicalli/clojure-cli-config.git $XDG_CONFIG_HOME/clojure\n
Clojure CLI will look for its configuration in $HOME/.clojure
directory if $XDG_CONFIG_HOME
and CLJ_CONFIG
environment variables not set. Via SSH ```shell git clone git@github.com:practicalli/clojure-cli-config.git $HOME/.clojure
```
Via HTTPS\n```shell\ngit clone https://github.com/practicalli/clojure-cli-config.git $HOME/.clojure\n```\n
"},{"location":"install/clojure-cli/#check-configuration","title":"Check Configuration","text":"clojure -Sdescribe
shows the version of Clojure CLI installed and configuration locations used.
clojure -Sdescribe\n
The output of the command includes the version of Clojure CLI in the :version
key
{:version \"1.11.1.1386\"\n :config-files [\"/usr/local/lib/clojure/deps.edn\" \"/home/practicalli/.config/clojure/deps.edn\" ]\n :config-user \"/home/practicalli/.config/clojure/deps.edn\"\n :config-project \"deps.edn\"\n :install-dir \"/usr/local/lib/clojure\"\n :config-dir \"/home/practicalli/.config/clojure\"\n :cache-dir \"/home/practicalli/.cache/clojure\"\n :force false\n :repro false\n :main-aliases \"\"\n :repl-aliases \"\"}\n
clojure -Sversion
will shows the version of Clojure CLI being when the clojure
command is used to run a REPL or other Clojure command.
"},{"location":"install/clojure-cli/#optional-rlwrap-readline","title":"Optional rlwrap readline","text":"The rlwrap
binary is a basic readline tool that provides a history of commands entered into a terminal UI when running a Clojure REPL with the clj
wrapper script.
Pressing the Up and Down keys will scroll through the code previously entered in the REPL.
rlwrap
is available with most Linux systems. Look for install instructions by searching for rlwrap in a web browser or build from source from the rlwrap GitHub repository.
Use Rebel Readline for a rich terminal UI experience
rebel readline is an advanced readline tool providing auto-completion, documentation, signature help and multi-line editing, all within a terminal UI
Rebel is a much richer experience than the clj
wrapper with rlwrap
. Rebel should not be used with clj
.
Rebel Readline is part of the Practicalli Clojure CLI config.
"},{"location":"install/java/","title":"Java Host","text":"Java is a host platform for Clojure, on which Clojure projects and tools run. Java provides a virtual machine which runs the bytecode generated when Clojure code is compiled.
Java virtual machine includes a Just In Time (JIT) compiler that optimises running of bytecode.
Practicalli recommends OpenJDK version 21
"},{"location":"install/java/#install-java","title":"Install Java","text":"Check to see if there is an appropriate version of Java already installed.
Open a terminal and run the command
java --version\n
If Java is installed and on the execution path, the version infomation is returned
Debian PackagesHomebrewWindowsManual Install Java development kit (JDK) using the apt
package manager (login as su -
or prefix the command with sudo
)
apt install openjdk-21-jdk\n
Check available versions of OpenJDK Long terms support versions should include OpenJDK 17 and may include OpenJDK 21. Check versions available via the apt
package management tool.
apt search --names-only openjdk\n
Optionally include Java docs and sources Install the openjdk-21-doc
locally to provide Javadocs to support Java Interop code.
Install the openjdk-21-source
package to support navigation of Java Object and Method source code, especially useful when using Java Interoperability from within Clojure code.
sudo apt install openjdk-21-doc openjdk-21-source\n
Practicalli Clojure CLI Config provides the :src/java17
alias to include the Java sources in the classpath when running a REPL.
If openjdk-21-jdk
package is not available, add the Ubuntu OpenJDK personal package archive
sudo add-apt-repository ppa:openjdk-r/ppa\nsudo apt-get update\n
When multiple versions of Java are installed, set the version using the update-alternatives
command in a terminal
sudo update-alternatives --config java\n
Available java versions will be listed. Enter the list number for the version you wish to use.
Using Homebrew, run the following command in a terminal to install Java 17:
brew install openjdk@21\n
Switching between Java versions More than one version of Java can be installed on MacOSX. Set the Java version by opening a terminal and using one of the following commands
Show the Java versions installed
/usr/libexec/java_home -V\n
Switch to Java version 21
export JAVA_HOME=$(/usr/libexec/java_home -v 21)\n
Alternatively, install JEnv Java version manager
For Windows 10 use Windows Subsystem for Linux and Windows Terminal are recommended if you have administrative privileges and are happy to use a Unix system on the command line.
Alternatively use scoop.sh, a command line installer for windows. Powershell 5 or greater is required.
Follow the scoop-clojure install instructions, summarized here:
scoop bucket add java\nscoop install temurin-lts-jdk\n
scoop can also be used to install clojure
If neither Scoop or Windows Subsystem for Linux work, try the Chocolatey package manager. Install the Java Runtime (JRE) using the following command in a command line window
choco install javaruntime\n
If Chocolatey does not work, then try the manual Java install.
Download OpenJDK from Adoptium - pre-build OpenJDK binaries freely available for multiple operating systems.
Run the file once downloaded and follow the install instructions.
"},{"location":"install/java/#multiple-versions-of-java","title":"Multiple versions of Java","text":" jenv provides a simple way to switch between multiple installed versions of Java. jenv can be used to set the java version globally, for the current shell or for a specific project by adding .java-version
file containing the Java version number in the root of the project.
"},{"location":"install/java/#a-little-java-knowledge","title":"A little Java Knowledge","text":"Very little knowledge of the Java language or the Java Virtual Machine is required.
It is quite simple to call Java methods from Clojure, although there are a wealth of functions and libraries provided by Clojure and its community to minimise the need for Java Interoperability.
Reading stack traces may benefit from some Java experience, although its usually the first couple of lines in a stack trace that describe the issue.
Clojure uses its own build tools (Leiningen, Clojure CLI tools) and so Java build tool knowledge is not required.
When libraries are added to a project, they are downloaded to the $HOME/.m2
directory. This is the default Maven cache used by all JVM libraries.
clojure -Spom
will generate a Maven pom.xml file used for deployment. Understanding of a minimal Maven POM (pom.xml) file is useful when managing issues with packaging and deployment.
Maven in 5 minutes
The Java Virtual Machine is highly optimised and does not usually require any options to enhance its performance. The most likely configuration to supply to the JVM are to manage the amount of memory assigned, specifically for resource constrained environments.
"},{"location":"introduction/clojure-in-15-minutes/","title":"Clojure in 15 minutes","text":"A quick tour of the Clojure syntax and common functions, which is so terse you can read through this page in around 15 minutes and have a basic understanding of the language.
Try the code out in the REPL
Start a Clojure REPL or use a Clojure aware editor connected to a REPL and experiment with these code examples.
Using the REPL provides instant feedback on each expression as they are evaluated, greatly increasing your understanding.
"},{"location":"introduction/clojure-in-15-minutes/#comments","title":"Comments","text":";;
two semi-colons for a line comment, ;
single semi-colon to comment the rest of the line
#_
comment reader macro to comment out the next form
(comment ,,,)
form to comment all the containing forms, useful to separate experimental and established code in a namespace.
"},{"location":"introduction/clojure-in-15-minutes/#clojure-expressions","title":"Clojure expressions","text":"Clojure is mostly written with \"expressions\", a lists of elements inside parentheses, ()
, separated by space characters.
Clojure evaluates the first element in an expression as a function call. Additional elements in the expression are passed as value arguments to the called function.
Function call with value and expression as arguments
(+ 2007 (* 1 16))\n
Functions can be passed as an argument
(map inc (range 0 99))\n
"},{"location":"introduction/clojure-in-15-minutes/#organising-clojure","title":"Organising Clojure","text":"Clojure code is organised into one or more namespaces. The namespace represents the directory path and file name that contains the code of the particular namespace.
A company name or community repository name is often used making the namespace unique and easier to share & reuse.
ns form returns nil value The (ns namespace.,,,)
expression returns a nil
value, as its work is done behind the scenes.
All Clojure functions must return a value and nil
is a value that means 'no value'.
Define a namespace
src/practicalli/game_board.clj(ns practicalli.game-board)\n
Define a longer namespace
src/com/company/product/component_name.clj(ns com.company.product.component-name)\n
Namespaces use dash, directory and file names use underscore Clojure uses kebab-case
for names (common in Lisp dialects)
Unfortunately the Java Virtual Machine that hosts Clojure does not support dash, -
, in file and directory names, so an underscore, -
, character is used
"},{"location":"introduction/clojure-in-15-minutes/#string-manipulation","title":"String manipulation","text":"The str
function creates a new string from all the arguments passed
Combine strings into a single string value
(str \"Hello\" \" \" \"World\")\n
\"Hello World\"
is returned from evaluating the expression.
clojure.string library for manipulating strings
clojure.string
library functions manipulate values and return string values (other clojure.core functions my return characters as results, e.g. map
)
"},{"location":"introduction/clojure-in-15-minutes/#math-truth-prefix-notation","title":"Math, Truth & prefix notation","text":"Functions use prefix notation, so you can do math with multiple values very easily
Prefix syntax takes multiple arguments
(+ 1 2 3 5 7 9 12) ; => 40\n
Math in Clojure is very precise, no need for operator precedence rules (as there are no operators)
Nesting forms defined a very precise calculation
Parentheses used instead of operator preceedence rules
(* 1 2 (- 24 (* 7 3)))\n
6
is returned as the value. Nested expressions are typically read inside out. (* 7 3)
is 21
, giving (- 24 21)
expression resulting in 3
. Finally the expression becomes (* 1 2 3)
, resulting in a value of 6
Maintain precision for calculations using a Ratio type in Clojure
Clojure Ratio value
(/ 27 7) ; => 27/7\n
22/7
is returned as the value, rather than a floating point value (double) which may loose some precision due to rounding.
"},{"location":"introduction/clojure-in-15-minutes/#equality","title":"Equality","text":"=
function provides a test for equality
Equal values return a boolean true
(= 1 1) ; => true\n
Unequals values return a boolean false
(= 2 1) ; => false\n
true
and false
are Boolean values and can be used literally in Clojure.
"},{"location":"introduction/clojure-in-15-minutes/#predicates","title":"Predicates","text":"A predicate is a function that returns a boolean true
or false
value and by convention the function name ends in ?
, e.g. true?
, false?
, seq?
, even?
, uuid?
.
and
& or
functions can be used to chain the results of predicates together for more interesting conditional tests.
All predicates are true, returning true
(and (true? true) (not false)) ; => true\n
One of the predicates or values is true
(or nil (not= true false) (true? (complement true?)) ) ; => true\n
Truthy and Falsy values in Clojure
false
boolean value and nil
value are considered false in Clojure.
All other values are consider true.
Clojure Standard Library Predicate Functions
"},{"location":"introduction/clojure-in-15-minutes/#collections-sequences","title":"Collections & Sequences","text":"The most common data collections in Clojure:
(1 2 \"three\")
or (list 1 2 \"three\")
- a list of values read from start to end (sequential access) [1 2 \"three\"]
or (list 1 2 \"three\")
- a vector of values with index (random access) {:key \"value\"}
or (hash-map :key \"value\")
- a hash-map with zero or more key value pairs (associative relation) #{1 2 \"three\"}
or (set 1 2 \"three\")
- a unique set of values
A list ()
is evaluated as a function call. The first element of the list the name of the function to call and additional values are arguments to the function.
The '
quote function informs the Clojure reader to treat the list as data only.
A quoted list is treated as data
'(1 2 3) ; => (1 2 3)\n
Lists and vectors are collections
(and (coll? '(1 2 3)) (coll? [1 2 3])) ; => true\n
Only lists are sequences
(seq? '(1 2 3)) ; => true\n(seq? [1 2 3]) ; => false\n
Sequences are an interface for logical lists, which can be lazy. \"Lazy\" means that a sequence of values are not evaluated until accessed.
A lazy sequence enables the use of large or even an infinite series, like so:
Lazy sequences
(range) ; => (0 1 2 3 4 ...) - an infinite series\n(take 4 (range)) ; (0 1 2 3) - lazyily evaluate range and stop when enough values are taken\n
Use cons to add an item to the beginning of a list or vector
(cons 4 [1 2 3]) ; => (4 1 2 3)\n(cons 4 '(1 2 3)) ; => (4 1 2 3)\n
Use conj to add an item relative to the type of collection, to the beginning of a list or the end of a vector
(conj [1 2 3] 4) ; => [1 2 3 4]\n(conj '(1 2 3) 4) ; => (4 1 2 3)\n
Use concat to add lists or vectors together
(concat [1 2] '(3 4)) ; => (1 2 3 4)\n
Use filter, map to interact with collections
(map inc [1 2 3]) ; => (2 3 4)\n(filter even? [1 2 3]) ; => (2)\n
Use reduce to reduce them
(reduce + [1 2 3 4])\n; = (+ (+ (+ 1 2) 3) 4)\n; => 10\n
Reduce can take an initial-value argument too
(reduce conj [] '(3 2 1))\n; => [3 2 1]\n
Equivalent of (conj (conj (conj [] 3) 2) 1)
"},{"location":"introduction/clojure-in-15-minutes/#annonymous-functions","title":"Annonymous Functions","text":"Use fn
to create new functions that defines some behaviour. fn
is referred to as an anonymous fuction as it has no external name to be referenced by and must be called within a list form.
(fn hello [] \"Hello World\")\n
Wrap a (fn ,,,)
form in parens to call it and return the result.
Call an anonymous function
((fn hello [] \"Hello World\")) ; => \"Hello World\"\n
Normally the anonymous function is used inline with other code
Use anonymous function within other code
(map (fn [x] (* x 2)) [1 2 3 4 [1 2 3 4 5]5])\n
Make the anonymous function reusable by binding it to a shared name (var
) using def
.
The var
name bound to the function can now be called anywhere in the namespace.
As def
creates a var
(variable) name, the developer can changed the expression the name is bound to and re-evaluated to use the changed behaviour.
Bind a name to the anonymous function
(def hello-world\n (fn hello [] \"Hello World\"))\n
Evaluate annonymous function by evaluating its name
hello-world\n
NOTE: hello-world
is a name and not a function call, so parentheses are not required.
"},{"location":"introduction/clojure-in-15-minutes/#shared-functions","title":"Shared Functions","text":"It is more common to use the defn
macro to define a function. This is the same as defining the fn
function and the def
name within the same expression
Define a function with defn macro
(defn hello-world\n \"I am a humble doc-string, please describe the function purpose\"\n []\n \"Hello World\")\n
#'user/hello-world
is the value returned from evaluating the expression, showing the fully qualified name of the function. Note: the fully qualified name will be different when defined in a differnt namespace than user
.
A defn
function has the scope of the current namespace, so can be called anywhere in the namespace or in a namepace that has used require
to include this namespace.
Call a function
(hello-world)\n
The []
vector is used to define the argument names for the function. There can be zero or more arguments.
Call function with arguments
(defn hello [name]\n (str \"Hello \" name))\n
The correct number of arguments must be used when calling a function, or an error will be returned.
Call function with arguments
(hello \"Steve\") ; => \"Hello Steve\"\n
Pass a hash-map as an argument Simplify the design of a function signature by passing all arguments as a hash-map.
(defn data-processing\n [data]\n (let [body (get data :body)])\n (transform body))\n
Associative Destructuring can be used to automatically create local variables from the desired keys contained in the map, giving access to the value of each key. (defn data-processing\n [{:keys [body]}]\n (transform body))\n
Clojure supports multi-variadic functions, allowing one function definition to respond to a function call with different number of arguments. This provides a simple form of polymorphism based on the number of arguments.
(defn hello-polly\n ([] \"Hello World\") ; (1)!\n ([name] (str \"Hello \" name))) ; (2)!\n
-
Call hello-polly
with one argument
(hello-polly \"Jake\") ; => \"Hello Jake\"\n
-
Call hello-polly
with zero arguments
(hello-polly) ; => \"Hello World\"\n
Functions can pack extra arguments up in a seq for you
(defn count-args [& args]\n (str \"You passed \" (count args) \" args: \" args))\n(count-args 1 2 3) ; => \"You passed 3 args: (1 2 3)\"\n
You can mix regular and packed arguments
(defn hello-count [name & args]\n (str \"Hello \" name \", you passed \" (count args) \" extra args\"))\n(hello-count \"Finn\" 1 2 3)\n; => \"Hello Finn, you passed 3 extra args\"\n
"},{"location":"introduction/clojure-in-15-minutes/#hash-map-collections","title":"Hash-map collections","text":"(class {:a 1 :b 2 :c 3}) ; => clojure.lang.PersistentArrayMap\n
Keywords are like strings with some efficiency bonuses
(class :a) ; => clojure.lang.Keyword\n
Maps can use any type as a key, but usually keywords are best
(def stringmap (hash-map \"a\" 1, \"b\" 2, \"c\" 3))\nstringmap ; => {\"a\" 1, \"b\" 2, \"c\" 3}\n\n(def keymap (hash-map :a 1 :b 2 :c 3))\nkeymap ; => {:a 1, :c 3, :b 2} (order is not guaranteed)\n
Commas are whitespace commas are always treated as whitespace and are ignored by the Clojure reader
Retrieve a value from a map by calling it as a function
(stringmap \"a\") ; => 1\n(keymap :a) ; => 1\n
Keywords can be used to retrieve their value from a map. Strings cannot be used.
(:b keymap) ; => 2\n\n(\"a\" stringmap)\n; => Exception: java.lang.String cannot be cast to clojure.lang.IFn\n
Retrieving a non-present value returns nil
(stringmap \"d\") ; => nil\n
Use assoc to add new keys to hash-maps
(assoc keymap :d 4) ; => {:a 1, :b 2, :c 3, :d 4}\n
But remember, clojure types are immutable!
keymap ; => {:a 1, :b 2, :c 3}\n
Use dissoc to remove keys
(dissoc keymap :a :b) ; => {:c 3}\n
"},{"location":"introduction/clojure-in-15-minutes/#sets","title":"Sets","text":"(class #{1 2 3}) ; => clojure.lang.PersistentHashSet\n(set [1 2 3 1 2 3 3 2 1 3 2 1]) ; => #{1 2 3}\n
Add a member with conj
(conj #{1 2 3} 4) ; => #{1 2 3 4}\n
Remove one with disj
(disj #{1 2 3} 1) ; => #{2 3}\n````\n\nTest for existence by using the set as a function:\n\n```clojure\n(#{1 2 3} 1) ; => 1\n(#{1 2 3} 4) ; => nil\n
There are more functions in the clojure.sets namespace.
"},{"location":"introduction/clojure-in-15-minutes/#useful-forms","title":"Useful forms","text":"Logic constructs in clojure are just macros, and look like everything else
(if false \"a\" \"b\") ; => \"b\"\n(if false \"a\") ; => nil\n
Use let to create temporary bindings
(let [a 1 b 2]\n (> a b)) ; => false\n
Group statements together with do
(do\n (print \"Hello\")\n \"World\") ; => \"World\" (prints \"Hello\")\n
Functions have an implicit do
(defn print-and-say-hello [name]\n (print \"Saying hello to \" name)\n (str \"Hello \" name))\n(print-and-say-hello \"Jeff\") ;=> \"Hello Jeff\" (prints \"Saying hello to Jeff\")\n
So does let
(let [name \"Urkel\"]\n (print \"Saying hello to \" name)\n (str \"Hello \" name)) ; => \"Hello Urkel\" (prints \"Saying hello to Urkel\")\n
"},{"location":"introduction/clojure-in-15-minutes/#namespaces-and-libraries","title":"Namespaces and Libraries","text":"Namespaces are used to organise code into logical groups. The top of each Clojure file has an ns
form that defines the namespace name. The domain part of the namespace name is typically the organisation or community name (e.g. GitHub user/organisation)
(ns domain.namespace-name)\n
All Practicalli projects have namespace domains of practicalli
(ns practicalli.service-name)\n
require
allows code from one namespace to be accessed from another namespace, either from a the same Clojure project or from a library added to the project classpath.
The :as
directive with require
is used to specify an alias name, a short-hand for the full library name
Or :refer [function-name var-name]
can be used to specify specific functions and data (vars) that are available directly
A required directive is typically added to a namespace form
(ns practicalli.service-name\n (require [clojure.set :as set]))\n
The functions from clojure.set can be used via the alias name, rather than the fully qualified name, i.e. clojure.set/intersection
(set/intersection #{1 2 3} #{2 3 4}) ; => #{2 3}\n(set/difference #{1 2 3} #{2 3 4}) ; => #{1}\n
:require
directive can be used to include multiple library namespaces
(ns test\n (:require\n [clojure.string :as string]\n [clojure.set :as set]))\n
require
can be used by itself, usually within a rich code block
(comment\n (require 'clojure.set :as set))\n
"},{"location":"introduction/clojure-in-15-minutes/#strong-dynamic-types","title":"Strong Dynamic Types","text":"Clojure is strongly typed, so everything is a type in Clojure.
Clojure is dynamically typed, so Clojure infers the type. A type does not need to be specified in the code, making the code simpler and more concise.
Clojure is a hosted language and uses the type system of the platform it runs upon. For example, Clojure uses Java object types for booleans, strings and numbers under the covers.
Use class
or type
function to inspect the type of some code in Clojure.
(type 1) ; Integer literals are java.lang.Long by default\n(type 1.); Float literals are java.lang.Double\n(type \"\"); Strings always double-quoted, and are java.lang.String\n(type false) ; Booleans are java.lang.Boolean\n(type nil); The \"null\" value is called nil\n
Vectors and Lists are java classes too!
(type [1 2 3]); => clojure.lang.PersistentVector\n(type '(1 2 3)); => clojure.lang.PersistentList\n
Type hints
Type hints can be used to avoid reflection look-ups where performace critical issues have been identified. Type hints are not required in general. Clojure Type Hints
"},{"location":"introduction/clojure-in-15-minutes/#java-interop","title":"Java Interop","text":"Java has a huge and useful standard library, so you'll want to learn how to get at it.
Use import to load a java package
(import java.util.Date)\n
Or import from a java package name
(ns test\n (:import\n java.util.Date\n java.util.Calendar))\n
Use the class name with a \".\" at the end to make a new instance
(Date.) ; <a date object>\n
Use .
to call methods. Or, use the \".method\" shortcut
(. (Date.) getTime) ; <a timestamp>\n(.getTime (Date.)) ; exactly the same thing.\n
Use / to call static methods
(System/currentTimeMillis) ; <a timestamp> (system is always present)\n
Use doto to make dealing with (mutable) classes more tolerable
(import java.util.Calendar)\n(doto (Calendar/getInstance)\n (.set 2000 1 1 0 0 0)\n .getTime) ; => A Date. set to 2000-01-01 00:00:00\n
"},{"location":"introduction/contributing/","title":"Contributing to Practicalli","text":"By submitting content ideas and corrections you are agreeing they can be used in any work by Practicalli under the Creative Commons Attribution ShareAlike 4.0 International license. Attribution will be detailed via GitHub contributors.
Please raise an issue before creating a pull request
Raising an issue or post on the #practicalli channel of Clojurians Slack community avoids disappointment if the contribution would not be accepted and saves time for all.
Practicalli books are written in markdown and use MkDocs to generate the published website via a GitHub workflow. MkDocs can also run a local server using the make docs
target from the Makefile
All content and interaction with any persons or systems must be done so with respect and within the Practicalli Code of Conduct.
"},{"location":"introduction/contributing/#book-status","title":"Book status","text":""},{"location":"introduction/contributing/#submit-and-issue-or-idea","title":"Submit and issue or idea","text":"If something doesnt seem quite right or something is missing from the book, please raise an issue via the GitHub repository explaining in as much detail as you can.
Raising an issue before creating a pull request will save you and the maintainer time.
Alternatively, reach out to Practicalli via the #practicalli
channel of the Clojurians Slack community.
Clojurians Slack community
"},{"location":"introduction/contributing/#considering-a-pull-request","title":"Considering a Pull request?","text":"Pull Request Commits must be cryptographically signed
All commits contributed to Practicalli must be signed via a legitimate SSH or GPG key to avoid the risk of commit spoofing.
Configure commit signing with SSH key - Practicalli Engineering
All pull requests must include an entry in CHANGELOG.md or will not be merged. A changelog entry allows the community to follow the changes to the book.
Each pull request will have a number of CI workflows run against the contribution, checking the format of the content and if a changelog entry has been provided.
Please keep pull requests small and focused, as they are much quicker to review and easier to accept. Ideally PR's should be for a specific page or at most a section.
A PR with a list of changes across different sections will be closed without merging as these take considerable time to review.
Issues such as grammar improvements are typically a sign of a rushed section that requires a rewrite, so a pull request to fix a typeographic error will probably not be merged. Raise an issue, or post a thread in the Clojurians Slack #practicall channel
"},{"location":"introduction/contributing/#thank-you-to-everyone-that-has-contributed","title":"Thank you to everyone that has contributed","text":"A huge thank you to Rich Hickey and the team at Cognitect for creating and continually guiding the Clojure language.
The Clojure community has been highly supportive of everyone using Clojure and I'd like to thank everyone for the feedback and contributions. I would also like to thank everyone that has joined in with the London Clojurins community, ClojureBridgeLondon, Clojurians Slack community, Clojurians Zulip community and Clojureverse community.
Thank you to everyone who sponsors the Practicalli websites and videos and for the Clojurists Together sponsorship, it helps me continue the work at a much faster pace.
Special thanks to Bruce Durling for getting me into Cloure in the first place.
"},{"location":"introduction/first-taste-of-clojure/","title":"Clojure Quick Reference","text":"The basic Clojure syntax and a few common functions you should probably learn first.
The examples are editable (using an embedded REPL) so feel free to experiment and watch as the return value changes as you change the code. Reload the page if you want to reset all the code back to the starting point.
Install Clojure on your computer if you want to experiment even further.
Want to go deeper already?
Watch the Clojure language video series by Brian Will for a detailed introduction to key parts of the language. Or discover Clojure core functions by completing challenges on 4Clojure.org and then watching how Practicalli solved them.
"},{"location":"introduction/first-taste-of-clojure/#calling-functions","title":"Calling functions","text":"The first element in a list, ()
, is a call to a function. Any other elements are passed to the function as arguments. The examples show how to call functions with multiple arguments.
(+ 1 2)\n
(+ 3 (* 2 (- 7 2) 4) (/ 16 4))\n
(str \"Clojure is \" (- 2021 2007) \" years old\")\n
(inc 1)\n
(map inc [1 2 3 4 5])\n
(filter odd? (range 11))\n
Prefix notation and parens
Hugging code with ()
is a simple syntax to define the scope of code expressions. No additional ;
, ,
or spaces are required.
Treating the first element of a list as a function call is referred to as prefix notation, which greatly simplifies Clojure syntax. Prefix notation makes mathematical expressions completely deterministic, eliminating the need for operator precedence.
"},{"location":"introduction/first-taste-of-clojure/#understanding-functions","title":"Understanding functions","text":"clojure.repl/doc
function returns the doc-string of the given function. A doc-string should be part of all public function definitions.
Clojure editors should provide commands to view doc-strings and the ability to jump to function definitions to view their source code
(clojure.repl/ddoc doc)\n
"},{"location":"introduction/first-taste-of-clojure/#modeling-data-with-collection-types","title":"Modeling data with Collection types","text":"Clojure has 4 main collection types, all immutable (cannot change once created) and can contain any Clojure types.
A list, ()
, used for calling functions and representing sequences. A linked list for sequential access.
(str \"lists used mainly \" (* 2 2) \" \" :code)\n
A vector, []
, used for simple collections of values. An indexed data structure for random access
[0 \"indexed\" :array (* 2 2) \"random-access\" 4 :data]\n
A map, {}
, use for descriptive data collections. An associative data structure for value lookup by unique keys (also known as a dictionary).
{ :hash-map :associative-collection :pairs {:key \"value\"} :aka \"dictionary\"}\n
A set, #{}
, use as a unique set of values. Sets are used to test if a value is contained within, i.e. predicates.
#{1 2 3 4 \"unique\" \"set\" \"of\" \"values\" \"unordered\" (* 3 9)}\n
Persistent data types
Values are immutable so when a function changes a value a new immutable value is created. When creating new collection values, unchanged values are shared with the original collection. This sharing model is called persistent data types and enables immutable data to be used efficiently.
"},{"location":"introduction/first-taste-of-clojure/#using-data-structures","title":"Using data structures","text":"Using the map
and inc
function, increment all the numbers in a vector
(map inc [1 2 3 4 5])\n
The above map
function is roughly equivalent to the following expression
(conj [] (inc 1) (inc 2) (inc 3) (inc 4) (inc 5))\n
The conj
function creates a new collection by combining a collection and one or more values.
map
reduce
filter
are common functions for iterating through a collection / sequence of values
(map * [1 3 5 8 13 21] [3 5 8 13 21 34])\n
(filter even? [1 3 5 8 13 21 34])\n
(reduce + [31 28 30 31 30 31])\n
(empty? [])\n
Many Clojure core functions for collections
map
, reduce
, apply
, filter
, remove
are just a few examples of Clojure core functions that work with data structures.
"},{"location":"introduction/first-taste-of-clojure/#defining-custom-functions","title":"Defining custom functions","text":"(defn square-of\n \"Calculates the square of a given number\"\n [number]\n (* number number))\n\n(square-of 9)\n
Function definitions can also be used within other expressions, useful for mapping custom functions over a collection
(map (fn [number] (* number number)) [1 2 3 4 5])\n
"},{"location":"introduction/first-taste-of-clojure/#defining-local-names","title":"Defining local names","text":"Use the let
function as a simple way to experiment with code designs
(let [data (range 24 188)\n total (reduce + data)\n values (count data)]\n (str \"Average value: \" (/ total values)))\n
Define local names to remove duplication in function definitions, or to simplify algorithms
(defn square-of\n \"Calculates the square of a given number\"\n [number]\n (* number number))\n\n(square-of 9)\n
"},{"location":"introduction/first-taste-of-clojure/#defining-names-for-values-vars","title":"Defining names for values (vars)","text":"A name bound to a value can be used to represent that value throughout the code. Names can be bound to simple values (numbers, strings, etc.), collections or even function calls.
def
binds a name to a value with the scope of the current namespace. def
is useful for data that is passed to multiple functions within a namespace.
Evaluating a name will return the value it is bound to.
(def public-health-data\n [{:date \"2020-01-01\" :confirmed-cases 23814 :recovery-percent 15}\n {:date \"2020-01-02\" :confirmed-cases 24329 :recovery-percent 14}\n {:date \"2020-01-03\" :confirmed-cases 25057 :recovery-percent 12}])\n\npublic-health-data\n
def for shared values, let for locally scoped values
let
function is used to bind names to values locally, such as within a function definition. Names bound with def
have namespace scope so can be used with any code in that namespace.
"},{"location":"introduction/first-taste-of-clojure/#iterating-over-collections","title":"Iterating over collections","text":"map
iterates a function over a collection of values, returning a new collection of values
(map inc (range 20))\n
reduce
iterates a function over the values of a collection to produce a new result
(reduce + (range 101))\n
Reducing functions are function definitions used by the reduce
function over a collection
(reduce (fn [[numerator denominator] accumulator]\n [(+ numerator accumulator)\n (inc denominator)])\n [0 0]\n (range 1 20))\n
Functions can call themselves to iterate over a collection. Using a lazy sequence means only the required numbers are generated, ensuring efficiency of operation and making the function usable in many different scenarios.
(defn fibonacci-sequence\n [current-number next-number]\n (lazy-seq\n (cons current-number\n (fibonacci-sequence next-number (+ current-number next-number)))))\n\n(take 10 (fibonacci-sequence 0 1))\n
"},{"location":"introduction/first-taste-of-clojure/#host-interoperability","title":"Host Interoperability","text":"The REPL in this web page is running inside a JavaScript engine, so JavaScript functions can be used from within ClojureScript code (ClojureScript is Clojure that runs in JavaScript environments).
In the box below, replace ()
with (js/alert \"I am a pop-up alert\")
()\n
Java libraries in Clojure
java.lang library is available in Clojure by default and many other Java methods can be included by using their full name, e.g. (java.lang.Date.)
will return the current date.
"},{"location":"introduction/first-taste-of-clojure/#next-steps","title":"Next steps","text":"Install Clojure on your computer if you want to experiment even further or keep on reading more about Clojure.
"},{"location":"introduction/five-steps-to-clojure/","title":"5 Steps to Clojure","text":""},{"location":"introduction/five-steps-to-clojure/#set-up-your-environment","title":"Set up your environment","text":"Install Clojure and a build tool
Setup a Clojure aware editor
- Emacs & CIDER - Spacemacs, Doom, Prelude
- Neovim & Conjure
- VSCode & Clover or Calva
- Sublime Text & SublimedClojure
"},{"location":"introduction/five-steps-to-clojure/#learn-the-syntax","title":"Learn the syntax","text":""},{"location":"introduction/five-steps-to-clojure/#practice-the-core-functions","title":"Practice the core functions","text":""},{"location":"introduction/five-steps-to-clojure/#def-defn-let","title":"def / defn / let","text":""},{"location":"introduction/five-steps-to-clojure/#map-reduce-apply","title":"map / reduce / apply","text":""},{"location":"introduction/five-steps-to-clojure/#for-while-loop-recur","title":"for / while / loop / recur","text":""},{"location":"introduction/five-steps-to-clojure/#adopt-functional-programming-practices","title":"Adopt functional programming practices","text":""},{"location":"introduction/five-steps-to-clojure/#learn-the-commonly-used-libraries","title":"Learn the commonly used libraries","text":""},{"location":"introduction/five-steps-to-clojure/#server-side-websites","title":"Server-side websites","text":""},{"location":"introduction/five-steps-to-clojure/#ring-compojure-reitit-hiccup-selma","title":"Ring / Compojure / Reitit / Hiccup | Selma","text":""},{"location":"introduction/five-steps-to-clojure/#react-client-side-single-page-apps","title":"React client-side single page apps","text":""},{"location":"introduction/five-steps-to-clojure/#reactjs-om-next-reagent-re-frame","title":"React.js / Om-next / Reagent / Re-frame","text":""},{"location":"introduction/five-steps-to-clojure/#coreasync","title":"core.async","text":""},{"location":"introduction/five-steps-to-clojure/#full-stack-apps","title":"Full Stack apps","text":""},{"location":"introduction/five-steps-to-clojure/#kit-framework","title":"Kit Framework","text":""},{"location":"introduction/learning-clojure/","title":"Learning Clojure","text":"Learning the syntax of Clojure is really quick (its very small and simple). Learning to think functionally and discovering the 700+ functions in the Clojure API can take a little longer. I recommend you find someone with a bit of Clojure experience to guide you.
Here is my suggested path to learning Clojure and thinking functionally. Many of the tasks can be done in parallel.
"},{"location":"introduction/learning-clojure/#simple-rather-than-complex-the-foundation-of-clojure","title":"Simple rather than complex - the foundation of Clojure","text":"Gaining an appreciation that systems should be simple is a crucial step truly understanding Clojure. So early in your journey into Clojure, spend an hour watching Rich Hickey talk about Simple made Easy - (transcript of talk).
"},{"location":"introduction/learning-clojure/#experience-the-clojure-syntax","title":"Experience the Clojure syntax","text":"Take a quick look at the Syntax of Clojure. The syntax is very small, so this will take about 15 minutes to 1 hour (dependent on your own experiences with coding). Don't try to remember all the syntax, it will come through practise.
- eg. Clojure in 15 minutes
"},{"location":"introduction/learning-clojure/#set-up-an-enjoyable-environment-to-work-in","title":"Set up an enjoyable environment to work in","text":"Find how to use Clojure with your favourite editor or IDE. Configure this tool so you can easily run a REPL and evaluate some expressions.
- repl.it - web based repl you can share / fork
- Spacemacs - for the ultimate Emacs & Vim experience
- IntelliJ and Cursive
- Leiningen & any editor you like
"},{"location":"introduction/learning-clojure/#building-a-frame-of-reference-for-functional-programming","title":"Building a frame of reference for functional programming","text":"Find an introductory book that you like which provides lots of example code to help you feel more comfortable with the syntax and more importantly the major concepts of functional programming with Clojure. Type in the exercises as you read and don't be afraid to play around with code examples
- Clojure for the Brave and the True
- Living Clojure - includes a training guide
- Practicalli Clojure - you are already here :)
- ClojureBridge London workshop - aimed at those new to coding
- PurelyFunctional - Introduction to Clojure
"},{"location":"introduction/learning-clojure/#practice-clojure-standard-library-clojurecore","title":"Practice Clojure standard library (clojure.core)","text":"Practice Clojure. Write lots of small and relatively simple examples in Clojure and experiment with the code in the REPL and try break things. This will start helping you learn the Clojure API
You should become comfortable in your understanding of:
- basic values (strings, numbers, etc) and persistent collections (list, vector, map, set)
- binding names to values and their scope (def, defn, let)
- calling functions, defining functions, arity options for functions
- Higher order functions and basics of functional composition (map, reduce, filter, etc)
- Designing with data, Extensible Data Notation (EDN), data manipulation
Activities to help practice Clojure include:
- 4Clojure.org - aim to complete the first 50 exercises, the first 10 are relatively easy
- Coding Kata exercises
- Awesome Kata collection
- Alice In Wonderland inspired Katas
- Attend coding dojo events - e.g. London Clojurians
"},{"location":"introduction/learning-clojure/#solidify-some-of-the-basics-you-have-learned-so-far","title":"Solidify some of the basics you have learned so far","text":"Work on a relatively small project that you care about enough to work on
- eg. a tool to help you at work
"},{"location":"introduction/learning-clojure/#learn-more-tools-to-help-you-think-functionally","title":"Learn more tools to help you think functionally","text":" - mostly using immutable values and pure functions
- functional composition, sequences and transducers
- atoms for managing mutable state changes (with immutable values)
"},{"location":"introduction/learning-clojure/#get-a-broader-view-of-clojure-and-learn-some-common-practices","title":"Get a broader view of Clojure and learn some common practices","text":"Start reading a book which is aimed at intermediate
Watch Video's about Clojure on subjects that are relevant to work or projects you want to work on.
Follow tutorials on Clojure, especially those that introduce the amazing libraries available in Clojure
- Lambda Island
- PurelyFunctional.tv
- Practical.li
Work with some of the most common libraries in Clojure
- Ring / Compojure for web development (server side)
- ClojureScript for web UI or native mobile apps (client side)
- Reagent - reactjs style single page apps
- Reagent deep dive - excellent tutorial
- core.async - for asynchronous programming
- clojure.string - specific functions for string manipulation
- tools.logging
- java.jdbc - database access
- data.zip - manipulating trees of data, e.g. XML
"},{"location":"introduction/repl-workflow/","title":"REPL Driven Development","text":"Always be REPL'ing
Coding without a REPL feels limiting. The REPL provides fast feedback from code as its crafted, testing assumptions and design choices every step of the journey to a solution - John Stevenson, Practical.li
Clojure is a powerful, fun and highly productive language for developing applications and services. The clear language design is supported by a powerful development environment known as the REPL (read, evaluate, print, loop). The REPL gives you instant feedback on what your code does and enables you to test either a single expression or run the whole application (including tests).
REPL driven development is the foundation of working with Clojure effectively
An effective Clojure workflow begins by running a REPL process. Clojure expressions are written and evaluated immediately to provide instant feedback. The REPL feedback helps test the assumptions that are driving the design choices.
- Read - code is read by the Clojure reader, passing any macros to the macro reader which converts those macros into Clojure code.
- Evaluate - code is compiled into the host language (e.g. Java bytecode) and executed
- Print - results of the code are displayed, either in the REPL or as part of the application.
- Loop - the REPL is a continuous process that evaluates code, either a single expression or the whole application.
Design decisions and valuable data from REPL experiments can be codified as specifications and unit tests
Practicalli REPL Reloaded Workflow
The principles of REPL driven development are implemented in practice using the Practicalli REPL Reloaded Workflow and supporting tooling. This workflow uses Portal to inspect all evaluation results and log events, hot-load libraries into the running REPL process and reloads namespaces to support major refactor changes.
"},{"location":"introduction/repl-workflow/#evaluating-source-code","title":"Evaluating source code","text":"A REPL connected editor is the primary tool for evaluating Clojure code from source code files, displaying the results inline.
Source code is automatically evaluated in its respective namespace, removing the need to change namespaces in the REPL with (in-ns
) or use fully qualified names to call functions.
Evaluate Clojure in a Terminal UI REPL Entering expressions at the REPL prompt evaluates the expression immediately, returning the result directly underneath
"},{"location":"introduction/repl-workflow/#rich-comment-blocks-living-documentation","title":"Rich Comment blocks - living documentation","text":"The (comment ,,,)
function wraps code that is only run directly by the developer using a Clojure aware editor.
Expressions in rich comment blocks can represent how to use the functions that make up the namespace API. For example, starting/restarting the system, updating the database, etc. Expressions provide examples of calling functions with typical arguments and make a project more accessible and easier to work with.
Clojure Rich Comment to manage a service
(ns practicalli.gameboard.service)\n\n(defn app-server-start [port] ,,,)\n(defn app-server-start [] ,,,)\n(defn app-server-restart [] ,,,)\n\n(defn -main\n \"Start the service using system components\"\n [& options] ,,,)\n\n(comment\n (-main)\n (app-server-start 8888)\n (app-server-stop)\n (app-server-restart 8888)\n\n (System/getenv \"PORT\")\n (def environment (System/getenv))\n (def system-properties (System/getProperties))\n ) ; End of rich comment block\n
Rich comment blocks are very useful for rapidly iterating over different design decisions by including the same function but with different implementations. Hide clj-kondo linter warnings for redefined vars (def
, defn
) when using this approach.
;; Rich comment block with redefined vars ignored\n#_{:clj-kondo/ignore [:redefined-var]}\n(comment\n (defn value-added-tax []\n ;; algorithm design - first idea)\n\n (defn value-added-tax []\n ;; algorithm design - second idea)\n\n ) ;; End of rich comment block\n
The \"Rich\" in the name is an honourary mention to Rich Hickey, the author and benevolent dictator of Clojure design.
"},{"location":"introduction/repl-workflow/#design-journal","title":"Design Journal","text":"A journal of design decisions makes the code easier to understand and maintain. Code examples of design decisions and alternative design discussions are captured, reducing the time spent revisiting those discussions.
Journals simplify the developer on-boarding processes as the journey through design decisions are already documented.
A Design Journal is usually created in a separate namespace, although it may start as a rich comment at the bottom of a namespace.
A journal should cover the following aspects
- Relevant expressions use to test assumptions about design options.
- Examples of design choices not taken and discussions why (saves repeating the same design discussions)
- Expressions that can be evaluated to explain how a function or parts of a function work
The design journal can be used to create meaningful documentation for the project very easily and should prevent time spent on repeating the same conversations.
Example design journal
Design journal for TicTacToe game using Reagent, ClojureScript and Scalable Vector Graphics
"},{"location":"introduction/repl-workflow/#viewing-data-structures","title":"Viewing data structures","text":"Pretty print shows the structure of results from function calls in a human-friendly form, making it easier for a developer to parse and more likely to notice incorrect results.
Tools to view and navigate code
- Cider inspector is an effective way to navigate nested data and page through large data sets.
- Portal Inspector to visualise many kinds of data in many different forms.
"},{"location":"introduction/repl-workflow/#code-style-and-idiomatic-clojure","title":"Code Style and idiomatic Clojure","text":"Clojure aware editors should automatically apply formatting that follows the Clojure Style guide.
Live linting with clj-kondo suggests common idioms and highlights a wide range of syntax errors as code is written, minimizing bugs and therefore speeding up the development process.
Clojure LSP is build on top of clj-kondo
Clojure LSP uses clj-kondo static analysis to provide a standard set of development tools (format, refactor, auto-complete, syntax highlighting, syntax & idiom warnings, code navigation, etc).
Clojure LSP can be used with any Clojure aware editor that provides an LSP client, e.g. Spacemacs, Doom Emacs, Neovim, VSCode.
Clojure Style Guide
The Clojure Style guide provides examples of common formatting approaches, although the development team should decide which of these to adopt. Emacs clojure-mode
will automatically format code and so will Clojure LSP (via cljfmt). These tools are configurable and should be tailored to the teams standard.
"},{"location":"introduction/repl-workflow/#data-and-function-specifications","title":"Data and Function specifications","text":" Clojure spec is used to define a contract on incoming and outgoing data, to ensure it is of the correct form.
As data structures are identified in REPL experiments, create data specification to validate the keys and value types of that data.
;; ---------------------------------------------------\n;; Address specifications\n(spec/def ::house-number string?)\n(spec/def ::street string?)\n(spec/def ::postal-code string?)\n(spec/def ::city string?)\n(spec/def ::country string?)\n(spec/def ::additional string?)\n\n(spec/def ::address ; Composite data specification\n (spec/keys\n :req-un [::street ::postal-code ::city ::country]\n :opt-un [::house-number ::additional]))\n;; ---------------------------------------------------\n
As the public API is designed, specifications for each functions arguments are added to validate the correct data is used when calling those functions.
Generative testing provides a far greater scope of test values used incorporated into unit tests. Data uses clojure.spec to randomly generate data for testing on each test run.
"},{"location":"introduction/repl-workflow/#test-driven-development-and-repl-driven-development","title":"Test Driven Development and REPL Driven Development","text":"Test Driven Development (TDD) and REPL Driven Development (RDD) complement each other as they both encourage incremental changes and continuous feedback.
Test Driven Development fits well with Hammock Time, as good design comes from deep thought
- RDD enables rapid design experiments so different approaches can easily and quickly be evaluated .
- TDD focuses the results of the REPL experiments into design decisions, codified as unit tests. These tests guide the correctness of specific implementations and provide critical feedback when changes break that design.
Unit tests should support the public API of each namespace in a project to help prevent regressions in the code. Its far more efficient in terms of thinking time to define unit tests as the design starts to stabilize than as an after thought.
clojure.test
library is part of the Clojure standard library that provides a simple way to start writing unit tests.
Clojure spec can also be used for generative testing, providing far greater scope in values used when running unit tests. Specifications can be defined for values and functions.
Clojure has a number of test runners available. Kaocha is a test runner that will run unit tests and function specification checks.
Automate local test runner
Use kaocha test runner in watch mode to run tests and specification check automatically (when changes are saved)
clojure -X:test/watch\n
"},{"location":"introduction/repl-workflow/#continuous-integration-and-deployment","title":"Continuous Integration and Deployment","text":"Add a continuous integration service to run tests and builds code on every shared commit. Spin up testable review deployments when commits pushed to a pull request branch, before pushing commits to the main deployment branch, creating an effective pipeline to gain further feedback.
- CircleCI provides a simple to use service that supports Clojure projects.
- GitHub Workflows and GitHub actions marketplace to quickly build a tailored continuous integration service, e.g. Setup Clojure GitHub Action.
- GitLab CI
"},{"location":"introduction/repl-workflow/#live-coding-with-data-stuart-halloway","title":"Live Coding with Data - Stuart Halloway","text":"There are few novel features of programming languages, but each combination has different properties. The combination of dynamic, hosted, functional and extended Lisp in Clojure gives developers the tools for making effective programs. The ways in which Clojure's unique combination of features can yield a highly effective development process.
Over more than a decade we have developed an effective approach to writing code in Clojure whose power comes from composing many of its key features. As different as Clojure programs are from e.g. Java programs, so to can and should be the development experience. You are not in Kansas anymore!
This talk presents a demonstration of the leverage you can get when writing programs in Clojure, with examples, based on my experiences as a core developer of Clojure and Datomic.
"},{"location":"introduction/who-uses-clojure/","title":"Who uses Clojure","text":"Hundreds of companies actively advertised their Clojure adoption. Given the broad participation in user groups there are clearly many more organizations using Clojure at some level in their technology stack.
A quick scan of various job sites shows Clojure positions at companies like Walmart, Facebook, Staples, Consumer Reports, Salesforce, and Amazon. It doesn't get much more mainstream than that.
If you are looking for a new role using Clojure or other functional programming languages, visit Functional Works, the only Functional Recruiters in the world.
Here is just a small and diverse set of example companies that I am aware of that use Clojure for development.
Company Type of applications Boeing Boeing 737 MAX - onboard maintenance Puppet Labs DevOps apps & services e.g. trapperkeeper Cisco Malware analysis & threat intelligence platform (expert system with core.logic) Deuche Bank (UK) Processing event streams from Apache Storm Atlassian Collaborative editing platform for all their products Netflix Map-Reduce languages for writing apps for Hadoop / Pig USwitch (UK) Creating meaningful data from multiple sources Daily Mail Online (UK) Publishing pipeline Circle CI (USA) Front-end of CI server in ClojureScript & test suite CitiGroup Financial Trading Student Loans company (UK) Loans management system written in Clojure LinkedIn Powers the LinkedIn social graph Walmart (USA) eReceipts project to process every purchase from 5,000+ stores SwiftKey (UK) Predictive intelligence platform (possibly used with Microsoft Cortana) Roomkey.co.uk Hotel booking system to rival Expedia (with a tiny development team) Funding Circle (UK & USA) Adopting Clojure as their main language (from Ruby, etc) Braintree Payment processing pipeline with Kafka Mastodon C Data centre analysis (Incanta, Storm) Thoughtworks Agile development for Client projects world wide Vero Insurance (AUS) Rebuilt policy management system in Clojure with Thoughworks Meta-X Performance art (Overtone, Quil) Salesforce (USA) Build & deployment with Puppet & Application Routing with nginx-clojure There are many more examples of projects on the HackerNews thread: Ask HN: Who's using Clojure in Production
"},{"location":"introduction/who-uses-clojure/#tech-radar","title":"Tech Radar","text":"Clojure is also defined as a technology that can be adopted since 2014, according to the Thoughtworks technology radar.
JUXT also created its own Clojure specific technology radar as there is such an encompassing ecosystem of libraries and services.
"},{"location":"introduction/writing-tips/","title":"Writing tips for MkDocs","text":"Making the docs more engaging using the mkdocs-material theme reference guide
Configuring Colors Material for MkDocs - Changing the colors lists the primary and accent colors available.
HSL Color Picker for codes to modify the theme style, overriding colors in docs/assets/stylesheets/extra.css
"},{"location":"introduction/writing-tips/#hypertext-links","title":"Hypertext links","text":"Links open in the same browser window/tab by default.
Add {target=_blank}
to the end of a link to configure opening in a new tab
[link text](url){target=_blank}\n
"},{"location":"introduction/writing-tips/#buttons","title":"Buttons","text":"Convert any link into a button by adding {.md-button}
class names to end of the markdown for a link, which uses .md-button-primary
by default. Include target=_blank
for buttons with links to external sites.
[link text](http://practical.li/blog){.md-button target=_blank}\n
Or specify a different class
[link text](http://practical.li/blog){.md-button .md-button-primary}\n
Add an icon to the button
Practicalli Issues Practicalli Blog
[:fontawesome-brands-github: Practicalli Issues](http://practical.li/blog){ .md-button .md-button-primary }\n[:octicons-heart-fill-24: Practicalli Blog](http://practical.li/blog){ .md-button .md-button-primary }\n
Search all supported icons
"},{"location":"introduction/writing-tips/#youtube-video","title":"YouTube video","text":"Use an iframe element to include a YouTube video, wrapping in a paragraph tag with center alignment to place the video in a centered horizontal position
<p style=\"text-align:center\">\n<iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/rQ802kSaip4\" title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>\n</p>\n
mkdocs material does not have direct support for adding a YouTube video via markdown.
"},{"location":"introduction/writing-tips/#admonitions","title":"Admonitions","text":"Supported admonition types
Note
Use !!!
followed by NOTE
Adding a title
Use !!!
followed by NOTE
and a \"title in double quotes\"
Shh, no title bar just the text... Use !!!
followed by NOTE
and a \"\"
empty double quotes
Abstract
Use !!!
followed by ABSTRACT
Info
Use !!!
followed by INFO
Tip
Use !!!
followed by TIP
Success
Use !!!
followed by SUCCESS
Question
Use !!!
followed by QUESTION
Warning
Use !!!
followed by WARNING
Failure
Use !!!
followed by FAILURE
Danger
Use !!!
followed by DANGER
Bug
Use !!!
followed by BUG
Example
Use !!!
followed by EXAMPLE
Quote
Use !!!
followed by QUOTE
"},{"location":"introduction/writing-tips/#collapsing-admonitions","title":"Collapsing admonitions","text":"Note Collapse those admonitions using ???
instead of !!!
Replace with a title Use ???
followed by NOTE
and a \"title in double quotes\"
Expanded by default Use ???+
, note the +
character, followed by NOTE
and a \"title in double quotes\"
"},{"location":"introduction/writing-tips/#inline-blocks","title":"Inline blocks","text":"Inline blocks of text to make a very specific callout within text
Info
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.
Adding something to then end of text is probably my favourite
Info
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.
"},{"location":"introduction/writing-tips/#code-blocks","title":"Code blocks","text":"Code blocks include a copy icon automatically
Syntax highlighting in code blocks
(defn my-function ; Write a simple function\n \"With a lovely doc-string\"\n [arguments]\n (map inc [1 2 3]))\n
Give the code block a title using title=\"\"
after the backtics and language name
src/practicalli/gameboard.clj(defn my-function\n \"With a lovely doc-string\"\n [arguments]\n (map inc [1 2 3]))\n
We all like line numbers, especially when you can set the starting line
src/practicalli/gameboard.clj(defn my-function\n \"With a lovely doc-string\"\n [arguments]\n (map inc [1 2 3]))\n
Add linenums=42
to start line numbers from 42 onward
clojure linenums=\"42\" title=\"src/practicalli/gameboard.clj\"\n
"},{"location":"introduction/writing-tips/#annotations","title":"Annotations","text":"Annotations in a code block help to highlight important aspects. Use the comment character for the language followed by a space and a number in brackets
For example, in a shell code block, use # (1)
where 1 is the number of the annotation
Use a number after the code block to add the text for the annotation, e.g. 1.
. Ensure there is a space between the code block and the annotation text.
ls -la $HOME/Downloads # (1)\n
- I'm a code annotation! I can contain
code
, formatted text, images, ... basically anything that can be written in Markdown.
Code blocks with annotation, add !
after the annotation number to suppress the #
character
(defn helper-function\n \"Doc-string with description of function purpose\" ; (1)!\n [data]\n (merge {:fish 1} data)\n )\n
- Always include a doc-string in every function to describe the purpose of that function, identifying why it was added and what its value is.
GitHub action example with multiple annotations
name: ci # (1)!\non:\n push:\n branches:\n - master # (2)!\n - main\npermissions:\n contents: write\njobs:\n deploy:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-python@v4\n with:\n python-version: 3.x\n - run: pip install mkdocs-material # (3)!\n - run: mkdocs gh-deploy --force\n
-
You can change the name to your liking.
-
At some point, GitHub renamed master
to main
. If your default branch is named master
, you can safely remove main
, vice versa.
-
This is the place to install further [MkDocs plugins] or Markdown extensions with pip
to be used during the build:
pip install \\\n mkdocs-material \\\n mkdocs-awesome-pages-plugin \\\n ...\n
"},{"location":"introduction/writing-tips/#highlight-lines-in-code-blocks","title":"Highlight lines in code blocks","text":"Add highlight line meta data to a code block after the opening backticks and code block language.
hl_lines=\"2\"
highlights line 2 in the codeblock
(defn my-function\n \"With a lovely doc-string\"\n [arguments]\n (map\n inc\n [1 2 3]))\n
"},{"location":"introduction/writing-tips/#embed-external-files","title":"Embed external files","text":"--8<--
in a code block inserts code from a source code file or other text file
Specify a local file from the root of the book project (the directory containing mkdocs.yml)
Scheduled Version Check GitHub Workflow from source code file scheduled version check\n
Practicalli Project Templates Emacs project configuration - .dir-locals.el((clojure-mode . ((cider-preferred-build-tool . clojure-cli)\n (cider-clojure-cli-aliases . \":test/env:dev/reloaded\"))))\n
Code example reuse
Use an embedded local or external file (URL) when the same content is required in more than one place in the book.
An effective way of sharing code and configuration mutliple times in a book or across multiple books.
"},{"location":"introduction/writing-tips/#content-tabs","title":"Content tabs","text":"Create in page tabs that can also be
Setting up a project
Clojure CLILeiningen clojure -T:project/new :template app :name practicalli/gameboard\n
lein new app practicalli/gameboard\n
Or nest the content tabs in an admonition
Run a terminal REPL
Clojure CLILeiningen clojure -T:repl/rebel\n
lein repl\n
"},{"location":"introduction/writing-tips/#diagrams","title":"Diagrams","text":"Neat flow diagrams
Diagrams - Material for MkDocs
graph LR\n A[Start] --> B{Error?};\n B -->|Yes| C[Hmm...];\n C --> D[Debug];\n D --> B;\n B ---->|No| E[Yay!];
UML Sequence Diagrams
sequenceDiagram\n Alice->>John: Hello John, how are you?\n loop Healthcheck\n John->>John: Fight against hypochondria\n end\n Note right of John: Rational thoughts!\n John-->>Alice: Great!\n John->>Bob: How about you?\n Bob-->>John: Jolly good!
state transition diagrams
stateDiagram-v2\n state fork_state <<fork>>\n [*] --> fork_state\n fork_state --> State2\n fork_state --> State3\n\n state join_state <<join>>\n State2 --> join_state\n State3 --> join_state\n join_state --> State4\n State4 --> [*]
Class diagrams - not needed for Clojure
Entity relationship diagrams are handy though
erDiagram\n CUSTOMER ||--o{ ORDER : places\n ORDER ||--|{ LINE-ITEM : contains\n LINE-ITEM {\n customer-name string\n unit-price int\n }\n CUSTOMER }|..|{ DELIVERY-ADDRESS : uses
"},{"location":"introduction/writing-tips/#keyboard-keys","title":"Keyboard keys","text":"Represent key bindings with Keyboard keys. Each number and alphabet character has their own key.
- 1
++1++
for numbers - l
++\"l\"++
for lowercase character - U
++u++
for uppercase character or ++\"U\"++
for consistency
Punctionation keys use their name
- Space
++spc++
- ,
++comma++
- Left
++arrow-left++
For key sequences, place a space between each keyboard character
- Space g s
++spc++ ++\"g\"++ ++\"s\"++
For key combinations, use join they key identifies with a +
- Meta+X
++meta+x++
- Ctrl+Alt+Del
++ctrl+alt+del++
MkDocs keyboard keys reference
"},{"location":"introduction/writing-tips/#images","title":"Images","text":"Markdown images can be appended with material tags to set the size of the image, whether to appear on light or dark theme and support lazy image loading in browsers
SizeLazy LoadingAlignTheme SpecificAll Image Attributes {style=\"height:150px;width:150px\"}
specifies the image size
![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png#only-dark){style=\"height:150px;width:150px\"}\n
{loading=lazy}
specifies an image should lazily load in the browser
![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png){loading=lazy}\n
{aligh=left}
or {aligh=right}
specifies the page alignment of an image.
![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png#only-dark){align=right}\n![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-dark.png#only-light){align=right}\n
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.
![Kitty Logo](image/kitty-light.png#only-dark)
or ![Kitty Logo](image/kitty-light.png#only-light)
specifies the theme the image should be shown, allowing different versions of images to be shown based on the theme.
![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png#only-dark){style=\"height:150px;width:150px\"}\n![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-dark.png#only-light){style=\"height:150px;width:150px\"}\n
Use the theme toggle in the top nav bar to see the icon change between light and dark. Requires the color pallet toggle
Alight right, lazy load and set image to 150x150
![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png#only-dark){align=right loading=lazy style=\"height:64px;width:64px\"}\n![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-dark.png#only-light){align=right loading=lazy style=\"height:64px;width:64px\"}\n
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.
"},{"location":"introduction/writing-tips/#lists","title":"Lists","text":"Task lists
- Lorem ipsum dolor sit amet, consectetur adipiscing elit
- Vestibulum convallis sit amet nisi a tincidunt
- In hac habitasse platea dictumst
- In scelerisque nibh non dolor mollis congue sed et metus
- Praesent sed risus massa
- Aenean pretium efficitur erat, donec pharetra, ligula non scelerisque
Task List example
- [x] Lorem ipsum dolor sit amet, consectetur adipiscing elit\n- [ ] Vestibulum convallis sit amet nisi a tincidunt\n * [x] In hac habitasse platea dictumst\n * [x] In scelerisque nibh non dolor mollis congue sed et metus\n * [ ] Praesent sed risus massa\n- [ ] Aenean pretium efficitur erat, donec pharetra, ligula non scelerisque\n
"},{"location":"introduction/writing-tips/#tooltips","title":"Tooltips","text":"The humble tool tip
Hover me
with references
Hover me
Icon tool tip with a title
"},{"location":"introduction/writing-tips/#abreviations","title":"Abreviations","text":"The HTML specification is maintained by the W3C.
[HTML]: Hyper Text Markup Language [W3C]: World Wide Web Consortium
"},{"location":"introduction/writing-tips/#magic-links","title":"Magic links","text":"MagicLink can auto-link HTML, FTP, and email links. It can auto-convert repository links (GitHub, GitLab, and Bitbucket) and display them in a more concise, shorthand format.
Email Practicalli
Practicalli Neovim
"},{"location":"introduction/concepts/","title":"Clojure concepts","text":"Clojure is an elegant language for a more civilized development experience.
Clojure supports the creation of simple software systems using immutable values and encouraging a pragmatic approach to pure functional design.
A simple syntax means Clojure is quick to learn and a wide range of open source libraries provides a rapid way to build any kind of software. Designed as a hosted language, Clojure runs on many platforms including the Java Virtual Machine, GraalVM, Microsoft.Net, JavaScript engines. Simple host language interoperability provides access to libraries from a wide range of programming languages, further extending the reach of Clojure.
Experiment with the Clojure language to help understand concepts
Spend some time eevaluating code in the REPL and then revisit this section to get a deeper understanding of the design and philosophy of the Clojure approach to functional programming.
Clojure concepts are easier to relate to whist practicing with Clojure and building Clojure software solutions.
"},{"location":"introduction/concepts/#ten-big-ideas-plus-one","title":"Ten Big Ideas plus one","text":"The key to understanding Clojure is ideas, not language constructs but the concepts that shape the language.
Each of these ideas is valuable by itself, not only in Clojure. Taken together, however, they Begin to fill in the picture of why Clojure is changing the way many programmers think about software development.
- Extensible Data Notation
- Persistent Data Structures
- Sequences
- Transducers
- Specification
- Dynamic Development
- Async Programming
- Protocols
- ClojureScript
- Logic query / Logic Programming
- Atomic Succession Model
Stuart Halloway presents Clojure in 10 big ideas (plus one) in the following video, also see presentation Content
- 2013 RuPy slides
- 2017 Chicago JUG slides
"},{"location":"introduction/concepts/#antithesis-of-clojure-and-simple-software-design","title":"Antithesis of Clojure and simple software design","text":"In Narcissistic Design by Stuart Halloway, the antithesis of the Clojure view of software development is presented as a description of how unproductive and valueless much of the software industry has been in the past.
Its essentially a guide on what to avoid if you are a responsible and professional software developer.
"},{"location":"introduction/concepts/all-bytecode-in-the-end/","title":"Its all Bytecode in the end","text":"The REPL is your compiler
As soon as you evaluate your code in the REPL it is also being compiled in the background into Java Bytecode. So there is no need for a separate build and run phase.
Injecting code into the running environment provides the means for fast iterative development of code.
"},{"location":"introduction/concepts/clojure-made-simple/","title":"Clojure from the Author","text":"A series of important videos from Rich Hickey, the author of Clojure who spent over 2 years designing core of Clojure around the concept of simplicity. Since then Rich has stewarded the continued design and development of Clojure, ensuring it stays true to is founding principles.
You do not need to watch these videos to start coding in Clojure, but they are very useful to adopt the approach and design idioms that make Clojure a highly effective language for software development.
"},{"location":"introduction/concepts/clojure-made-simple/#expert-to-expert-rich-hickey-and-brian-beckman-inside-clojure","title":"Expert to Expert: Rich Hickey and Brian Beckman - Inside Clojure","text":"Discussing some of the key characteristics of the Clojure language and why those decisions were taken
"},{"location":"introduction/concepts/clojure-made-simple/#clojure-made-simple","title":"Clojure made simple","text":"Covers the major problems with software development and the challenges most programming languages fail to tackle completely.
Discusses a simple approach to software development and the big picture view of how Clojure solves these problems
"},{"location":"introduction/concepts/clojure-made-simple/#simplicity-matters","title":"Simplicity Matters","text":"!!! QUOTE Rich Hickey, Clojure Benevolent Dictator for Life As we move forward we have to take what we already have and make that [software] do more, make it do things differently, make it do things better, and as we try to take on manipulating software we are going to be challenged to understand it in order to make that happen. And I'll contend that you will completely be dominated by complexity. I don't care what processes you are using, I don't care how well you test or anything else. Complexity will dominate what you do.
"},{"location":"introduction/concepts/clojure-made-simple/#discussing-design","title":"Discussing Design","text":""},{"location":"introduction/concepts/clojure-made-simple/#the-value-of-values","title":"The value of values","text":"Rich Hickey provides analysis of the changing way we think about values (not the philosophical kind) in light of the increasing complexity of information technology and the advent of Big Data
Also see the related video: Database as a value by Rich Hickey
"},{"location":"introduction/concepts/clojure-made-simple/#understanding-clojure-as-a-programming-language","title":"Understanding Clojure as a programming language","text":""},{"location":"introduction/concepts/design/","title":"Clojure Design","text":"Clojure leads to a very component based approach to development. There are no huge and bloated frameworks in Clojure. The core is very small. Hundreds of focused libraries to use in collaboration.
Boiled down to the most simplest structure, Clojure applications you write typically look like this:
;; define a namespace\n(ns name-space.name)\n\n;; define one or more immutable data structures - the fewer the better typically\n(def my-data-structure [[{}{}]])\n\n;; define behaviour that acts on data structures inside one or more functions\n(defn my-function [parameter]\n (my-behaviour parameter))\n\n;; Call those functions to make your application do something\n(my-behaviour data)\n
Hint As functions always evaluate to a value, a function can be used as an argument to another function (or itself if you get recursive !!)
"},{"location":"introduction/concepts/design/#data-focused-design-maps-vectors","title":"Data focused design - Maps & Vectors","text":"Maps (hash-map) and vectors are two more built-in persistent data structures that are more commonly used to represent data within a Clojure application.
A vector is similar to an array in that its an indexed collection and so has fast random access. Vectors are a catch all data structure that can hold any type of information, including other data structures and function calls.
A hash-map is an associative data structure with key value pairs. The keys are most commonly represented with Clojure keywords, although keys can be strings, numbers, collections or functions so long as all the keys are unique.
Hash-maps are a collection of key / value pairs that provide an easy way to reference data by keys. Its common to use a Clojure keyword
type as the keys as keywords are self-referential (they point to themselves). Using keywords in a map means you can use a specific keyword as a function call on the map that returns its associated value.
Some examples of using these data structures this are:
;; A map of maps of maps with occasional vectors\n\n{:star-wars {\n :characters {\n :jedi [\"Luke Skywalker\"\n \"Obiwan Kenobi\"]\n :sith [\"Darth Vader\"\n \"Darth Sideous\"]\n :droids [\"C3P0\"\n \"R2D2\"]}\n :ships {\n :rebel-alliance [\"Millennium Falcon\"\n \"X-wing fighter\"]\n :imperial-empire [\"Intergalactic Cruiser\"\n \"Destroyer\"\n \"Im just making these up now\"]}}}\n
"},{"location":"introduction/concepts/design/#hintbasic-design-principle","title":"Hint::Basic design principle","text":"\u201cIt is better to have 100 functions operate on one data structure than 10 functions on 10 data structures.\u201d \u2014Alan Perlis
"},{"location":"introduction/concepts/design/#extensibility-via-macros","title":"Extensibility via Macros","text":"You can extend the language and define your own constructs using Macros.
The first example of this you see is from Leiningen. The defproject
function is a macro that helps you easily define the configuration of a Clojure project.
An example of a macro that is part of the core Clojure language is defn
. When you define a function with defn
it is syntactic sugar for defining a thing that is a function.
(defn my-function [argument] (my-behaviour argument) )\n\n (def my-function\n (fn [argument] (my-behaviour argument)))\n
"},{"location":"introduction/concepts/design/#special-forms-the-building-blocks-of-clojure","title":"Special forms - the building blocks of Clojure","text":"The following are the building blocks of Clojure, everything else is either a macro or a function
The Clojure / LISP special forms
def, do, if, let, loop, fn, quote, recur, set!, var\n
The forms added for Interoperability with the host platform (mainly Java / JVM)
monitor-enter, monitor-exit,\ncatch, dot ('.'), finally, new, throw, try\n
"},{"location":"introduction/concepts/features/","title":"Features of Clojure","text":""},{"location":"introduction/concepts/features/#dynamic-language","title":"Dynamic language","text":"A problem space can quickly be explored through code to test your assumptions. The design of code is easy to change as you are not managing type changes, Clojure is very good at managing data that would otherwise lead to exceptions.
As a dynamic language the code is quite terse and developers are encouraged to write very modular code, therefore it is easy to refactor.
"},{"location":"introduction/concepts/features/#dynamic-development-repl","title":"Dynamic Development - REPL","text":"Clojure has a REPL (Read Evaluate Print Loop), this is the Clojure run-time environment. You can define functions and data structures, then evaluate them to run either all your code or just a single expression. You can even change code and re-evaluate it whilst your application is still running and immediately see the effect that change has.
So the REPL is a very fast way to explore your problem domain with code
You could even connect to the REPL of a live system and change its behaviour without any down time (unless of course you write code that crashes).
"},{"location":"introduction/concepts/features/#pure-functional-programming","title":"'Pure' Functional Programming","text":"Functions return a value (even if that value is nil) and you can therefore use a function as an argument to another function. This is termed as first order functions.
Clojure encourages a relatively pure approach to functional programming and Clojure can be considered immutable by default
"},{"location":"introduction/concepts/features/#immutability","title":"Immutability","text":" - immutable data structures at its core, everything is immutable by default
- in imperative programming, we change state where ever we like
- in functional programming we avoid changing state as much as possible
- if a function does not change state it is referentially transparent, always returning the same result when given the same input (arguments) - often returned as a pure function
- impure functions can affect other functions and therefore has to be very mindful of the changes it makes and when it makes them
- pure functions are truly modular as they do not affect any other part of the system
"},{"location":"introduction/concepts/features/#persistent-data-structures","title":"Persistent Data Structures","text":"List, Map, Vector and Set are all built in data structures that are immutable.
If you run a function that seems to change a data structure, its actually returning a new data structure. Via a shared-memory model, new data structures are created cheaply as they share the common data elements from the original data structure and only include additional elements.
"},{"location":"introduction/concepts/features/#homoiconicity","title":"Homoiconicity","text":"One thing that keeps Clojure a small language is the fact that the same syntax is used to represent data and behaviour. For example, a function call is defined using a list, data structures and functions are defined using a list. In fact everything is a list, although we use a little syntactic sugar here and there to make the code quicker for a human to parse.
"},{"location":"introduction/concepts/features/#clojure-is-an-implementation-of-lisp","title":"Clojure is an implementation of Lisp","text":"Lisp stands for LISt Processing, so its no surprise that all Clojure code is defined in a list.
The open Parenthesis (
denotes the start of a list, the first element of that list is evaluated as a function call, everything else in the list is data.
The evaluation of the first element of a list can be behaviour of (
can be over-ridden using quote
or its short form the quote character, ', so the list elements are all treated as data.
"},{"location":"introduction/concepts/features/#runtime-polymorphism","title":"Runtime Polymorphism","text":"See Clojure arity and multi-methods for more information
"},{"location":"introduction/concepts/features/#concurrent-programming-parallelism","title":"Concurrent Programming & Parallelism","text":"Concurrent code is much safer when you data does not change state (eg. immutable values). Clojure encourages an immutable approach with its built in persistent data structures (list, Map, Vector, Set). Using Pure Functions that are not affected by or cause side effects also make writing concurrent code trivial.
Clojure helps you scale your applications by with a parallel processing approach, as you can run functions over immutable data-structures without conflict.
"},{"location":"introduction/concepts/features/#hosted-on-the-jvm","title":"Hosted on the JVM","text":"Clojure is compiled to bytecode that runs on the Java Virtual Machine. This helps Clojure run at a very high performance (close to Java, C++, etc.)
Clojure has a concise and easy to use Java Interoperability, enabling you to use any libraries that run on the JVM (Java, Groovy, Scala, Jruby, Jython, etc).
- many parts of the Clojure standard library, Clojure.core defer to the Java Standard library, for example for I/O (reading,writing files)
- Clojure makes invoking Java very convenient and provides special primitive constructs in the Clojure language to do so (new .javaMethodName javaClassName. etc)
ClojureScript generated JavaScript that will run in a browser. ClojureCLR will compile to bytecode that runs on the Microsoft .Net platform.
"},{"location":"introduction/concepts/features/#managed-state-changes","title":"Managed State Changes","text":"Using atoms
or refs
in clojure you can have mutable data. Changes are done safely within Software Transactional Memory (STM), like having an in-memory ACID database managing access
"},{"location":"introduction/concepts/features/#extend-the-language-with-macros","title":"Extend the language with Macros","text":"Clojure uses macros
Fixme Review the following content to see if its relevant ?
** Input & output with functional programming
- other fp languages like haskel & Scala use monads to encapsulate data changes whilst appearing stateless to the rest of the program - monads allow us to sneak in impure code into the context of pure code.
- Clojure doesn't try and enforce functional purity, so any function can include impure code
- most functions should be pure though or you loose the benefits of functional programming
- Clojure encourages minimal state changes / mutable state - so its up to the developer to keep the ratio of mutable data small
-
Clojure uses reference types to manage threads and mutable state. References provide synchronisation of threads without using locks (notoriously cumbersome). See STM
-
Supporting concurrency
-
atoms etc
- automatic management of state changes via Software transactional memory - like having an ACID database in memory, managing requests to change values over time.
-
by having immutable data structures - if your values do not change then its trivial to have massive parallelism.
-
A modern LISP
-
leaner syntax and not as many brackets as LISP
- clean data structure syntax at the core of the language
- LiSP was the first language to introduce first class functions, garbage collection and dynamic typing, which are common in languages used today
Macros
- a function that takes in source code and returns source code, replacing the macro code
- use macros to take out repetition / boilerplate code
- as LISP syntax is extremely simple it is much easier to write macros that work compared to non-LISP languages
Clojure emphasizes safety in its type system and approach to parallelism, making it easier to write correct multi-threaded programs.
Clojure is very concise, requiring very little code to express complex operations.
Data centric design - a well constructed data structure helps define and clarify the purpose of the code
Modularity - Clojure and its community build things in modules / components that work together (in a similar design approach to the Unix file system, for example).
It offers a REPL and dynamic type system: ideal for beginners to experiment with, and well-suited for manipulating complex data structures.
A consistently designed standard library and full-featured set of core data types rounds out the Clojure toolbox.
Clojure is close to the speed of Java
"},{"location":"introduction/concepts/features/#constraints","title":"Constraints","text":"Clojure relies on the JVM so there can be a longer boot time than a scripting language like Javascript. However, as you can connect to the Clojure runtime (the REPL) of a live system and because Clojure is dynamic, you can make changes to that live system without any downtime.
If you require more performance from Clojure, you can specify ahead of time compilation.
"},{"location":"introduction/concepts/functional-reactive-programming/","title":"Functional Reactive Programming","text":"Functional Reactive programming is used in ClojureScript with libraries including reagent, re-frame, rum, etc.
Functional Reactive Programming is an elegant means of modeling state over time in an easily composable way. Approaching the problem conceptually and developing a formal semantics first can lead to better optimization potential and simpler implementation.
Taking a functional reactive programming approach results in systems that are:
- Easier to reason about
- Simpler to implement
- Easier to optimize
Functional Reactive Programming (FRP) is a specific formalism of a model of behaviors that change in response to events, created by Conal Elliot. That's a pretty abstract statement, but FRP is abstract itself. It does not denote a particular implementation, but rather a formal semantics. It is not a style of programming or a paradigm. It's simply a formalism.
Functional Reactive Programming (and Denotational Design, also by Conal Elliott) has a lot to teach us about how to design functional programs.
"},{"location":"introduction/concepts/functional-reactive-programming/#conal-elliott-on-frp-audio-interview","title":"Conal Elliott on FRP Audio Interview","text":"If you're looking for an explanation of the Functional Reactive Programming from the man who invented it, along with an idea of the intriguing process he used to invent it, this HaskellCast episode is for you.
"},{"location":"introduction/concepts/functional-reactive-programming/#functional-reactive-animation","title":"Functional Reactive Animation","text":"A great paper from 1997 that spells out an early form of Functional Reactive Programming. It specifies behaviors (functions of time to a value) that change when events occur.
"},{"location":"introduction/concepts/functional-reactive-programming/#conal-elliots-home-page","title":"Conal Elliot's home page","text":"Conal Elliot created FRP while he was researching graphics and animation. Be sure to check out his FRP papers section.
"},{"location":"introduction/concepts/functional-reactive-programming/#push-pull-functional-reactive-programming","title":"Push-pull functional reactive programming","text":"A more advanced formulation of Functional Reactive Programming that formalizes the types and operations using Haskell's common type classes (Functor, ApplicativeFunctor, Monoid, etc). This one also includes a video of the paper presentation given at ICFP.
The main breakthrough of this paper is to model the notion of a future value for events that have not yet happened. But if they have not happened, how can future values be compared? For instance, how does one ask if event a happens before event b if neither of them has happened yet? The paper develops a cool and inspiring formalism which resolves this problem. And one is left with a value that represents the entire behavior of a system past, present, and future.
"},{"location":"introduction/concepts/functional-reactive-programming/#elm-thesis-pdf","title":"Elm Thesis - PDF","text":"Elm is a different take on FRP (and it is potentially not FRP, according to some). Instead of behaviors (functions of time to a value), Elm uses discreet signals which are transformed to other signals using functional map-like operations. It also solves a real issue with computationally expensive operations blocking the propagation of signals by making some signals asynchronous.
All-in-all, the thesis is a pleasure to read. It is very clear and a decent introduction to the myriad implementations of FRP out there. See the bibliography.
"},{"location":"introduction/concepts/misc/","title":"Misc","text":""},{"location":"introduction/concepts/misc/#characteristics","title":"Characteristics","text":" - Dynamic
- typed - like Python, Ruby or Groovy
- because its a LISP - you can redefine running code by redefining functions and re-evaluating
-
REPL - a fast way to explore your problem domain with code
-
Functional programming
- in contrast to imperative programing
- immutable data structures at its core, everything is immutable by default
- if any piece of data can be changed, that is mutable state
- in imperative programming, we change state where ever we like
- in functional programming we avoid changing state as much as possible
- if a function does not change state it is referentially transparent, always returning the same result when given the same input (arguments) - often returned as a pure function
- impure functions can affect other functions and therefore has to be very mindful of the changes it makes and when it makes them
- pure functions are truly modular as they do not affect any other part of the system ** Changing state
- rather than changing a data structure, fp instead creates a new data structure that contains the changes and copies of the existing data.
- to manage the potential overhead of copying data structures, Clojure uses Persistent collections (Lists, Vectors, Maps) which are immutable but provide an efficient way to mutate by sharing common elements (data) ** Input & output with functional programming
- other fp languages like haskel & Scala use monads to encapsulate data changes whilst appearing stateless to the rest of the program - monads allow us to sneak in impure code into the context of pure code.
- Clojure doesn't try and enforce functional purity, so any function can include impure code
- most functions should be pure though or you loose the benefits of functional programming
- Clojure encourages minimal state changes / mutable state - so its up to the developer to keep the ratio of mutable data small
-
Clojure uses reference types to manage threads and mutable state. References provide synchronisation of threads without using locks (notoriously cumbersome). See STM
-
Hosted on the Java Virtual Machine
- written for the JVM & heavily integrated, giving beautiful integration
- Clojure is compiled to Java byte code
- many parts of the Clojure standard library, Clojure.core defer to the Java Standard library, for example for I/O (reading,writing files)
-
Clojure makes invoking Java very convenient and provides special primitive constructs in the Clojure language to do so (new .javaMethodName javaClassName. etc)
-
Supporting concurrency
- atoms etc
- automatic management of state changes via Software transactional memory - like having an ACID database in memory, managing requests to change values over time.
-
by having immutable data structures - if your values do not change then its trivial to have massive parallelism.
-
A modern LISP
- leaner syntax and not as many brackets as LISP
- clean data structure syntax at the core of the language
- LiSP was the first language to introduce first class functions, garbage collection and dynamic typing, which are common in languages used today
Macros
- a function that takes in source code and returns source code, replacing the macro code
- use macros to take out repetition / boilerplate code
- as LISP syntax is extremely simple it is much easier to write macros that work compared to non-LISP languages
fixme assuming you need more, I'll add to this page, but Clojure is a very powerful language, incredibly flexible and tonnes of fun. What more do you need ?
fixme concepts to explore
Clojure emphasizes safety in its type system and approach to parallelism, making it easier to write correct multi-threaded programs.
Clojure is very concise, requiring very little code to express complex operations.
Data centric design - a well constructed data structure helps define and clarify the purpose of the code
Modularity - Clojure and its community build things in modules / components that work together (in a similar design approach to the Unix file system, for example).
It offers a REPL and dynamic type system: ideal for beginners to experiment with, and well-suited for manipulating complex data structures.
A consistently designed standard library and full-featured set of core data types rounds out the Clojure toolbox.
Clojure is close to the speed of Java
"},{"location":"introduction/concepts/misc/#constraints","title":"Constraints","text":"Clojure relies on the JVM so there can be a longer boot time than a scripting language like Javascript. However, as you can connect to the Clojure runtime (the REPL) of a live system and because Clojure is dynamic, you can make changes to that live system without any downtime.
If you require more performance from Clojure, you can specify ahead of time compilation.
"},{"location":"introduction/concepts/naming-local/","title":"Naming - local scope","text":""},{"location":"introduction/concepts/naming-local/#local-names-in-functions","title":"Local names in functions","text":"You can define names for things within the scope of a function using the let
function.
"},{"location":"introduction/concepts/naming-local/#example","title":"Example","text":"You can use the let function to define a simple expression, for which everything will go out of scope once it has been evaluated
(let [local-name \"some value\"])\n(let [minutes-in-a-day (* 60 60 24)])\n
You can also use let
inside a function to do something with the arguments passed to that function. Here we calculate the hourly-rate from a yearly salary, returning the calculated-rate.
(defn hourly-rate [yearly-salary weeks-in-year days-in-week] (let [calculated-rate (/ yearly-salary weeks-in-year days-in-week)] calculated-rate))
(hourly-rate 60000 48 5)
## Local names in data structures\n\n When defining a map you are creating a series of key value pairs. The key is essentially a name that represents the value it is paired with. Keys are often defined using a `:keyword`.\n\n```clojure\n {:radius 10, :pi 22/7 :colour purple}\n\n (def my-circle {:radius 10, :pi 22/7 :colour purple})\n
Fixme This is incorrect, as a Clojure keyword type (a name starting with :) have global scope within a namespace. If the keys were strings, then they would have the scope of just the collection.
"},{"location":"introduction/concepts/naming-things/","title":"Naming things - data structures and functions","text":"The def
function is used to name data structures in Clojure.
You can also use def
to name functions, however it is more common to use defn
(which is a macro around def) to give a function a name.
"},{"location":"introduction/concepts/naming-things/#keeping-things-private","title":"Keeping things private","text":"There is less emphasis on keeping functions and data structures private (compared to Java, C++, C#). If you want to define a function name so that it is only accessible by other functions of the same namespace, you can use the defn-
function.
There is no private equivalent for def
(as of Clojure 1.6) however you can use metadata to specify this
(def ^:private name data)
TODO: check if there is anything new around this or other common practices
"},{"location":"introduction/concepts/naming-things/#misc-writing-a-private-def-macro","title":"Misc - writing a private def macro","text":"You could write your own macro to create a private def
called def-
(defmacro def- [item value]\n `(def ^{:private true} ~item ~value)\n)\n
There are no naming conventions for a private symbol name. As its defined an used within the scope of that one namespace (file), then there is no real need to make a special convention. Private functions will just be called as normal within the namespace and it will be quite clear from the function definition that it is private.
Clojure community style guide
"},{"location":"introduction/concepts/naming-things/#example","title":"example","text":"Learning Clojure #4: private functions http://tech.puredanger.com/2010/02/09/clojure-4-private-functions/
Sometimes in a Clojure file you just want some helper functions that shouldn\u2019t be exposed outside the namespace. You can create a private function using the special defn- macro instead of defn.
For instance, create a file foo/bar.clj with a public and a private function:
(ns foo.bar) (defn- sq [x] (* x x)) (defn sum-squares [a b] (+ (sq a) (sq b)))
Then use it from the REPL:
user=> (use 'foo.bar) nil user=> (sum-squares 3 4) 25 user=> (sq 5) java.lang.Exception: Unable to resolve symbol: sq in this context (NO_SOURCE_FILE:6)
"},{"location":"introduction/concepts/purpose/","title":"When to use Clojure","text":"Clojure is a general purpose language suitable for any kind of application or service. As Clojure implementations run across multiple technology platforms and operating systems, there are very few barriers to its use.
So Clojure is great for webapps, data science, big data, finance industry (banking, trading, insurance, etc), devops tools (log analysis, etc) and anything else really.
There are areas where Clojure obviously excels.
"},{"location":"introduction/concepts/purpose/#effective-data-manipulation","title":"Effective Data Manipulation","text":"Fundamentally all software systems take in data (in the form of values or events), process or react to that data and return as a result.
The persistent data structures in Clojure (list, vector, hash-map and set) provide an efficient way to use immutable collections of data.
The clojure.core
library contains a vast number of data processing functions in Clojure so data is easily transformed
"},{"location":"introduction/concepts/purpose/#highly-scalable","title":"Highly Scalable","text":"Clojure code is encouraged to be immutable and functions to be pure, you can run millions of parallel instances of your application or service for massive processing power. These features also vastly simplify concurrent programming.
"},{"location":"introduction/concepts/purpose/#reducing-complexity","title":"Reducing Complexity","text":"Clojure encourages a component design through functional composition, breaking down problems into components
Clojure and its libraries are all great examples of well designed components and the community strongly encourages this approach.
"},{"location":"introduction/concepts/purpose/#hintfunctional-reactive-programming","title":"Hint::Functional Reactive Programming","text":"You can also use ClojureScript for Functional Reactive programming of client-side apps for browsers and mobile device.
"},{"location":"introduction/concepts/rationale/","title":"The rationale for Clojure","text":"At the time Clojure was created there were no LISP based languages that ran on a widely adopted platform, that also made concurrency easier for the developer to manage.
Developers and the companies that hire them are comfortable with the performance, security and stability of the Java Virtual Machine.
While Java developers may envy the succinctness, flexibility and productivity of dynamic languages, they have concerns about running on customer-approved infrastructure, access to their existing code base and libraries, and performance. In addition, they face ongoing problems dealing with concurrency using native threads and locking. Clojure is an effort in pragmatic dynamic language design in this context. It endeavors to be a general-purpose language suitable in those areas where Java is suitable. It reflects the reality that, for the concurrent programming future, pervasive, uncontrolled mutation simply has to go.
Clojure meets its goals by: embracing an industry-standard, open platform - the JVM; modernizing a venerable language - Lisp; fostering functional programming with immutable persistent data structures; and providing built-in concurrency support via software transactional memory and asynchronous agents. The result is robust, practical, and fast.
Clojure has a distinctive approach to state and identity.
Why Clojure?
"},{"location":"introduction/concepts/rationale/#motivating-ideas-behind-clojure","title":"Motivating ideas behind Clojure","text":"A LISP base language design is very effecitve
Standard Lisps (Common Lisp, Scheme) have not evolved over time, since standardisation. Their core data structures are mutable and not extensible and therefore no mechanisms for effectively dealing with concurrency.
Clojure is a Lisp not constrained by backwards compatibility, allowing modernisation of the language that otherwise deters developers from adoption.
Clojure extends the code-as-data paradigm to maps and vectors
All data structures default to immutability
Core data structures are extensible abstractions
Embraces a platform (JVM)
Functional programming is a good thing
- Immutable data + first-class functions
- Could always be done in Lisp, by discipline/convention
But if a data structure can be mutated, dangerous to presume it won't be In traditional Lisp, only the list data structure is structurally recursive Pure functional languages tend to strongly static types Not for everyone, or every task Clojure is a functional language with a dynamic emphasis All data structures immutable & persistent, supporting recursion Heterogeneous collections, return types Dynamic polymorphism Languages and Platforms VMs, not OSes, are the platforms of the future, providing: Type system Dynamic enforcement and safety Libraries Abstract away OSes Huge set of facilities Built-in and 3rd-party Memory and other resource management GC is platform, not language, facility Bytecode + JIT compilation Abstracts away hardware Language as platform vs. language + platform Old way - each language defines its own runtime GC, bytecode, type system, libraries etc New way (JVM, .Net) Common runtime independent of language Language built for platform vs language ported-to platform Many new languages still take 'Language as platform' approach When ported, have platform-on-platform issues Memory management, type-system, threading issues Library duplication If original language based on C, some extension libraries written in C don't come over Platforms are dictated by clients 'Must run on JVM' or .Net vs 'must run on Unix' or Windows JVM has established track record and trust level Now also open source Interop with other code required C linkage insufficient these days Java/JVM is a language and platform Not the original story, but other languages for JVM always existed, now embraced by Sun Java can be tedious, insufficiently expressive Lack of first-class functions, no type inference, etc Ability to call/consume Java is critical Clojure is the language, JVM the platform Object Orientation is overrated Born of simulation, now used for everything, even when inappropriate Encouraged by Java/C# in all situations, due to their lack of (idiomatic) support for anything else Mutable stateful objects are the new spaghetti code Hard to understand, test, reason about Concurrency disaster Inheritance is not the only way to do polymorphism \"It is better to have 100 functions operate on one data structure than to have 10 functions operate on 10 data structures.\" - Alan J. Perlis Clojure models its data structures as immutable objects represented by interfaces, and otherwise does not offer its own class system. Many functions defined on few primary data structures (seq, map, vector, set). Write Java in Java, consume and extend Java from Clojure. Polymorphism is a good thing Switch statements, structural matching etc yield brittle systems Polymorphism yields extensible, flexible systems Clojure multi-methods decouple polymorphism from OO and types Supports multiple taxonomies Dispatches via static, dynamic or external properties, metadata, etc Concurrency and the multi-core future Immutability makes much of the problem go away Share freely between threads But changing state a reality for simulations and for in-program proxies to the outside world Locking is too hard to get right over and over again Clojure's software transactional memory and agent systems do the hard part
In short, I think Clojure occupies a unique niche as a functional Lisp for the JVM with strong concurrency support. Check out some of the features.
"},{"location":"introduction/concepts/what-is-functional-programming/","title":"What is Functional Programming","text":"Functional programming can seem quite different from imperative programming used in languages like C, C++ and Java.
Imperative languages may seem easier initially, as defining one step after another is familiar approach to many things in live. As the scale of a system grows, so does complexity. Imperative languages applied object oriented design to manage complexity with varied rates of success.
When shared mutable state is common in an OO design, then a system quickly becomes complex and very difficult to reason about.
Functional programming is actually simpler that the OO approach, although initially it may be unfamiliar and not considered as easy. As systems grow in complexity, the building blocks are still simple and deterministic, creating a system that is far easier to reason about.
"},{"location":"introduction/concepts/what-is-functional-programming/#imperative-programming-languages","title":"Imperative programming languages","text":"In Imperative languages code is written that specifies a sequential of instructions that complete a task. These instructions typically modifies program state until the desired result is achieved.
Variables typically represent memory addresses that are mutable (can be changed) by default.
"},{"location":"introduction/concepts/what-is-functional-programming/#functional-programming-languages","title":"Functional programming languages","text":"Individual tasks are small and achieved by passing data to a function which returns a result.
Functions are composed together to form more complex tasks and satisfy larger business logic. These composed functions pass the result of their evaluation to the next function, until all functions in the composition have been evaluated.
The entire functional program can be thought of as a single function defined in terms of smaller ones.
Program execution is an evaluation of expressions, with the nesting structure of function composition determining program flow.
Data is immutable and cannot be change once created. Changes are expressed as new values, with complex values sharing common values for efficiency.
"},{"location":"iterate-over-data/","title":"Iterate over data","text":"Clojure data is typically within one or more of the built in collection types (vector, map, list, set).
We can use some functions in Clojure core directly on these collection types. Other clojure core functions need a little help.
"},{"location":"iterate-over-data/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"iterate-over-data/#map","title":"map","text":"Used to create a new collection by applying a given function to each element of the collection in turn.
(map inc [1 2 3])\n
If there are multiple collections, map returns a new collection with values created by calling the function with a value from each of the collections. Once map reaches the end of one collection it stops and returns the result.
(map + [1 2 3] [4 5 6] [7 8 9])\n
"},{"location":"iterate-over-data/#apply","title":"apply","text":"Used to remove all the values from a collection so they are treated as individual arguments to the function given to apply.
(= (apply + [1 2 3])\n (+ 1 2 3))\n
"},{"location":"iterate-over-data/#reduce","title":"reduce","text":"reduce can be used in a similar way as apply, to transform a collection into a different value.
reduce can also take an argument referred to as an accumulator, used to keep local state as reduce iterates through the values in the collection.
A function used with reduce is called a reducing function and is a more abstract approach to loop/recur although its possible to give your reducing function a name so is more reusable.
"},{"location":"iterate-over-data/#threading-macros","title":"threading macros","text":"Write code that reads as a sequential series of function calls, rather that the nested function calls typical in lisp.
A threading macro is often used to thread a collection through a number of function calls and expressions.
"},{"location":"iterate-over-data/#comp","title":"comp","text":"Compose functions together that work over a collection. It can be seen as a more abstract approach to a threading macro or nested function calls.
"},{"location":"iterate-over-data/#transduce","title":"transduce","text":"Used like comp to create a pipeline of function calls, however, each function call or expression must return a transducer (transforming reduction). Many clojure.core
functions return a transducer if you do not provide the collection argument.
"},{"location":"iterate-over-data/apply/","title":"apply","text":""},{"location":"iterate-over-data/apply/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"iterate-over-data/comp/","title":"comp - composing functions","text":""},{"location":"iterate-over-data/comp/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"iterate-over-data/filter-remove/","title":"filter and remove","text":"Use filter and remove with predicate functions, those returning true or false, to create a sub-set of the data.
filter creates a new collection that contains all the matching values from the predicate function (true).
remove
creates a new collection with contains all the values that didn't match the predicate function (false).
(filter odd? [1 2 3 4 5 6 7 8 9])\n
"},{"location":"iterate-over-data/filter-remove/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"iterate-over-data/map-fn/","title":"map with fn - anonymous function","text":"(map (fn [arg] (+ arg 5)) [1 2 3 4 5])\n
There is a syntactic short-cut for the anonymous function that does not require a name for the arguments
#(+ %1 5)
Adding this into our previous expression we can see that its still quite readable and helps keep the code clean.
(map #(+ arg 5) [1 2 3 4 5])\n
"},{"location":"iterate-over-data/map-fn/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"iterate-over-data/map-fn/#hintanonymous-function-name","title":"Hint::Anonymous function name","text":"Anonymous functions do not have an externally referable name, so must be used in-line with an expression.
The fn
function can be defined with a name, however, this is only available in the scope of that function definition, the name cannot be used to refer to that function outside of its definition.
Including a name within a fn
definition enables the function to call itself, therefore creating an anonymous recursive function.
"},{"location":"iterate-over-data/map-partial/","title":"map with partial","text":""},{"location":"iterate-over-data/map-partial/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"iterate-over-data/map/","title":"map function","text":""},{"location":"iterate-over-data/map/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"Add examples of using the map function with data
"},{"location":"iterate-over-data/reduce/","title":"reduce","text":""},{"location":"iterate-over-data/reduce/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"iterate-over-data/transduce/","title":"transduce and transforming reducers","text":""},{"location":"iterate-over-data/transduce/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"libraries/","title":"Libraries","text":""},{"location":"libraries/clojars/","title":"Clojars","text":""},{"location":"libraries/clojure-core-lisp-comprehension/","title":"Libraries: clojure.core
lisp comprehension","text":""},{"location":"libraries/clojure-core-lisp-comprehension/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"discuss functions for list comprehension for
"},{"location":"libraries/clojure-core/","title":"Understanding Clojure Libraries: clojure.core
","text":""},{"location":"libraries/clojure-core/#warningvery-rough-draft-of-an-idea","title":"Warning::Very rough draft of an idea","text":"Experimenting with the value of grouping functions in clojure.core to help ensure you are exposed to most of the concepts in Clojure
"},{"location":"libraries/clojure-core/#families-of-functions","title":"Families of functions","text":" - List Comprehension (for, while, )
-
used to process multiple collections
-
Transformations (map, filter, apply, reduce )
- transform the contents of a collection
"},{"location":"libraries/edn/","title":"edn","text":""},{"location":"libraries/om/","title":"om","text":""},{"location":"modifying-data-structures/","title":"Modifying data structures","text":"Wait, I thought you said that data structures were immutable! So how can we change them then?
Yes, lists, vectors, maps and sets are all immutable. However, you can get a new data structure that has the changes you want. To make this approach efficient, the new data structure contains only the new data and links back to the existing data structure for shared data elements.
We will see some of the most common functions that work with data structures in this section. In actuality, everything can be considered a function that works on a data structure though, as that is the language design of clojure.
"},{"location":"modifying-data-structures/lists/","title":"Lists","text":"You can change lists with the cons
function, see (doc cons)
for details
(cons 5 '(1 2 3 4))
You will see that cons
does not change the existing list, it create a new list that contains the number 5 and a link to all the elements of the existing list.
You can also use cons on vectors (cons 5 [1 2 3 4])
(cons \"fish\" '(\"and\" \"chips\"))\n\n(conj '(1 2 3 4) 5)\n\n(conj [1 2 3 4] 5)\n\n\n;; Lets define a simple list and give it a name\n(def list-one '(1 2 3))\n\n;; the name evaluates to what we expect\nlist-one\n\n;; If we add the number 4 using the cons function, then we\n;; get a new list in return, with 4 added to the front (because thats how lists work with cons)\n(cons 4 list-one)\n\n;; If we want to keep the result of adding to the list, we can assign it a different name\n(def list-two (cons 4 list-one))\n;; and we get the result we want\nlist-two\n\n;; we can also pass the original name we used for the list to the new list\n(def list-one (cons 4 list-one))\n\n;; If we re-evaluate the definition above, then each time we will get an extra\n;; number 4 added to the list.\n\nlist-one\n\n;; Again, this is not changing the original list, we have just moved the name\n;; of the list to point to the new list.\n;; Any other function working with this data structure before reassigning the name\n;; will not be affected by the re-assignment and will use the unchanged list.\n
"},{"location":"modifying-data-structures/maps/","title":"Maps","text":"The conj
function works on all of the Clojure collections. The map collection also has functions that affect the evaluation of a map and the value of map returned.
"},{"location":"modifying-data-structures/maps/#adding-new-values-with-conj","title":"Adding new values with conj
","text":"If you have a collection of maps, you can add another map to that collection with the conj
function.
(conj [{:map1 1}] {:map2 2})\n\n;; => [{:map1 1} {:map2 2}]\n
"},{"location":"modifying-data-structures/maps/#changing-values-with-assoc","title":"Changing values with assoc
","text":"The assoc
function is used to update a value in a map without necessary being concerned about the current value. assoc
returns a complete new map with the specified value.
(assoc {:food \"Fish\"} :food \"Fish&Chips\")\n\n;; => {:food \"Fish&Chips\"}\n
It does not matter how many keys are in the map, as keys are unique, then assoc
will look up the specific key and change its value to that specified in the third argument.
If a key is not in the map, assoc
will add both the key and the value.
(def alphabet-soup {:a 1 :b 2 :c 3})\n\n(assoc alphabet-soup :d 4)\n\n;; => {:a 1, :b 2, :c 3, :d 4}\n
If there are multiple levels to the structure of your map, ie. the value of a key in the map is also a map
For example, the value of :luke
in the star-wars-characters
map is represented as a map too {:fullname \"Luke Skywalker\" :skill \"Targeting Swamp Rats\"}
(def star-wars-characters\n {:luke {:fullname \"Luke Skywalker\" :skill \"Targeting Swamp Rats\"}\n :vader {:fullname \"Darth Vader\" :skill \"Crank phone calls\"}\n :jarjar {:fullname \"JarJar Binks\" :skill \"Upsetting a generation of fans\"}})\n
To update the skill of one of the characters we can use assoc-in
to update the correct value by traversing the map via the given keys.
(assoc-in star-wars-characters [:vader :skill] \"The Dark Side of the Force\")\n\n;; => {:luke {:fullname \"Luke Skywalker\", :skill \"Targeting Swamp Rats\"},\n :vader {:fullname \"Darth Vader\", :skill \"The Dark Side of the Force\"},\n :jarjar {:fullname \"JarJar Binks\", :skill \"Upsetting a generation of fans\"}}\n
"},{"location":"modifying-data-structures/maps/#update-values-in-a-map-with-update","title":"Update values in a map with update
","text":"Rather than replace the current value with one specified, update
applies a function to the existing value in order to update that value.
(def alphabet-soup {:a 1 :b 2 :c 3})\n\n(update alphabet-soup :a inc)\n\n;; => {:a 2, :b 2, :c 3}\n
Hint As with assoc
you can also use update
on nested maps using the update-in
function.
"},{"location":"modifying-data-structures/sets/","title":"Sets","text":""},{"location":"modifying-data-structures/vectors/","title":"Vectors","text":""},{"location":"performance/","title":"Clojure Performance and benchmarks","text":"There are several aspects to performance testing
- time taken by individual functions / expressions
- time through a specific path in your application
- response times under different loads
The purpose of performance testing and bench-marking is to understand the expected behaviour of your application under various usage patterns. This kind of testing can also suggest areas of the application that might benefit from optimisation
"},{"location":"performance/#performance-tools-for-clojure","title":"Performance tools for Clojure","text":" - Criterium - benchmarks for Clojure expressions
- Gatling
"},{"location":"performance/#gatling","title":"Gatling","text":"The Gatling Project is a free and open source performance testing tool. Gatling has a basic GUI that's limited to test recorder only. However, the tests can be developed in easily readable/writable domain-specific language (DSL).
Key Features of Gatling:
- HTTP Recorder
- An expressive self-explanatory DSL for test development
- Scala-based
- Production of higher load using an asynchronous non-blocking approach
- Full support of HTTP(S) protocols and can also be used for JDBC and JMS load testing
- Multiple input sources for data-driven tests
- Powerful and flexible validation and assertions system
- Comprehensive informative load reports
"},{"location":"performance/#reference-other-performance-tools","title":"Reference: Other performance tools","text":"Other notable performance tools include:
- The Grinder
- Apache JMeter (Java desktop app)
- Tsung (required Erlang)
- Locust (python)
Key Features of The Grinder:
- TCP proxy to record network activity into the Grinder test script
- Distributed testing that scales with an the increasing number of agent instances
- Power of Python or Closure, combined with any Java API, for test script creation or modification
- Flexible parameterization, which includes creating test data on the fly and the ability to use external data sources like files and databases
- Post-processing and assertion with full access to test results for correlation and content verification
- Support of multiple protocols
Key Features of JMeter:
- Desktop GUI tool
- Cross-platform. JMeter can run on any operating system with Java
- Scalable. When you need a higher load than a single machine can create, JMeter can execute in a distributed mode, meaning one master JMeter machine controls a number of remote hosts.
- Multi-protocol support. The following protocols are all supported out-of-the-box: HTTP, SMTP, POP3, LDAP, JDBC, FTP, JMS, SOAP, TCP
- Multiple implementations of pre- and post-processors around sampler. This provides advanced setup, teardo* wn parametrization, and correlation capabilities
- Various assertions to define criteria
- Multiple built-in and external listeners to visualize and analyze performance test results
- Integration with major build and continuous integration systems, making JMeter performance tests part of the full software development life cycle
- Extensions via plugins
"},{"location":"performance/#resources","title":"Resources","text":" - Open source load testing tools - which one should you use
"},{"location":"performance/load-testing/","title":"Load Testing","text":"The Gatling project can be used to create and run load tests using Clojure (and get fancy reports).
Add the following to your project.clj :dependencies
[clj-gatling \"0.11.0\"]\n
"},{"location":"performance/load-testing/#describe-a-load-test","title":"Describe a Load Test","text":"This is how we would write a simple load test which performs 50 GET requests against a server running at test.com:
class SimpleSimulation extends Simulation {\n //declare a scenario with a simple get request performed 5 times\n val scn = scenario(\"myScenario\")\n .exec(http(\"myRequest\").get(\"http://test.com/page.html\"))\n .repeat(5)\n\n //run the scenario with 10 concurrent users\n setUp(scn.users(10))\n}\n
Gatling refers to load tests as Simulations which have one or more Scenarios. In the one above we are saying we will have 10 users execute 5 requests each in parallel. We could provide a Content-Type header with the request and check for a 200 response code like this:
http(\"myRequest\")\n .get(\"http://test.com/page.html\")\n .header(\"Content-Type\", \"text/html\")\n .check(status.is(200))\nIf we wanted to do a POST request with a JSON body and basic authentication, as well as verify something in the response:\n\nhttp(\"myRequest\")\n .post(\"http://test.com/someresource\"))\n .body(StringBody(\"\"\"{ \"myContent\": \"myValue\" }\"\"\"))\n .asJSON\n .basicAuth(\"username\", \"password\")\n .check(jsonPath(\"$..someField\").is(\"some value\"))\n
The expression used to extract someField from the response is passed to jsonPath() and is based on Goessner\u2019s JsonPath syntax. We use is() to verify the expected value is equal to some value. We can also do other forms of verification on the response json like:
- not(expectedValue): not equal to expectedValue
- in(sequence): to check that a value belongs to the given sequence
- exists(), notExists(): to check for the presence/absence of a field
For a multipart request with 2 parts and gzip compression:
http(\"myRequest\")\n .post(\"http://test.com/someresource\"))\n .bodyPart(StringBodyPart(\"\"\"{ \"myContent\": \"myValue\" }\"\"\"))\n .bodyPart(RawFileBodyPart(\"file\", \"test.txt\")\n .processRequestBody(gzipBody)\nWe can also create scenarios with multiple requests and use the result from previous requests in subsequent requests like this:\n\nscenario(\"myScenario\")\n .exec(http(\"request1\")\n .post(\"http://test.com/resource1\")\n .body(StringBody\"\"\"{ \"myContent\": \"\"}\"\"\")\n .check(jsonPath(\"$..myResponse.guid\").saveAs(\"guid\")))\n .exec(http(\"request2\")\n .put(\"http://test.com/resource2/${guid}\")\n .body(StringBody\"\"\"{ \"someOtherField\": \"\"}\"\"\"))\n
guid is extracted from the response of the first call using saveAs(\"guid\") and used in the path to the PUT call.
Scenarios can also be run with a ramp up. If we wanted to run the scenario above with 1000 users with a ramp up of 20 seconds we would do:
setUp(scn.users(1000).ramp(20))\n
"},{"location":"performance/load-testing/#run-a-simulation","title":"Run a Simulation","text":"There are a number of ways to run Gatling simulations. You can download the bundle, place your simulations under the user-files/simulations directory and then run bin/gatling.sh.
If you prefer integration with your build system there are plugins for Maven, Gradle and SBT. For example, for Maven we just add the dependencies in the pom.xml:
<dependencies>\n <dependency>\n <groupId>io.gatling.highcharts</groupId>\n <artifactId>gatling-charts-highcharts</artifactId>\n <scope>test</scope>\n </dependency>\n</dependencies>\n\n<build>\n <plugins>\n <plugin>\n <groupId>io.gatling</groupId>\n <artifactId>gatling-maven-plugin</artifactId>\n </plugin>\n </plugins>\n</build>\n
Place simulations under src/test/scala/com/company/service and then in the terminal:
mvn gatling:execute -Dgatling.simulationClass=com.company.service.YourSimulation\n
"},{"location":"performance/testing-functions/","title":"Testing Functions","text":""},{"location":"performance/testing-functions/#adding-the-criterium-library","title":"Adding the Criterium library","text":"Add [criterium \"0.4.4\"]
to you project.clj
file.
Add criterium to the namespace were you run your tests
(ns ,,,\n :require [criterium.core] :refer [quick-bench])\n
Or simply require criterium in the REPL
(require '[criterium.core] :refer [quick-bench])\n
"},{"location":"performance/testing-functions/#using-criterium-to-test-code","title":"Using Criterium to test code","text":"Lets try a few similar Clojure functions to see the Criterium benchmark in action
(let [number 5]\n (quick-bench (cond = 5 1 1 2 2 3 3 4 4 5 5)))\n
Benchmark output is sent to the REPL
Evaluation count : 50788488 in 6 samples of 8464748 calls.\n Execution time mean : 2.535916 ns\n Execution time std-deviation : 0.096838 ns\n Execution time lower quantile : 2.435814 ns ( 2.5%)\n Execution time upper quantile : 2.686146 ns (97.5%)\n Overhead used : 9.431514 ns\n\nFound 1 outliers in 6 samples (16.6667 %)\n low-severe 1 (16.6667 %)\n Variance from outliers : 13.8889 % Variance is moderately inflated by outliers\n
Running the benchmark again for the same expression, we get pretty consistent results
Evaluation count : 50408712 in 6 samples of 8401452 calls.\n Execution time mean : 2.571379 ns\n Execution time std-deviation : 0.163071 ns\n Execution time lower quantile : 2.366952 ns ( 2.5%)\n Execution time upper quantile : 2.721099 ns (97.5%)\n Overhead used : 9.431514 ns\n
There is a parallized version of cond
called condp
.
(let [number 5]\n (quick-bench (condp = 5 1 1 2 2 3 3 4 4 5 5)))\n
Evaluation count : 3625284 in 6 samples of 604214 calls.\n Execution time mean : 156.813816 ns\n Execution time std-deviation : 2.560629 ns\n Execution time lower quantile : 154.222522 ns ( 2.5%)\n Execution time upper quantile : 160.425030 ns (97.5%)\n Overhead used : 9.431514 ns\n\nFound 1 outliers in 6 samples (16.6667 %)\n low-severe 1 (16.6667 %)\n Variance from outliers : 13.8889 % Variance is moderately inflated by outliers\n
That figure is quite high, lets run that again.
Evaluation count : 3707922 in 6 samples of 617987 calls.\n Execution time mean : 154.219102 ns\n Execution time std-deviation : 3.427811 ns\n Execution time lower quantile : 149.777377 ns ( 2.5%)\n Execution time upper quantile : 159.225180 ns (97.5%)\n Overhead used : 9.431514 ns\n
So using a parallized version of a function adds a significant exectution time. I believe the extra time is due to setting up a thread. If so, then when using condp
you only get a more effective throughput when running multiple parallel threads, which should be fairly obvious.
Now lets benchmark a similar function called case
. This function is nicely optimised on the JVM especially when the values are sequential, so we should see faster results
(let [number 5]\n (quick-bench (case 5 1 1 2 2 3 3 4 4 5 5)))\n
Benchmark output is sent to the REPL
Evaluation count : 56533626 in 6 samples of 9422271 calls.\n Execution time mean : 1.158650 ns\n Execution time std-deviation : 0.187322 ns\n Execution time lower quantile : 1.021431 ns ( 2.5%)\n Execution time upper quantile : 1.471115 ns (97.5%)\n Overhead used : 9.431514 ns\n\nFound 1 outliers in 6 samples (16.6667 %)\n low-severe 1 (16.6667 %)\n Variance from outliers : 47.5092 % Variance is moderately inflated by outliers\n
"},{"location":"puzzles/","title":"Puzzles","text":"Simple puzzles to help you start thinking functionally
"},{"location":"puzzles/random-seat-assignment/","title":"Random Seat assignment","text":"https://github.com/practicalli/clojure-practicalli-content/issues/4
Take a functional / data oriented approach to solving this problem
"},{"location":"puzzles/random-seat-assignment/#description","title":"Description","text":"You want to randomly assign seating to a number of people for a fixed number of seats. Each seat is represented by an integer number between 1 and 30.
How do you randomly assign seats without choosing the same seat twice.
"},{"location":"puzzles/random-seat-assignment/#loop-recur-approach","title":"Loop / recur approach","text":"Bad...
"},{"location":"puzzles/random-seat-assignment/#recursive-function","title":"recursive function","text":""},{"location":"quickstart/quick-reference/","title":"Clojure Quick Reference","text":"The basic Clojure syntax and a few common functions you should probably learn first.
Also see the Clojure.org cheat-sheet
"},{"location":"quickstart/quick-reference/#calling-functions","title":"Calling functions","text":"The first element in a list, ()
, is treated as a call to a function. This is known as prefix notation which greatly simplifies Clojure syntax and makes mathematical expressions completely deterministic, eliminating the need for operator precedence.
(+ 2 3 5 8 13 (* 3 7))\n(+ 3 (* 2 (- 7 2) 4) (/ 16 4))\n(clojure-version)\n
Functions contain doc-strings and you can ask for a functions documentation, or show the source code.
(doc doc)\n(source doc)\n
Clojure is a dynamically typed language, it is also strongly typed (everything is a type, but you dont have to express the type in your code). The type of anything in Clojure can be returned.
(type 42)\n(type {:hash \"data\" :map \"more data\"})\n
"},{"location":"quickstart/quick-reference/#modeling-data-with-collection-types","title":"Modeling data with Collection types","text":"Clojure has 4 main collection types, all immutable (cannot change once created) and can contain any Clojure types.
(str \"lists used mainly\" (* 2 2) :code)\n\n[0 \"indexed array\"]\n\n{:key \"value\" :pairs \"hash-map\" :aka \"dictionary\"}\n\n#{1 2 3 4 \"unique\" \"set\" \"of\" \"values\" \"unordered\" (* 3 9)}\n
"},{"location":"quickstart/quick-reference/#defining-names-for-values-vars","title":"Defining names for values (vars)","text":"Names can be bound to any values, simple values like numbers, collections or functions. A convenient way to refer to value in your code.
(def public-health-data\n ({:date \"2020-01-01\" :confirmed-cases 23014 :recovery-percent 15}\n {:date \"2020-01-02\" :confirmed-cases 23014 :recovery-percent 15}\n {:date \"2020-01-03\" :confirmed-cases 23014 :recovery-percent 15}))\n\n(def add-hundred (partial + 100))\n
"},{"location":"quickstart/quick-reference/#map-reduce-filter","title":"map reduce filter","text":"Common functions for iterating through a collection / sequence of values
(map * [1 3 5 8 13 21] [3 5 8 13 21 34])\n\n(filter even? [1 3 5 8 13 21 34])\n\n(reduce + [31 28 30 31 30 31])\n
"},{"location":"quickstart/quick-reference/#using-data-structures","title":"Using data structures","text":"Using the map
and inc
function, increment all the numbers in a vector
(map inc [1 2 3 4 5])\n
The above map
function is roughly equivalent to the following expression
(conj [] (inc 1) (inc 2) (inc 3) (inc 4) (inc 5))\n
The conj
function creates a new collection by combining a collection and one or more values.
"},{"location":"quickstart/quick-reference/#defining-custom-functions","title":"Defining custom functions","text":"(defn square-of\n \"Calculates the square of a given number\"\n [number]\n (* number number))\n
Function definitions can also be used within other expressions, useful for mapping custom functions over a collection
(fn [x] (* x x))\n\n(map (fn [x] (* x x)) [1 2 3 4 5])\n
"},{"location":"quickstart/quick-reference/#ratio-type","title":"Ratio Type","text":"; Using the division function (/ ) shows another interesting characteristic of Clojure, the fact that it is lazy. This is not lazy in a bad way, but lazy evaluation of data structures. This actually helps to make clojure more efficient at dealing with data, especially very large data sets.
(/ 22 7)\n22/7\n\n(/ 22 7.0)\n3.142857142857143\n\n(type (/ 22 7))\n
Using a Ratio means that the mathematical division is not evaluated when using whole numbers (Integers) that would produce a decimal number. If you do return a decimal number then what precision of decimal are you expecting. By specifying one or more of the numbers as a decimal value you are giving Clojure a precision to infer and can therefore provide a specific decimal result.
"},{"location":"quickstart/quick-reference/#java-interoperability","title":"Java interoperability","text":".
and new
are Clojure functions that create a Java object. This allows you to use values from Java constants, i.e. PI is a static double from the java.lang.Math object
(. Math PI)\n3.141592653589793\n
Also call static and instance methods from Java objects.
(Math/cos 3)\n\n(javax.swing.JOptionPane/showMessageDialog nil\n \"Hello Java Developers\")\n
"},{"location":"quickstart/quick-reference/#recursion","title":"Recursion","text":"Recursive function
(defn recursive-counter\n [value]\n (if (< value 1000)\n (recur (+ value 25))))\n\n(recursive-counter 100)\n
"},{"location":"reference/","title":"Reference","text":""},{"location":"reference/basic-syntax/","title":"Reference: Basic Syntax","text":""},{"location":"reference/basic-syntax/#notes-from-aphyr","title":"Notes from Aphyr","text":"Let\u2019s write a simple program. The simplest, in fact. Type \u201cnil\u201d, and hit enter.
user=> nil nil nil is the most basic value in Clojure. It represents emptiness, nothing-doing, not-a-thing. The absence of information.
user=> true true user=> false false true and false are a pair of special values called Booleans. They mean exactly what you think: whether a statement is true or false. true, false, and nil form the three poles of the Lisp logical system.
user=> 0 0 This is the number zero. Its numeric friends are 1, -47, 1.2e-4, 1/3, and so on. We might also talk about strings, which are chunks of text surrounded by double quotes:
user=> \"hi there!\" \"hi there!\" nil, true, 0, and \"hi there!\" are all different types of values; the nouns of programming. Just as one could say \u201cHouse.\u201d in English, we can write a program like \"hello, world\" and it evaluates to itself: the string \"hello world\". But most sentences aren\u2019t just about stating the existence of a thing; they involve action. We need verbs.
user=> inc
"},{"location":"reference/basic-syntax/#_1","title":"This is a verb called inc\u2013short for \u201cincrement\u201d. Specifically, inc is a symbol which points to a verb: #\u2013 just like the word \u201crun\u201d is a name for the concept of running.
There\u2019s a key distinction here\u2013that a signifier, a reference, a label, is not the same as the signified, the referent, the concept itself. If you write the word \u201crun\u201d on paper, the ink means nothing by itself. It\u2019s just a symbol. But in the mind of a reader, that symbol takes on meaning; the idea of running.
Unlike the number 0, or the string \u201chi\u201d, symbols are references to other values. when Clojure evaluates a symbol, it looks up that symbol\u2019s meaning. Look up inc, and you get #.
Can we refer to the symbol itself, without looking up its meaning?
user=> 'inc inc Yes. The single quote ' escapes a sentence. In programming languages, we call sentences expressions or statements. A quote says \u201cRather than evaluating this expression\u2019s text, simply return the text itself, unchanged.\u201d Quote a symbol, get a symbol. Quote a number, get a number. Quote anything, and get it back exactly as it came in.
user=> '123 123 user=> '\"foo\" \"foo\" user=> '(1 2 3) (1 2 3) A new kind of value, surrounded by parentheses: the list. LISP originally stood for LISt Processing, and lists are still at the core of the language. In fact, they form the most basic way to compose expressions, or sentences. A list is a single expression which has multiple parts. For instance, this list contains three elements: the numbers 1, 2, and 3. Lists can contain anything: numbers, strings, even other lists:
user=> '(nil \"hi\") (nil \"hi\") A list containing two elements: the number 1, and a second list. That list contains two elements: the number 2, and another list. That list contains two elements: 3, and an empty list.
user=> '(1 (2 (3 ()))) (1 (2 (3 ()))) You could think of this structure as a tree\u2013which is a provocative idea, because languages are like trees too: sentences are comprised of clauses, which can be nested, and each clause may have subjects modified by adjectives, and verbs modified by adverbs, and so on. \u201cLindsay, my best friend, took the dog which we found together at the pound on fourth street, for a walk with her mother Michelle.\u201d
Took Lindsay my best friend the dog which we found together at the pound on fourth street for a walk with her mother Michelle But let\u2019s try something simpler. Something we know how to talk about. \u201cIncrement the number zero.\u201d As a tree:
Increment the number zero We have a symbol for incrementing, and we know how to write the number zero. Let\u2019s combine them in a list:
clj=> '(inc 0) (inc 0) A basic sentence. Remember, since it\u2019s quoted, we\u2019re talking about the tree, the text, the expression, by itself. Absent interpretation. If we remove the single-quote, Clojure will interpret the expression:
user=> (inc 0) 1 Incrementing zero yields one. And if we wanted to increment that value?
Increment increment the number zero user=> (inc (inc 0)) 2 A sentence in Lisp is a list. It starts with a verb, and is followed by zero or more objects for that verb to act on. Each part of the list can itself be another list, in which case that nested list is evaluated first, just like a nested clause in a sentence. When we type
(inc (inc 0)) Clojure first looks up the meanings for the symbols in the code:
(# (# 0)) Then evaluates the innermost list (inc 0), which becomes the number 1:
(# 1) Finally, it evaluates the outer list, incrementing the number 1:
2 Every list starts with a verb. Parts of a list are evaluated from left to right. Innermost lists are evaluated before outer lists.
(+ 1 (- 5 2) (+ 3 4)) (+ 1 3 (+ 3 4)) (+ 1 3 7) 11 That\u2019s it.
The entire grammar of Lisp: the structure for every expression in the language. We transform expressions by substituting meanings for symbols, and obtain some result. This is the core of the Lambda Calculus, and it is the theoretical basis for almost all computer languages. Ruby, Javascript, C, Haskell; all languages express the text of their programs in different ways, but internally all construct a tree of expressions. Lisp simply makes it explicit.
","text":""},{"location":"reference/books/","title":"Books & Tutorials on Clojure","text":"Here is a list of Clojure books, grouped by skill level. There is also a book list on Clojure.org or via a search for Clojure on o'Reilly lists most of these books.
"},{"location":"reference/books/#for-clojure-beginners","title":"For Clojure beginners","text":" - Living Clojure, O'Reilly April 2015
- Clojure for the Brave and the True, NoStartch Press September 2015
- Clojurescript Unraveled - June 2016
- Clojure from the ground up
- Practicalli Clojure
- Practicalli Clojure WebApps
"},{"location":"reference/books/#reference","title":"Reference","text":" - Clojure Programming Cookbook, Packt August 2016
- Clojure Cookbook, O'Reilly March 2014 - hundreds of real-world problems and solutions, ranging from basic utilities to rich web services to heavy data processing
"},{"location":"reference/books/#intermediate-level","title":"Intermediate level","text":" - Web Development with Clojure, Pragmatic July 2016
- Mastering Clojure, Packt March 2016
- Clojure in Action, Manning December 2015
- Programming Clojure, Pragmatic October 2015
- Clojure Applied: From Practice to Practitioner, Pragmatic September 2015
- Joy of Clojure, Manning May 2014
- Clojure Programming, O'Reilly March 2012
"},{"location":"reference/books/#other-books","title":"Other books","text":" - Learning Clojurescript, Packt June 2016
- Professional Clojure, Wiley/Wrox May 2016
- Clojure for Java Developers, Packt February 2016
- Clojure for Finance, January 2016
- Clojure Recipes, Addison-Wesley October 2015
- Clojure for Data Science, Packt September 2015
- Clojure High Performance Programming, Packt September 2015
- Clojure Data Structures & Algorithms, Packt August 2015
- Mastering Clojure Data Analysis, Packt
- Clojure Reactive Programming, Packt March 2015
- Clojure Web Development Essentials, Packt February 2015
- Clojure Data Analysis Cookbook, Packt January 2015
- Mastering Clojure Macros, Pragmatic August 2014
- Clojure for Machine Learning, Packt April 2014
- Clojure for Domain-specific Languages, Packt December 2013
- Clojurescript: Up and Running, O'Reilly October 2012
- Building Web Applications with Clojure, Packt April 2012
- Functional Programming Patterns in Scala and Clojure, Pragmatic August 2014
"},{"location":"reference/changing-state/","title":"Changing State","text":""},{"location":"reference/code-analysis/","title":"Code Analysis","text":"clj-kondo is a lint tool that highlights syntactic errors and suggests idioms for Clojure, ClojureScript and EDN.
Use clj-kondo with your preferred editor to warning about errors as you type so issues can be fixed as soon as they occur, enhancing your joy of Clojure.
clj-kondo can also be used as a command line tool for checking projects in development environments and continuous integration service, such as the setup-clojure GitHub action.
Clojure LSP includes clj-kondo
Clojure LSP install includes clj-kondo, removing the need for a separate install of clj-kondo
"},{"location":"reference/code-analysis/#install","title":"Install","text":"Follow the clj-kondo install guide for your operating system.
Clj-kondo config contains additional configuration for using clj-kondo with libraries that extend the Clojure language via macros.
SpacemacsDoom EmacsNeovim clj-kondo can be used if cider
is configured as the clojure layer backend. If LSP is configured as the backend, should not be used as it may duplicate analysis results (e.g. doubling error and warning messgeas).
Use Clojure LSP with Doom rather than clj-kondo by itself.
Add the +lsp
feature to the Clojure module and enable the lsp
module .config/doom/init.el
(clojure +lsp)\nlsp\n
Add the respective LSP server implementation to the operating system Practicalli Neovim provides a guide to configure Neovim with Treesitter as an LSP client, as well as a fennel based configuration for Neovim.
"},{"location":"reference/code-analysis/#command-line-analysis","title":"Command Line analysis","text":"Run clj-kondo
with the --lint option and specify a file or path
To analyse a specific file
clj-kondon --lint ~/.config/deps.edn\n
Analyse a project, running the clj-kondo command from the root of the project
clj-kondon --lint .\n
"},{"location":"reference/code-analysis/#clj-kondo-with-github-actions","title":"clj-kondo with GitHub actions","text":"Add clj-kondo linting to continuous integration workflow.
"},{"location":"reference/control-flow/","title":"Control Flow","text":""},{"location":"reference/core-async/","title":"Core.async","text":""},{"location":"reference/doc-and-source-functions/","title":"Doc and source functions","text":""},{"location":"reference/doc-and-source-functions/#the-doc-source-functions","title":"The doc & source functions","text":"If you are not using a Clojure aware editor or spend a lot of time in the REPL you can also view the documentation of a function by calling the doc
function and see the source by calling the source
function.
To use the doc
& source
functions in the REPL you should be in the user
namespace.
Note On the command line, start a REPL with the command lein repl
and then view the documentation for three common functions used in clojure
Make sure you are in the user
namespace before calling the doc
function. If you are in another namespace, either change back using (ns 'user)
or see the next section on using these functions in another namespace.
(doc doc)\n(doc map)\n(doc filter)\n(doc cons)\n\n(source doc)\n(source map)\n
Here is the doc string for doc
Here is the source code for the source
function
Hint As the documentation for a function is part of its definition, by looking at the source of a function you also get the documentation.
"},{"location":"reference/doc-and-source-functions/#using-doc-source-function-from-another-namespace","title":"Using doc & source function from another namespace","text":"The doc
and source
functions are only included in the user
namespace. If you switch to another namespace or your editor places you in the current namespace of your project, these functions will not be available unless you including core.repl
in the current namespace.
From the REPL, evaluate the expression:
(use 'clojure.repl)\n
You could also require
the clojure.repl
library in your own code, however if you have a good editor it should provide these features without including this library. Therefore the following code is shown only as an example and not a recommended approach.
(ns foobar\n(:require [clojure.repl :refer :all]))\n
"},{"location":"reference/kebab-case/","title":"Clojure names use kebab-case","text":"kebab-case is a clean, human-readable way to combine the words that would otherwise have spaces.
Cloure uses kebab-case to combines words with a dash, -
, rather than a space. e.g. rock-paper-scissors
, tic-tac-toe
or (def db-spec-development {:db-type \"h2\" :db-name \"banking-on-clojure\"})
kebab-case is used throughout Clojure, including
- Var names with
def
and function names with defn
- Local names with
let
- Clojure spec names
kebab-case is used in lisp languages including Clojure. The style is also used in website URLs, e.g. practicalli.github.io/clojure-webapps
"},{"location":"reference/kebab-case/#using-meaningful-names","title":"Using meaningful names","text":"To provide greater clarity to human developers, words may be combined for the names used when writing the code. Using multiple words can give greater context in to the purpose of that code.
Using a combination of meaningful names makes understanding and debugging code far easier.
"},{"location":"reference/kebab-case/#spaces-characters-have-special-meaning","title":"Spaces characters have special meaning","text":"Programming languages remove spaces between words because the space character is used as a separator when parsing the code.
If spaces were not used as a separator for the some other character would be required, adding complexity to the language syntax.
"},{"location":"reference/kebab-case/#other-styles","title":"Other Styles","text":" - camelCase - used in Java and C-style programming languages
- PascalCase - used in the Pascal programming language
- snake_case - used for
ENVIRONMENT_VARIABLES
and database_table_names_and_columns
"},{"location":"reference/naming-conventions/","title":"Naming Conventions","text":""},{"location":"reference/naming-conventions/#kebab-case","title":"Kebab-case","text":"Kebab-case is the naming convention for all Clojure function names than contain more than one word. Its name comes from the Shish Kebab style of cooking, where the words are the tofu and vegetables and the dashes are the skewers.
clj-time\nstring-parser\ndisplay-name\n
"},{"location":"reference/naming-conventions/#predicates","title":"Predicates","text":"Examples of predicate naming conventions from clojure.core
contains?\nempty?\nevery?\nnot-empty?\nnull?\n
"},{"location":"reference/naming-conventions/#namespace-requires-and-aliases","title":"Namespace requires and aliases","text":"Required libraries should be given a contextually meaningful name as an alias, helping to identify the purpose of functions defined outside of the namespace.
Giving meaningful context helps code to be understood by any person reading the code. It is also easier to search for usage of functions from that context in the current project.
Aliases are rarely typed more than once in full as Clojure editors have auto-complete, so there is no benefit to short of single character aliases.
(ns status-monitor.handler\n (:require [hiccup.page :refer :as web-page]\n [hiccup.form :refer :as web-form]))\n
In very commonly used libraries or very highly used functions through out the code, refer those functions explicitly
(ns naming.is.hard\n (:require [compojure.core :refer [defroutes GET POST]]\n [ring.middleware.defaults :refer [wrap-defaults site-defaults]]))\n
"},{"location":"reference/naming-conventions/#converting-functions","title":"Converting functions","text":"When a function takes values in one format or type and converts them to another
Examples
md->html\n\nmap->Record-name ; map factory function of a record -- creates a new record from a map\n->Record-name ; positional factory function of a record -- creates a new record from a list of values\n
"},{"location":"reference/naming/","title":"Naming","text":""},{"location":"reference/prasmatic-schema/","title":"Prasmatic Schema","text":""},{"location":"reference/reader-macros/","title":"Reader Macros","text":"This is a collection of reader macros (think syntactic sugar) that are valid in Clojure. These macros are useful for commenting out expressions, defining sets, ...
Many reader macros start with the character #, which is in fact the Dispatch macro that tells the Clojure reader (the thing that takes a file of Clojure text and parses it for consumption in the compiler) to go and look at another read table for the definition of the next character - in essence this allows extending default reader behaviour.
-
#_ - Discard macro - ignore the next expression. Often used to comment out code, especially when nested inside other expressions
-
#' - Var macro - returns the reference to the var. Used to pass the definition of something rather than the result of evaluating it.
There is a nice list of reader macros in the article: The weird and wonderful characters of Clojure by @kouphax.
Hint Reader macros are part of the Clojure language specification, so are different to macros, which can be defined by anyone.
"},{"location":"reference/reader-macros/#todore-write","title":"Todo::Re-write","text":""},{"location":"reference/recursion/","title":"Recursion","text":"Recursion is a highly valuable tool in functional programming as it provides an idiomatic way of processing collections of data.
"},{"location":"reference/recursion/#normal-recursion-is-more-idiomatic","title":"normal recursion is more idiomatic","text":"On average it tends to give you clearer, more functional code whereas loop/recur tens to push you more towards an imperative, iterative style.
"},{"location":"reference/recursion/#recursive-functions","title":"Recursive functions","text":""},{"location":"reference/recursion/#warningrecursion-can-hit-the-limit-of-your-heapstack-and-cause-a-exception","title":"Warning::Recursion can hit the limit of your heap/stack and cause a ... exception","text":""},{"location":"reference/recursion/#tail-call-optimisation-with-recur","title":"Tail-call Optimisation with recur
","text":"Tail-call optimisation is where a part of memory is over-written by additional calls during recursive calls. By using the same memory segment each time, then the memory footprint of your code does not increase.
Therefore recur
is good choice for deeply nested recursion or when manipulating larger (non-lazy) data structures.
Without tail-call optimisation the code may otherwise cause a StackOverflow / Heap out of memory Error
"},{"location":"reference/recursion/#info","title":"Info::","text":"Using the recur
function as the last line of a loop
or function will enable tail call optimisation.
"},{"location":"reference/recursion/#fast","title":"Fast","text":"Using loop
and recur
it's one of the most efficient constructs in Clojure, match the speed of an equivalent for loop in Java code.
"},{"location":"reference/recursion/#restrictions","title":"Restrictions","text":"you can only recur in tail position, you can't do mutual recursion between two different function etc.
Sometime it simply isn't possible to use loop/recur or it may require the contort of code to something very unmanageable to do so.
"},{"location":"reference/recursion/#hintuse-recur-once-you-have-created-a-new-recursive-function","title":"Hint::Use recur once you have created a new recursive function","text":"By calling a recursive function by name rather than using recur
can prevent your code from remaining in an infinite loop if you get a terminating condition wrong. Without recur you memory space will be eaten up and your code will stop. Once your function is working correctly, then you can replace the call to itself with recur
.
"},{"location":"reference/recursion/#examples","title":"Examples","text":"Here are two examples using two different recursion approaches. What are the guidelines of usage of one over another?
This example recursively calls itself
(defn take-while\n \"Returns a lazy sequence of successive items from coll while\n (pred item) returns true. pred must be free of side-effects.\"\n {:added \"1.0\"\n :static true}\n [pred coll]\n (lazy-seq\n (when-let [s (seq coll)]\n (when (pred (first s))\n (cons (first s) (take-while pred (rest s)))))))\n
This example uses loop
and recur
for recursively processing the collection.
(defn take-last\n \"Returns a seq of the last n items in coll. Depending on the type\n of coll may be no better than linear time. For vectors, see also subvec.\"\n {:added \"1.1\"\n :static true}\n [n coll]\n (loop [s (seq coll), lead (seq (drop n coll))]\n (if lead\n (recur (next s) (next lead))\n s)))\n
"},{"location":"reference/recursion/#hint","title":"Hint::","text":"The above example could not use recur
instead of the recursive call to take-while
as that call is not in the last position. The cons
function is in the last position of this function.
"},{"location":"reference/recursion/#misc","title":"Misc","text":"The only one reason to use lazy-seq/lazy-cons mechanism is generating lazy sequences. If you don't need them then loop/recur should undoubtedly be used.
"},{"location":"reference/sequences/","title":"Reference: Clojure from the ground up: sequences","text":"In Chapter 3, we discovered functions as a way to abstract expressions; to rephrase a particular computation with some parts missing. We used functions to transform a single value. But what if we want to apply a function to more than one value at once? What about sequences?
For example, we know that (inc 2) increments the number 2. What if we wanted to increment every number in the vector [1 2 3], producing [2 3 4]?
user=> (inc [1 2 3]) ClassCastException clojure.lang.PersistentVector cannot be cast to java.lang.Number clojure.lang.Numbers.inc (Numbers.java:110) Clearly inc can only work on numbers, not on vectors. We need a different kind of tool.
A direct approach Let\u2019s think about the problem in concrete terms. We want to increment each of three elements: the first, second, and third. We know how to get an element from a sequence by using nth, so let\u2019s start with the first number, at index 0:
user=> (def numbers [1 2 3])
"},{"location":"reference/sequences/#usernumbers","title":"'user/numbers","text":"user=> (nth numbers 0) 1 user=> (inc (nth numbers 0)) 2 So there\u2019s the first element incremented. Now we can do the second:
user=> (inc (nth numbers 1)) 3 user=> (inc (nth numbers 2)) 4 And it should be straightforward to combine these into a vector\u2026
user=> [(inc (nth numbers 0)) (inc (nth numbers 1)) (inc (nth numbers 2))] [2 3 4] Success! We\u2019ve incremented each of the numbers in the list! How about a list with only two elements?
user=> (def numbers [1 2])
"},{"location":"reference/sequences/#usernumbers_1","title":"'user/numbers","text":"user=> [(inc (nth numbers 0)) (inc (nth numbers 1)) (inc (nth numbers 2))]
IndexOutOfBoundsException clojure.lang.PersistentVector.arrayFor (PersistentVector.java:107) Shoot. We tried to get the element at index 2, but couldn\u2019t, because numbers only has indices 0 and 1. Clojure calls that \u201cindex out of bounds\u201d.
We could just leave off the third expression in the vector; taking only elements 0 and 1. But the problem actually gets much worse, because we\u2019d need to make this change every time we wanted to use a different sized vector. And what of a vector with 1000 elements? We\u2019d need 1000 (inc (nth numbers ...)) expressions! Down this path lies madness.
Let\u2019s back up a bit, and try a slightly smaller problem.
Recursion What if we just incremented the first number in the vector? How would that work? We know that first finds the first element in a sequence, and rest finds all the remaining ones.
user=> (first [1 2 3]) 1 user=> (rest [1 2 3]) (2 3) So there\u2019s the pieces we\u2019d need. To glue them back together, we can use a function called cons, which says \u201cmake a list beginning with the first argument, followed by all the elements in the second argument\u201d.
user=> (cons 1 [2]) (1 2) user=> (cons 1 [2 3]) (1 2 3) user=> (cons 1 [2 3 4]) (1 2 3 4) OK so we can split up a sequence, increment the first part, and join them back together. Not so hard, right?
(defn inc-first [numbers] (cons (inc (first numbers)) (rest numbers))) user=> (inc-first [1 2 3 4]) (2 2 3 4) Hey, there we go! First element changed. Will it work with any length list?
user=> (inc-first [5]) (6) user=> (inc-first [])
NullPointerException clojure.lang.Numbers.ops (Numbers.java:942) Shoot. We can\u2019t increment the first element of this empty vector, because it doesn\u2019t have a first element.
user=> (first []) nil user=> (inc nil)
NullPointerException clojure.lang.Numbers.ops (Numbers.java:942) So there are really two cases for this function. If there is a first element in numbers, we\u2019ll increment it as normal. If there\u2019s no such element, we\u2019ll return an empty list. To express this kind of conditional behavior, we\u2019ll use a Clojure special form called if:
"},{"location":"reference/sequences/#user-doc-if","title":"user=> (doc if)","text":"if (if test then else?) Special Form Evaluates test. If not the singular values nil or false, evaluates and yields then, otherwise, evaluates and yields else. If else is not supplied it defaults to nil.
Please see http://clojure.org/special_forms#if To confirm our intuition:
user=> (if true :a :b) :a user=> (if false :a :b) :b Seems straightforward enough.
(defn inc-first [numbers] (if (first numbers) ; If there's a first number, build a new list with cons (cons (inc (first numbers)) (rest numbers)) ; If there's no first number, just return an empty list (list)))
user=> (inc-first []) () user=> (inc-first [1 2 3]) (2 2 3) Success! Now we can handle both cases: empty sequences, and sequences with things in them. Now how about incrementing that second number? Let\u2019s stare at that code for a bit.
(rest numbers) Hang on. That list\u2013(rest numbers)\u2013that\u2019s a list of numbers too. What if we\u2026 used our inc-first function on that list, to increment its first number? Then we\u2019d have incremented both the first and the second element.
(defn inc-more [numbers] (if (first numbers) (cons (inc (first numbers)) (inc-more (rest numbers))) (list))) user=> (inc-more [1 2 3 4]) (2 3 4 5) Odd. That didn\u2019t just increment the first two numbers. It incremented all the numbers. We fell into the complete solution entirely by accident. What happened here?
Well first we\u2026 yes, we got the number one, and incremented it. Then we stuck that onto (inc-first [2 3 4]), which got two, and incremented it. Then we stuck that two onto (inc-first [3 4]), which got three, and then we did the same for four. Only that time around, at the very end of the list, (rest [4]) would have been empty. So when we went to get the first number of the empty list, we took the second branch of the if, and returned the empty list.
Having reached the bottom of the function calls, so to speak, we zip back up the chain. We can imagine this function turning into a long string of cons calls, like so:
(cons 2 (cons 3 (cons 4 (cons 5 '())))) (cons 2 (cons 3 (cons 4 '(5)))) (cons 2 (cons 3 '(4 5))) (cons 2 '(3 4 5)) '(2 3 4 5) This technique is called recursion, and it is a fundamental principle in working with collections, sequences, trees, or graphs\u2026 any problem which has small parts linked together. There are two key elements in a recursive program:
Some part of the problem which has a known solution A relationship which connects one part of the problem to the next Incrementing the elements of an empty list returns the empty list. This is our base case: the ground to build on. Our inductive case, also called the recurrence relation, is how we broke the problem up into incrementing the first number in the sequence, and incrementing all the numbers in the rest of the sequence. The if expression bound these two cases together into a single function; a function defined in terms of itself.
Once the initial step has been taken, every step can be taken.
user=> (inc-more [1 2 3 4 5 6 7 8 9 10 11 12]) (2 3 4 5 6 7 8 9 10 11 12 13) This is the beauty of a recursive function; folding an unbounded stream of computation over and over, onto itself, until only a single step remains.
Generalizing from inc We set out to increment every number in a vector, but nothing in our solution actually depended on inc. It just as well could have been dec, or str, or keyword. Let\u2019s parameterize our inc-more function to use any transformation of its elements:
(defn transform-all [f xs] (if (first xs) (cons (f (first xs)) (transform-all f (rest xs))) (list))) Because we could be talking about any kind of sequence, not just numbers, we\u2019ve named the sequence xs, and its first element x. We also take a function f as an argument, and that function will be applied to each x in turn. So not only can we increment numbers\u2026
user=> (transform-all inc [1 2 3 4]) (2 3 4 5) \u2026but we can turn strings to keywords\u2026
user=> (transform-all keyword [\"bell\" \"hooks\"]) (:bell :hooks) \u2026or wrap every element in a list:
user=> (transform-all list [:codex :book :manuscript]) ((:codex) (:book) (:manuscript)) In short, this function expresses a sequence in which each element is some function applied to the corresponding element in the underlying sequence. This idea is so important that it has its own name, in mathematics, Clojure, and other languages. We call it map.
user=> (map inc [1 2 3 4]) (2 3 4 5) You might remember maps as a datatype in Clojure, too\u2013they\u2019re dictionaries that relate keys to values.
{:year 1969 :event \"moon landing\"} The function map relates one sequence to another. The type map relates keys to values. There is a deep symmetry between the two: maps are usually sparse, and the relationships between keys and values may be arbitrarily complex. The map function, on the other hand, usually expresses the same type of relationship, applied to a series of elements in fixed order.
Building sequences Recursion can do more than just map. We can use it to expand a single value into a sequence of values, each related by some function. For instance:
(defn expand [f x count] (if (pos? count) (cons x (expand f (f x) (dec count))))) Our base case is x itself, followed by the sequence beginning with (f x). That sequence in turn expands to (f (f x)), and then (f (f (f x))), and so on. Each time we call expand, we count down by one using dec. Once the count is zero, the if returns nil, and evaluation stops. If we start with the number 0 and use inc as our function:
user=> user=> (expand inc 0 10) (0 1 2 3 4 5 6 7 8 9) Clojure has a more general form of this function, called iterate.
user=> (take 10 (iterate inc 0)) (0 1 2 3 4 5 6 7 8 9) Since this sequence is infinitely long, we\u2019re using take to select only the first 10 elements. We can construct more complex sequences by using more complex functions:
user=> (take 10 (iterate (fn [x] (if (odd? x) (+ 1 x) (/ x 2))) 10)) (10 5 6 3 4 2 1 2 1 2) Or build up strings:
user=> (take 5 (iterate (fn [x] (str x \"o\")) \"y\")) (\"y\" \"yo\" \"yoo\" \"yooo\" \"yoooo\") iterate is extremely handy for working with infinite sequences, and has some partners in crime. repeat, for instance, constructs a sequence where every element is the same.
user=> (take 10 (repeat :hi)) (:hi :hi :hi :hi :hi :hi :hi :hi :hi :hi) user=> (repeat 3 :echo) (:echo :echo :echo) And its close relative repeatedly simply calls a function (f) to generate an infinite sequence of values, over and over again, without any relationship between elements. For an infinite sequence of random numbers:
user=> (rand) 0.9002678382322784 user=> (rand) 0.12375594203332863 user=> (take 3 (repeatedly rand)) (0.44442397843046755 0.33668691162169784 0.18244875487846746) Notice that calling (rand) returns a different number each time. We say that rand is an impure function, because it cannot simply be replaced by the same value every time. It does something different each time it\u2019s called.
There\u2019s another very handy sequence function specifically for numbers: range, which generates a sequence of numbers between two points. (range n) gives n successive integers starting at 0. (range n m) returns integers from n to m-1. (range n m step) returns integers from n to m, but separated by step.
user=> (range 5) (0 1 2 3 4) user=> (range 2 10) (2 3 4 5 6 7 8 9) user=> (range 0 100 5) (0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95) To extend a sequence by repeating it forever, use cycle:
user=> (take 10 (cycle [1 2 3])) (1 2 3 1 2 3 1 2 3 1) Transforming sequences Given a sequence, we often want to find a related sequence. map, for instance, applies a function to each element\u2013but has a few more tricks up its sleeve.
user=> (map (fn [n vehicle] (str \"I've got \" n \" \" vehicle \"s\")) [0 200 9] [\"car\" \"train\" \"kiteboard\"]) (\"I've got 0 cars\" \"I've got 200 trains\" \"I've got 9 kiteboards\") If given multiple sequences, map calls its function with one element from each sequence in turn. So the first value will be (f 0 \"car\"), the second (f 200 \"train\"), and so on. Like a zipper, map folds together corresponding elements from multiple collections. To sum three vectors, column-wise:
user=> (map + [1 2 3] [4 5 6] [7 8 9]) (12 15 18) If one sequence is bigger than another, map stops at the end of the smaller one. We can exploit this to combine finite and infinite sequences. For example, to number the elements in a vector:
user=> (map (fn [index element] (str index \". \" element)) (iterate inc 0) [\"erlang\" \"ruby\" \"haskell\"]) (\"0. erlang\" \"1. ruby\" \"2. haskell\") Transforming elements together with their indices is so common that Clojure has a special function for it: map-indexed:
user=> (map-indexed (fn [index element] (str index \". \" element)) [\"erlang\" \"ruby\" \"haskell\"]) (\"0. erlang\" \"1. ruby\" \"2. haskell\") You can also tack one sequence onto the end of another, like so:
user=> (concat [1 2 3] [:a :b :c] [4 5 6]) (1 2 3 :a :b :c 4 5 6) Another way to combine two sequences is to riffle them together, using interleave.
user=> (interleave [:a :b :c] [1 2 3]) (:a 1 :b 2 :c 3) And if you want to insert a specific element between each successive pair in a sequence, try interpose:
user=> (interpose :and [1 2 3 4]) (1 :and 2 :and 3 :and 4) To reverse a sequence, use reverse.
user=> (reverse [1 2 3]) (3 2 1) user=> (reverse \"woolf\") (\\f \\l \\o \\o \\w) Strings are sequences too! Each element of a string is a character, written \\f. You can rejoin those characters into a string with apply str:
user=> (apply str (reverse \"woolf\")) \"floow\" \u2026and break strings up into sequences of chars with seq.
user=> (seq \"sato\") (\\s \\a \\t \\o) To randomize the order of a sequence, use shuffle.
user=> (shuffle [1 2 3 4]) [3 1 2 4] user=> (apply str (shuffle (seq \"abracadabra\"))) \"acaadabrrab\" Subsequences We\u2019ve already seen take, which selects the first n elements. There\u2019s also drop, which removes the first n elements.
user=> (range 10) (0 1 2 3 4 5 6 7 8 9) user=> (take 3 (range 10)) (0 1 2) user=> (drop 3 (range 10)) (3 4 5 6 7 8 9) And for slicing apart the other end of the sequence, we have take-last and drop-last:
user=> (take-last 3 (range 10)) (7 8 9) user=> (drop-last 3 (range 10)) (0 1 2 3 4 5 6) take-while and drop-while work just like take and drop, but use a function to decide when to cut.
user=> (take-while pos? [3 2 1 0 -1 -2 10]) (3 2 1) In general, one can cut a sequence in twain by using split-at, and giving it a particular index. There\u2019s also split-with, which uses a function to decide when to cut.
(split-at 4 (range 10)) [(0 1 2 3) (4 5 6 7 8 9)] user=> (split-with number? [1 2 3 :mark 4 5 6 :mark 7]) [(1 2 3) (:mark 4 5 6 :mark 7)] Notice that because indexes start at zero, sequence functions tend to have predictable numbers of elements. (split-at 4) yields four elements in the first collection, and ensures the second collection begins at index four. (range 10) has ten elements, corresponding to the first ten indices in a sequence. (range 3 5) has two (since 5 - 3 is two) elements. These choices simplify the definition of recursive functions as well.
We can select particular elements from a sequence by applying a function. To find all positive numbers in a list, use filter:
user=> (filter pos? [1 5 -4 -7 3 0]) (1 5 3) filter looks at each element in turn, and includes it in the resulting sequence only if (f element) returns a truthy value. Its complement is remove, which only includes those elements where (f element) is false or nil.
user=> (remove string? [1 \"turing\" :apple]) (1 :apple) Finally, one can group a sequence into chunks using partition, partition-all, or partition-by. For instance, one might group alternating values into pairs:
user=> (partition 2 [:cats 5 :bats 27 :crocodiles 0]) ((:cats 5) (:bats 27) (:crocodiles 0)) Or separate a series of numbers into negative and positive runs:
(user=> (partition-by neg? [1 2 3 2 1 -1 -2 -3 -2 -1 1 2]) ((1 2 3 2 1) (-1 -2 -3 -2 -1) (1 2)) Collapsing sequences After transforming a sequence, we often want to collapse it in some way; to derive some smaller value. For instance, we might want the number of times each element appears in a sequence:
user=> (frequencies [:meow :mrrrow :meow :meow]) {:meow 3, :mrrrow 1} Or to group elements by some function:
user=> (pprint (group-by :first [{:first \"Li\" :last \"Zhou\"} {:first \"Sarah\" :last \"Lee\"} {:first \"Sarah\" :last \"Dunn\"} {:first \"Li\" :last \"O'Toole\"}])) {\"Li\" [{:last \"Zhou\", :first \"Li\"} {:last \"O'Toole\", :first \"Li\"}], \"Sarah\" [{:last \"Lee\", :first \"Sarah\"} {:last \"Dunn\", :first \"Sarah\"}]} Here we\u2019ve taken a sequence of people with first and last names, and used the :first keyword (which can act as a function!) to look up those first names. group-by used that function to produce a map of first names to lists of people\u2013kind of like an index.
In general, we want to combine elements together in some way, using a function. Where map treated each element independently, reducing a sequence requires that we bring some information along. The most general way to collapse a sequence is reduce.
"},{"location":"reference/sequences/#user-doc-reduce","title":"user=> (doc reduce)","text":"clojure.core/reduce ([f coll] [f val coll]) f should be a function of 2 arguments. If val is not supplied, returns the result of applying f to the first 2 items in coll, then applying f to that result and the 3rd item, etc. If coll contains no items, f must accept no arguments as well, and reduce returns the result of calling f with no arguments. If coll has only 1 item, it is returned and f is not called. If val is supplied, returns the result of applying f to val and the first item in coll, then applying f to that result and the 2nd item, etc. If coll contains no items, returns val and f is not called. That\u2019s a little complicated, so we\u2019ll start small. We need a function, f, which combines successive elements of the sequence. (f state element) will return the state for the next invocation of f. As f moves along the sequence, it carries some changing state with it. The final state is the return value of reduce.
user=> (reduce + [1 2 3 4]) 10 reduce begins by calling (+ 1 2), which yields the state 3. Then it calls (+ 3 3), which yields 6. Then (+ 6 4), which returns 10. We\u2019ve taken a function over two elements, and used it to combine all the elements. Mathematically, we could write:
1 + 2 + 3 + 4 3 + 3 + 4 6 + 4 10 So another way to look at reduce is like sticking a function between each pair of elements. To see the reducing process in action, we can use reductions, which returns a sequence of all the intermediate states.
user=> (reductions + [1 2 3 4]) (1 3 6 10) Oftentimes we include a default state to start with. For instance, we could start with an empty set, and add each element to it as we go along:
user=> (reduce conj #{} [:a :b :b :b :a :a])
"},{"location":"reference/sequences/#a-b","title":"{:a :b}","text":"Reducing elements into a collection has its own name: into. We can conj [key value] vectors into a map, for instance, or build up a list:
user=> (into {} [[:a 2] [:b 3]]) {:a 2, :b 3} user=> (into (list) [1 2 3 4]) (4 3 2 1) Because elements added to a list appear at the beginning, not the end, this expression reverses the sequence. Vectors conj onto the end, so to emit the elements in order, using reduce, we might try:
user=> (reduce conj [] [1 2 3 4 5]) (reduce conj [] [1 2 3 4 5]) [1 2 3 4 5] Which brings up an interesting thought: this looks an awful lot like map. All that\u2019s missing is some kind of transformation applied to each element.
(defn my-map [f coll] (reduce (fn [output element] (conj output (f element))) [] coll)) user=> (my-map inc [1 2 3 4]) [2 3 4 5] Huh. map is just a special kind of reduce. What about, say, take-while?
(defn my-take-while [f coll] (reduce (fn [out elem] (if (f elem) (conj out elem) (reduced out))) [] coll)) We\u2019re using a special function here, reduced, to indicate that we\u2019ve completed our reduction early and can skip the rest of the sequence.
user=> (my-take-while pos? [2 1 0 -1 0 1 2]) [2 1] reduce really is the uber function over sequences. Almost any operation on a sequence can be expressed in terms of a reduce\u2013though for various reasons, many of the Clojure sequence functions are not written this way. For instance, take-while is actually defined like so:
user=> (source take-while) (defn take-while \"Returns a lazy sequence of successive items from coll while (pred item) returns true. pred must be free of side-effects.\" {:added \"1.0\" :static true} [pred coll] (lazy-seq (when-let [s (seq coll)] (when (pred (first s)) (cons (first s) (take-while pred (rest s))))))) There\u2019s a few new pieces here, but the structure is essentially the same as our initial attempt at writing map. When the predicate matches the first element, cons the first element onto take-while, applied to the rest of the sequence. That lazy-seq construct allows Clojure to compute this sequence as required, instead of right away. It defers execution to a later time.
Most of Clojure\u2019s sequence functions are lazy. They don\u2019t do anything until needed. For instance, we can increment every number from zero to infinity:
user=> (def infinite-sequence (map inc (iterate inc 0)))
"},{"location":"reference/sequences/#userinfinite-sequence","title":"'user/infinite-sequence","text":"user=> (realized? infinite-sequence) false That function returned immediately. Because it hasn\u2019t done any work yet, we say the sequence is unrealized. It doesn\u2019t increment any numbers at all until we ask for them:
user=> (take 10 infinite-sequence) (1 2 3 4 5 6 7 8 9 10) user=> (realized? infinite-sequence) true Lazy sequences also remember their contents, once evaluated, for faster access.
Putting it all together We\u2019ve seen how recursion generalizes a function over one thing into a function over many things, and discovered a rich landscape of recursive functions over sequences. Now let\u2019s use our knowledge of sequences to solve a more complex problem: find the sum of the products of consecutive pairs of the first 1000 odd integers.
First, we\u2019ll need the integers. We can start with 0, and work our way up to infinity. To save time printing an infinite number of integers, we\u2019ll start with just the first 10.
user=> (take 10 (iterate inc 0)) (0 1 2 3 4 5 6 7 8 9) Now we need to find only the ones which are odd. Remember, filter pares down a sequence to only those elements which pass a test.
user=> (take 10 (filter odd? (iterate inc 0))) (1 3 5 7 9 11 13 15 17 19) For consecutive pairs, we want to take [1 3 5 7 ...] and find a sequence like ([1 3] [3 5] [5 7] ...). That sounds like a job for partition:
user=> (take 3 (partition 2 (filter odd? (iterate inc 0)))) ((1 3) (5 7) (9 11)) Not quite right\u2013this gave us non-overlapping pairs, but we wanted overlapping ones too. A quick check of (doc partition) reveals the step parameter:
user=> (take 3 (partition 2 1 (filter odd? (iterate inc 0)))) ((1 3) (3 5) (5 7)) Now we need to find the product for each pair. Given a pair, multiply the two pieces together\u2026 yes, that sounds like map:
user=> (take 3 (map (fn [pair] (* (first pair) (second pair))) (partition 2 1 (filter odd? (iterate inc 0))))) (3 15 35) Getting a bit unwieldy, isn\u2019t it? Only one final step: sum all those products. We\u2019ll adjust the take to include the first 1000, not the first 3, elements.
user=> (reduce + (take 1000 (map (fn [pair] (* (first pair) (second pair))) (partition 2 1 (filter odd? (iterate inc 0))))) 1335333000 The sum of the first thousand products of consecutive pairs of the odd integers starting at 0. See how each part leads to the next? This expression looks a lot like the way we phrased the problem in English\u2013but both English and Lisp expressions are sort of backwards, in a way. The part that happens first appears deepest, last, in the expression. In a chain of reasoning like this, it\u2019d be nicer to write it in order.
user=> (->> 0 (iterate inc) (filter odd?) (partition 2 1) (map (fn [pair] (* (first pair) (second pair)))) (take 1000) (reduce +)) 1335333000 Much easier to read: now everything flows in order, from top to bottom, and we\u2019ve flattened out the deeply nested expressions into a single level. This is how object-oriented languages structure their expressions: as a chain of function invocations, each acting on the previous value.
But how is this possible? Which expression gets evaluated first? (take 1000) isn\u2019t even a valid call\u2013where\u2019s its second argument? How are any of these forms evaluated?
What kind of arcane function is ->>?
All these mysteries, and more, in Chapter 5: Macros.
Problems Write a function to find out if a string is a palindrome\u2013that is, if it looks the same forwards and backwards. Find the number of \u2018c\u2019s in \u201cabracadabra\u201d. Write your own version of filter. Find the first 100 prime numbers: 2, 3, 5, 7, 11, 13, 17, \u2026.
"},{"location":"reference/threading-macros/","title":"Reference: Threading macros","text":"Using the threading macro, the result of every function is passed onto the next function in the list. This can be seen very clearly using ,,, to denote where the value is passed to the next function
(->\n \"project.clj\"\n slurp ,,,\n read-string ,,,\n (nth ,,, 2))\n
To make this really simple lets create a contrived example of the threading macro. Here we use the str
function to join strings together. Each individual str
function joins its own strings together, passing the resulting string as the first argument to the next function.
(->\n (str \"This\" \" \" \"is\" \" \")\n (str \"the\" \" \" \"threading\" \" \" \"macro\")\n (str \"in\" \" \" \"action.\"))\n
Output
;; => \"This is the threading macro in action\"\n
"},{"location":"reference/threading-macros/#hintcommas-in-clojure-are-whitespace","title":"Hint::Commas in clojure are whitespace","text":"Commas are simply ignored when the Clojure Reader parses code. Commas are rarely used and only to help human readability of the code
"},{"location":"reference/threading-macros/#thread-last-macro","title":"Thread-last macro","text":"Using the thread-last macro, ->>, the result of a function is passed as the last argument of the next function call. So in another simple series of str function calls, our text comes out backwards.
(->> \" this\"\n (str \" is\")\n (str \" backwards\"))\n
"},{"location":"reference/clojure-cli/","title":"Reference: Clojure CLI","text":"A reference on using Clojure CLI and using community tools effectively.
- structure of the deps.edn configuration
- execution options
- Java Virtual Machine options
- defining custom aliases
- common aliases from practicalli/clojure-deps-edn project
"},{"location":"reference/clojure-cli/example-alias-definitions/","title":"Common alias definitions","text":""},{"location":"reference/clojure-cli/example-alias-definitions/#task-run-a-simple-terminal-repl","title":"Task: Run a simple terminal REPL","text":"clojure
and clj
(requires rlwrap) will run a REPL if given no other arguments.
Running either command from the root directory of a project will merge the deps.edn
configuration with ~/.clojure/deps.edn
.
"},{"location":"reference/clojure-cli/example-alias-definitions/#task-run-a-repl-with-additional-dependencies-and-paths","title":"Task: Run a REPL with additional dependencies and paths","text":"clojure -M:alias
will run a repl if the alias does not contain a main namespace defined in :main-opts
, e.g. :main-opts [\"-m\" \"namespace.main\"]
. The deps and path values are included from the alias.
If the following alias is defined in the project deps.edn
file
:env/dev\n{:extra-paths [\"resources\"]\n :extra-deps {com.h2database/h2 {:mvn/version \"1.4.200\"}}}\n
clojure -M:env/dev
will add resources
directory to the path and the h2 database library to the dependencies, then runs a REPL.
Including the -r
option in the command line forces a REPL to run, even if a main namespace is provided via :main-opts
or the command line.
clojure -r -M:alias1:alias2\n
The dependencies and paths will be merged from the alias from left to right, with each successive alias over-riding the value of any matching keys in the dependencies.
"},{"location":"reference/clojure-cli/example-alias-definitions/#task-create-a-new-project-from-template","title":"Task: Create a new project from template","text":"The clj-new
community tool can be used to create a Clojure / ClojureScript project, using a template to provide a project structure and example source code and tests.
Using the :main-opts
approach, an alias for clj-new
would be defined as follows
:project/new\n {:extra-deps {seancorfield/clj-new {:mvn/version \"1.0.215\"}}\n :main-opts [\"-m\" \"clj-new.create\"]}\n
The clj-new
tool can be run using the -M
flag, passing the template and project names as arguments.
clojure -M:project/new template-name project-domain/application-name
To create a project as an application (to be run via the command line) for the practicalli domain with the application called banking-on-clojure
clojure -M:new app practicalli/banking-on-clojure\n
The latest version of the clj-new
project also supports using the -X
flag and default arguments.
Adding the :exec-fn
to the clj-new
alias, the -X
flag can be used instead of the -M
. Arguments are supplied as key/value pairs
:project/new\n {:extra-deps {seancorfield/clj-new {:mvn/version \"1.1.215\"}}\n :exec-fn clj-new/create}\n
Use this alias with the -X
flag
clojure -X:project/new :template template-name :name practicalli/banking-on-clojure\n
Default values can be added using the :exec-args
key to the alias
:project/new\n{:extra-deps {seancorfield/clj-new {:mvn/version \"1.1.215\"}}\n :exec-fn clj-new.create\n :exec-args {:template lib :name practicalli/playground}}\n
clojure -M:project/new :name practicalli/awesome-webapp
will create a new project using the {:template lib :name practicalli/awesome-webapp}
argument.
"},{"location":"reference/clojure-cli/example-alias-definitions/#task-executing-a-specific-function","title":"Task: Executing a specific function","text":"Clojure can run a specific function, useful for one off tasks or timed batch processing (via cron or similar tool) as well as complete applications.
Arguments to the function are passed as a hash-map, defined in either an aliases :exec-args
key or as key value pairs on the command line. Command line key value pairs are merged with the :exec-arg
hash-map, replacing the values from the command line if there are matching keys.
Scenarios
clojure -X namespace/fn
runs the function specified on the command line, passing an empty hash-map as an argument
clojure -X:alias fn
runs the function if the :ns-default
is set to the namespace that contains the function, otherwise \"Unqualified function can't be resolved: fn-name\" error is returned.
clojure -X:alias
runs the function specified by :exec-fn
in the alias. The function must include its namespace or have that namespace defined in :ns-default
. If :exec-args
is defined in the alias, its value is passed to the function, otherwise an empty hash-map is passed to the function as an argument.
clojure -X:alias namespace/fn
will run the function specified on the command line, over-riding :exec-fn
if it is defined in the alias. :exec-args
will be passed to the command line function if defined in the alias. Dependencies and paths will be used from the alias. Assumption: the command line namespace also overrides the :ns-default
value if set.
clojure -X:alias :key1 val1 :key2 val2
will execute the function defined in :exec-fn
and pass it the key value pairs from the command line as a hash map. If the alias has :exec-args
defined, command line args are merged into the :exec-fn
hash-map, replacing the default values in :exec-args
where keys match.
Assuming there is an alias called database/migrate
defined in the project deps.edn
:database/migrate\n{:exec-fn practicalli.banking-on-clojure.database/migrate\n :exec-args {:db-type \"h2\" :database \"banking-on-clojure\"}}\n
clojure -X:database/migrate :database \"specs-repository\"
would merge the command line args with :exec-args
to create the hash-map {:db-type \"h2\" :database \"specs-repository\"}
which is passed to the practicalli.banking-on-clojure.database/migrate
function as an argument.
"},{"location":"reference/clojure-cli/example-alias-definitions/#task-executing-a-range-of-functions","title":"Task: Executing a range of functions","text":":ns-default
in an alias defines the namespace that contains the functions that could be executed.
{:aliases\n {:project/run\n {:ns-default practicalli/banking-on-clojure}}}\n
Specific functions from the namespace can be called via the command line
clojure -X:project/run migrate-db :db-type h2 :database banking-on-clojure\nclojure -X:project/run server-start :port 8080\n
"},{"location":"reference/clojure-cli/example-alias-definitions/#task-dry-run-or-prepare-for-ci-containers","title":"Task: Dry Run or Prepare for CI / Containers","text":"clojure -P
will download the libraries defined in :deps
in the project deps.edn
and do nothing else. Standard out shows downloading of dependencies not already cached locally, including name and versions and repository downloaded from.
Qualified namespaces required
If an unqualified library name is used, e.g. compojure
, then a warning is sent to the standard out. Change the name of the library to be fully qualified e.g. weavejester/compojure
. Use the same name if there is no official qualified domain, e.g. http-kit/http-kit
The -P
flag can be used to modify an existing command to ensure no execution takes place, ensuring a prepare only (dry run) action.
clojure -P -M:alias-name
downloads the dependencies for the specific aliases and multiple aliases can be chained together, e.g. clojure -P -M:dev/env:test-runner/kaocha
The -P
flag uses everything from an alias not related to execution.
The classic way to download deps was to run clojure -A:aliases -Spath
, where -Spath
prevented execution of repl or main.
"},{"location":"reference/clojure-cli/example-alias-definitions/#run-a-clojure-application","title":"Run a Clojure application","text":"clojure -m full.namespace.to.dash-main
calls the -main
function from the given namespace. Arguments to the function are simply added to the end of the command line and passed to the -main
function in the given namespace.
The -m
flag in the CLI tools pre-release returns a warning that -M
should be used.
Using -M
and -m
works, but seems redundant. Using -M
by itself runs the REPL.
clojure -M -m full.namespace.to.dash-main\n
-M
seems useful when including an alias with extra configuration (eg. :extra-deps
, :extra-paths
, :main-opts
). As :main-opts
is no different to the -m
option, creating an alias just to avoid the warning seems excessive.
"},{"location":"reference/clojure-cli/example-alias-definitions/#task-executing-a-project-using-edn-style-args","title":"Task: Executing a project - using Edn style args","text":"Clojure CLI tools is encouraging a move to functions that take a hash-map for their arguments. Passing arguments in as an edn data structure has more rigor than options and strings on the command line.
The simplest form is to define an alias to run the project, specifying just the function to execute using :exec-fn
:aliases\n {:project/run\n {:exec-fn practicalli.banking-on-clojure/server-start}\n } ;; End of Aliases\n
Then the project can be run using this alias.
clojure -X:project/run\n
Arguments can be passed to the function as key/value pairs on the command line.
clojure -X:project/run :port 8080 :host \"localhost\"\n
:exec-args
provides a way to define default arguments for the function, regardless of if it is defined in ;:exec-fn
or passed via the command line.
:exec-args
defines a hash-map of arguments so the function must support taking a hash-map as an argument.
A function may take variable args, especially if it is supporting both hash-maps and strings as options.
:aliases\n {:project/run\n {:exec-fn fully.qualified/namespace\n :exec-args {:default \"arguments\" :can-be-over-ridden-by \"command-line-args\"} }\n } ;; End of Aliases\n
Adding :exec-args
to the :run-project
:aliases\n {:project/run\n {:exec-fn practicalli.banking-on-clojure/server-start\n :exec-args {:port 8888 :host \"localhost\"}}\n } ;; End of Aliases\n
"},{"location":"reference/clojure-cli/example-alias-definitions/#example-of-running-a-clojure-project-hello-world","title":"Example of running a Clojure project - hello-world","text":"In this example I use the hello-world example from https://clojure.org/guides/deps_and_cli#_writing_a_program A project deps.edn
file was created containing the dependency for clojure.java-time and the source code from that page copied into src/hello.clj
clojure -m
hello runs the project and returns the time from running the -main function. However this gives a warning:
WARNING: When invoking clojure.main, use -M\n
clojure -M
runs a REPL
clojure -M -m hello
runs the project and returns the time. But then I ask myself what is the purpose of -M
Creating an alias to run the project seems an interesting idea, as I could also set default arguments.
Adding an :project-run
alias to the project deps.edn
works when calling with clojure -M:project-run
:aliases\n {:project-run {:main-opts [\"-m\" \"hello\"]}}\n
Changing the :project-run
alias to use :exec-fn
and a fully qualified function (-main by default) should work when calling with clojure -X:project-run
. :aliases {:run-project {:exec-fn hello]}}
However, the hello-world
project has an unqualified function and cannot be resolved.
Moving the source code to src/practicalli/hello.clj
and calling clojure -X:run-project
gives an execution error, (ArityException)
as the -main
function does not take any arguments, (defn -main [] ,,,)
.
Changing the -main
function to (defn -main [& args] ,,,)
fixes the arity exception and calling clojure -X:run-project
works.
"},{"location":"reference/clojure-cli/example-alias-definitions/#local-maven-install","title":"Local Maven install","text":"Install a jar into the local Maven cache, typically ~/.m2/repository/
directory, organised by groupId
clojure -X:deps mvn-install :jar '\"/path/to.jar\"'\n
edn strings must be in double quotes, and then single-quoted for the shell
mvn-install
uses the .pom
file contained in the jar (if it exists) to determine the groupId, artifactId, and version coordinates to use when the jar is installed.
The .pom
file can also be specified using the :pom
argument.
The install argmap takes the following options:
key Required Description :jar
required path to the jar file to install :pom
optional path to .pom file (if .jar file does not contain .pom) :lib
optional qualified symbol e.g my.org/lib
:version
optional Version number of library (string type) :classifier
optional (string type) :local-repo
optional path to local repo (default = ~/.m2/repository)"},{"location":"reference/clojure-cli/jvm-options/","title":"Reference: Clojure CLI JVM Options","text":"JDK_JAVA_OPTIONS
Environment Variable
JDK_JAVA_OPTIONS
is the official Environment Variable for setting options when calling java
, javac
and other Java commands to start running a Java Virtual Machine (Java version 9 onward).
Java Virtual Machine options can be passed using the Clojure CLI, either via the -J
command line flag or :jvm-opts
in a deps.edn
alias.
Java Virtual Machine configuration and reporting
Java Virtual Machine section covers commonly used options, reporting JVM metrics and optimisation of the JVM process.
"},{"location":"reference/clojure-cli/jvm-options/#clojure-cli-command-line-options","title":"Clojure CLI command line options","text":"Clojure CLI -J
flag passes configuration options to the JVM. When there are multiple, each must be prefixed with -J
.
clojure -J-XX:+UnlockDiagnosticVMOptions -J\u2011XX:NativeMemoryTracking=summary -J\u2011XX:+PrintNMTStatistics\n
"},{"location":"reference/clojure-cli/jvm-options/#clojure-cli-depsedn-configuration","title":"Clojure CLI deps.edn configuration","text":":jvm-opts
key in an alias adds JVM options to Clojure CLI deps.edn configuration. The :jvm-opts
key has a value that is a collection of string JVM options [\"-Xms2048m\" \"-Xmx4096\"]
Alias to set a large heap size
:jvm/heap-max-2g {:jvm-opts [\"-Xmx2G\"]}\n
Report a full breakdown of the HotSpot JVM\u2019s memory usage upon exit using the following option combination:
:jvm/report {:jvm-opts [\"-XX:+UnlockDiagnosticVMOptions\"\n \"\u2011XX:NativeMemoryTracking=summary\"\n \"\u2011XX:+PrintNMTStatistics\"]}\n
Add a Java module
:jvm/xml-bind {:jvm-opts [\"\u2013add-modules java.xml.bind\"]}\n
Ignoring unrecognised options
:jvm-opts [\"-XX:+IgnoreUnrecognizedVMOptions\"]\n
The aliases can be used with the Clojure CLI execution options: -A
(for built-in REPL invocation), -X
and -T
(for clojure.exec function execution), or -M
(for clojure.main execution).
-J
JVM options specified on the command line are concatenated after the alias options
"},{"location":"reference/clojure-cli/jvm-options/#calling-a-clojure-uberjar","title":"Calling A Clojure Uberjar","text":"JVM options must be specified when calling an uberjar with the java
command, :jvm-opts
in the project deps.edn
are not used with the java
command
java -jar project-uberjar.jar -J...\n
Use JDK_JAVA_OPTIONS
to define JVM options
JDK_JAVA_OPTIONS
environment variable is used to define options that are used whenever the java
command is called, greatly simplifying java
commands.
The JDK_JAVA_OPTIONS
environment variable can be used with deployment systems and passed to container environments to simplify adjustment of resources used by the JVM process.
"},{"location":"reference/clojure-cli/jvm-options/#clojure-related-jvm-options","title":"Clojure related JVM options","text":"Specify options or system properties to set up the Clojure service
-Dclojure.compiler.disable-locals-clearing=true
- make more info available to debuggers
-Dclojure.main.report=stderr
- print stack traces to standard error instead of saving to file, useful if process failing on startup
-Dclojure.spec.skip-macros=false
- skip spec checks against macro forms
"},{"location":"reference/clojure-cli/jvm-options/#memory-management","title":"Memory Management","text":"-XX:CompressedClassSpaceSize=3G
- prevent a specific type of OOMs
-XX:MaxJavaStackTraceDepth=1000000
- prevents trivial Stack Overflow errors
-Xmx24G
- set high maximum heap, preventing certain types of Out Of Memory errors (ideally high memory usage should be profiled if cause not known)
-Xss6144k
- increase stack size x6 to prevent Stack Overflow errors
The current default can be found with java -XX:+PrintFlagsFinal -version 2>/dev/null | grep \"intx ThreadStackSize\"
-Xms6G
- Set minimum memory that is equal or greater than memory used by a running REPL, to improve performance
-Xmx1G
- limit maximum heap allocation so a process can never use more memory, useful for environments with limited memory resources
:jvm/mem-max1g {:jvm-opts [\"-Xmx1G\"]}\n
"},{"location":"reference/clojure-cli/jvm-options/#container-memory-management","title":"Container Memory Management","text":"JDK_JAVA_OPTIONS
environment variable should be used for setting JVM options within a container or in the provisioning service (e.g. Kubernettes / Argo CD) that deploys containers.
Use JVM options that optimise running in a container
-
-XshowSettings:system
to output the resources the JVM believes it has access too, a very simple diagnostic tool to include
-
-XX:+UseContainerSupport
instruct the JVM that it is running in a container environment, disabling the checks the JVM would otherwise carry out to determine if it was running in a container. Can save a very small amount of start up time, though mainly used to ensure the JVM knows its in a container.
-
-XX:MaxRAMPercentage=90
to set a relative maximum percentage of heap to use, based on the memory available from the host, e.g. -XX:MaxRAMPercentage=80
will use a heap size of 80% of the available host memory
"},{"location":"reference/clojure-cli/jvm-options/#dockerfile-example-with-jdk_java_options-environment-variable","title":"Dockerfile example with JDK_JAVA_OPTIONS environment variable","text":"In this Dockerfile
excerpt the JDK_JAVA_OPTIONS
environment variable is used to print out the resources the JVM believes it has access to at startup. The JVM is instructed that it is running in a container environment and should use a maximum 90% heap size of the hosts memory resource.
ENV JDK_JAVA_OPTIONS \"-XshowSettings:system -XX:+UseContainerSupport -XX:MaxRAMPercentage=90\"\nCMD [\"java\", \"-jar\", \"/opt/practicalli-service.jar\"]\n
"},{"location":"reference/clojure-cli/jvm-options/#low-latency-systems","title":"Low latency systems","text":"For systems that require very low latency, use the Z Garbage collector
\"-XX:+UnlockExperimentalVMOptions -XX:+UseZGC\"\n
"},{"location":"reference/clojure-cli/jvm-options/#stack-traces","title":"Stack traces","text":"-XX:+TieredCompilation
- enable tiered compilation to support accurate bench-marking (increases startup time)
-XX:-OmitStackTraceInFastThrow
- don't elide stack traces
"},{"location":"reference/clojure-cli/jvm-options/#startup-options","title":"Startup options","text":"-Xverify:none
option reduces startup time of the JVM by skipping verification process
\"-Xverify:none\"\n
The verification process is a valuable check, especially for code that has not been run before. So the code should be run through the verification process before deploying to production.
"},{"location":"reference/clojure-cli/jvm-options/#benchmark-options","title":"Benchmark options","text":"Enable various optimizations, for guaranteeing accurate benchmarking (at the cost of slower startup):
\"-server\"
"},{"location":"reference/clojure-cli/jvm-options/#graphical-ui-related-options","title":"Graphical UI related options","text":"-Djava.awt.headless=true
- disable all UI features for disabling the clipboard for personal security:
-Dapple.awt.UIElement=true
- remove icon from the MacOSX Dock
-Dclash.dev.expound=true
- ?
"},{"location":"reference/clojure-cli/jvm-options/#garbage-collection","title":"Garbage Collection","text":"Setup GC with short STW pauses which can be relevant for very high web server workloads
:jvm/g1gc\n{:jvm-opts [\"-XX:+UseG1GC\"\n \"-XX:MaxGCPauseMillis=200\"\n \"-XX:ParallelGCThreads=20\"\n \"-XX:ConcGCThreads=5\"\n \"-XX:InitiatingHeapOccupancyPercent=70\"]}\n
- Source: Tuning Garbage Collection with Oracle JDK
"},{"location":"reference/clojure-cli/jvm-options/#view-jvm-options-of-a-running-jvm-process","title":"View JVM options of a running JVM process","text":"Use a JMX client, e.g. VisualVM
jcmd pid VM.system_properties
or jcmd pid VM.flags
using jcmd -l
to get the pid of the JVM process
On Linux ps -ef | grep java
which includes the options to run the JVM process, ps -auxww
to show long arguments
Getting the parameters of a running JVM
"},{"location":"reference/clojure-cli/jvm-options/#references","title":"References","text":"JVM Options cheatsheet - JRebel
"},{"location":"reference/clojure-svg/","title":"Clojure Scalable Vector Graphics - SVG","text":"Scalable Vector Graphics, SVG, is an image format for two-dimensional (2D) graphics.
An SVG image uses data to describe how to draw an image, ensuring that images can shrink and scale easily and retain a high quality image. As images are formed from data, shapes can easily be combined or intersected to form new shapes. Using a data format also means SVG images can be created from code and therefore animated.
Raster image formats like gif, jpeg and png use a grid of squares called pixels to define an image (also known as a bitmap). Each pixel has a colour and position in an image. When zooming into an image the pixels grow larger distorting the sharpness of an image, referred to as pixelation, .Multiple versions of raster images are often created at different resolutions to reduce the loss of quality when viewed at different sizes.
"},{"location":"reference/clojure-svg/#hintwork-in-progress","title":"Hint::Work in progress","text":""},{"location":"reference/clojure-svg/#concepts","title":"Concepts","text":" - viewbox
- style - border, background, width, height, stoke, fill, draw (path)
- shapes - circle, path
"},{"location":"reference/clojure-svg/#viewbox","title":"Viewbox","text":"A viewbox defines a co-ordinate system for the image. Defining a size for the viewbox defining a frame for the image where positions are relative to that frame, irrespective of the size of the image or how that image is scaled.
A viewbox size should be selected to make the image as simple as possible to define within itself.
Example: tictactoe O's and X's and the grid that represents the board.
tictactoe O's and X's and the grid that represents the board
"},{"location":"reference/clojure-svg/#related-projects","title":"Related projects","text":" - TicTacToe with ClojureScript, Reagent and SVG
- System monitoring
- Practicalli SVG examples library
- Programming SVG with Clojure (TODO)
"},{"location":"reference/clojure-svg/#references","title":"References","text":" - SVG: Scalable Vector Graphics - Mozilla Developer network
"},{"location":"reference/clojure-syntax/assigning-names/","title":"Assigning Names","text":"If we have to type the same values over and over, it would be very hard to write a program. What we need are names for values, so we can refer to them in a way we can remember. We do that using def
.
(def mangoes 3)\n(def oranges 5)\n(+ mangoes oranges)\n
When you assign a name to a value, that name is called a symbol. You can assign more than simple values to symbols. Try the following:
(def fruit (+ mangoes oranges))\n(def average-fruit-amount (/ fruit 2))\naverage-fruit-amount\n
Look at the last line, and see how we can use symbols by themselves to refer to a value.
Note Take the Clojure syntax you have learnt to far and write a metric/imperial converter
Take your height in feet and inches and convert it to inches using arithmetic in Clojure.
Then convert that to centimeters. There are 2.54 centimeters in an inch.
Lastly, ask two people near you for their height in centimeters. Find the average of your heights.
Note Bonus: Convert that average back to feet and inches. The feet and the inches will be separate numbers. (quot x y)
will give you the whole number part when dividing two numbers. (mod x y)
will give you the remainder when dividing two numbers.
"},{"location":"reference/clojure-syntax/code-documentation/","title":"Code documentation","text":"Clojure functions are documented by adding a string to the function definition, after the function name. This is referred to as the doc string.
(defn example-function\n \"This is the documentation for this function, referred to as a doc string\"\n [arguments]\n (str \"some behaviour\"))\n
def
bindings can also be documented to provide context to the data the name is bound to.
(def weather-data\n \"Data set for weather across the major capitals of Europe\"\n [{:date \"2020-05-015\" :city \"Berlin\" :temperature-max 24 :temperature-min 13 :rainfall 1}\n {:date \"2020-05-015\" :city \"Amsterdam\" :temperature-max 25 :temperature-min 14 :rainfall 0}])\n
"},{"location":"reference/clojure-syntax/code-documentation/#write-easily-understandable-docstrings","title":"Write easily understandable docstrings","text":"Practically recommends including specific details of the arguments passed to a function and the expected return type. Including this at the end of the docstring makes that information very quick to find.
\"Geographic visualization data set generator\n\n Arguments:\n - combined data set of GeoJSON and Cases\n - maximum integer value for scale\n Returns:\n - Oz view hash-map\"\n
"},{"location":"reference/clojure-syntax/code-documentation/#reading-source-code","title":"Reading source code","text":"clojure.repl/source
will show the source code of a given function, which can be a valuable way to understand the function. Reading function source code also provides ideas when writing custom Clojure code.
Reading the source code for clojure.core
functions is a good way to learn those functions, although some functions have been optimised for performance and are harder to follow.
Source code for clojure.core is available online and is also linked to from the function descriptions on clojuredocs.org.
"},{"location":"reference/clojure-syntax/code-documentation/#writing-your-own-documentation","title":"Writing your own documentation","text":"Writing good documentation for your own functions take practice which pays off in the long run.
()\n
(defn my-function\n \"I should practice writing clear and meaningful documentation for my functions.\n Arguments: brief description of arguments\"\n [arguments]\n (str \"I should write pure functions where ever possible. \"\n \"Each function should have a specific purpose. \"\n \"A function should be clean and easy to read.\"))\n
"},{"location":"reference/clojure-syntax/code-documentation/#notedefine-your-own-function","title":"Note::Define your own function","text":"Practice writing a meaningful documentation in the doc string
"},{"location":"reference/clojure-syntax/comments/","title":"Comments","text":"As well as the classic line comments, Clojure also can comment specific parts of the code structure, even when it run over multiple lines.
;;
to comment a whole line and ;
to add a comment after the start of a line
(comment )
wraps forms and returns nil
when evaluated, referred to as rich comments
#_
to ignore the next form as if it has not been written, commonly used for debugging
"},{"location":"reference/clojure-syntax/comments/#line-comments","title":"Line comments","text":"Add general documentation for a namespace, such as a comment header that describes the overall purpose of a namespace.
Separate a namespace into logical sections to aid navigation and help identify opportunities to refactor a namespace as it grows.
"},{"location":"reference/clojure-syntax/comments/#comment-function","title":"comment function","text":"The (comment ,,,)
function is used to included code that is only run by the developer directly.
(comment (map + [1 2 3] [1 2 3]))\n
The comment
function returns nil
so its advised not to use it inside another form. For example:
(map + [1 2 3] (comment [1 2 3])) ; nil will be passed to map as the third argument\n
This will fail as it tries to use the +
function to add 1
to nil
The #_
is the appropriate comment style for this example
"},{"location":"reference/clojure-syntax/comments/#rich-comment","title":"Rich comment","text":"The comment
expression is referred to a a rich comment, as it is often used to evaluate expressions it contains as part of a REPL driven development workflow.
Unlike line comments, forms inside a comment block can be evaluated in a Clojure aware editor to help the developer work with a project.
Rich comment are useful for rapidly iterating over different design decisions by including the same function but with different implementations. Hide clj-kondo linter](/clojure-cli/install/install-clojure.html#clj-kondo-static-analyser--linter) warnings for redefined vars (def
, defn
) when using this approach.
;; Rich comment block with redefined vars ignored\n#_{:clj-kondo/ignore [:redefined-var]}\n(comment\n\n ) ;; End of rich comment block\n
The expressions can represent example function for using the project, such as starting/restarting the system, updating the database, etc.
Expressions in rich comment blocks can also represent how to use a namespace API, providing examples of arguments to supply to further convey meaning to the code.
These rich comments make a project more accessible and easier to use.
The \"Rich\" in the name also refers to Rich Hickey, the author and benevolent leader of the Clojure language.
"},{"location":"reference/clojure-syntax/comments/#comment-forms-with-the-comment-reader-macro","title":"Comment forms with the comment reader macro","text":"#_
is the comment reader macro that instructs the Clojure reader to completely ignore the next form, as if it had never been written.
No value is returned, so this comment is safe to use within an expression.
You can place #_
before the start of a form and everything inside that form will be commented
#_(def my-data [1 2 3 4 5])\n
#_
will comment forms that span multiple lines, for example function definitions
#_(defn my-function\n [args]\n (str \"I am an experiment, so not always needed\"))\n
#_
can also be put on the line(s) before the Clojure form, which can make your code more readable and keep alignment of your code consistent.
"},{"location":"reference/clojure-syntax/comments/#debugging-with-comment-macro","title":"debugging with comment macro","text":"As the comment macro can be used without returning a value, it can safely be added to code to help with debugging.
This code example finds the most common word in the text of a book. Most of the lines of code in the threading macro have been commented to discover what the non-commented code does.
As each expression in the threading macros is understood, by looking at its results, comments can be removed to understand more of the code.
(defn most-common-words [book]\n (->> book\n (re-seq #\"[a-zA-Z0-9|']+\" ,,,)\n #_(map #(clojure.string/lower-case %))\n #_(remove common-english-words)\n #_frequencies\n #_(sort-by val)\n #_reverse\n ))\n
This is an effective way to deconstruct parts of a larger Clojure expression.
Watch episode #13 of Practicalli Clojure study group to see this in practice.
"},{"location":"reference/clojure-syntax/comments/#comment-nested-forms","title":"comment nested forms","text":"#_
tells the reader to ignore the next form, it is therefore never evaluated and neither is the #_
. This means that #_
can be used inside a nested form to comment just a part of the expression
In this example the third vector of values is not read by the Clojure reader and therefore is not passed as an argument to +
function by map
(map + [1 2 3] [4 5 6] #_[7 8 9])
"},{"location":"reference/clojure-syntax/comments/#stacking-comments","title":"Stacking comments","text":"The comment reader macro has the ability to stack these comments on forms, so using #_#_
will comment out two successive forms.
In a let
form we can comment out a name binding that is causing problems. As the name and value are both forms, then we use a stacked #_
to comment both out. We also do the same in the body of the let, so as to not include the evaluation of the string or name2
local name in the str
form.
(let [name1 \"value\"\n #_#_name2 \"another-value]\n (str \"First name is: \" name1 #_#_\" second name is: \" name2\n
"},{"location":"reference/clojure-syntax/control-flow/","title":"Control Flow","text":"The following section of functions gives examples of simple control flow. As you gain more experience with Clojure, you will discover more functional ways to achieve the same (or better) results.
Hint Although these functions may seem similar to other non-functional languages, there are subtle differences
"},{"location":"reference/clojure-syntax/control-flow/#if","title":"If","text":"Using the if
function you can test if an expression evaluates to true. If it is true, the first value is returned, if its false the second value is returned.
Here is a simple example to see if one number is bigger that another
(if (> 3 2)\n \"Higher\"\n \"Lower\")\n\n=> \"Higher\"\n
Here is an example of an condition inside an anonymous function.
(defn even-number [number]\n (if (odd? number)\n (inc number)\n number))\n\n(even-number 41)\n;; => 42\n
"},{"location":"reference/clojure-syntax/control-flow/#when","title":"When","text":"When a condition is true, then return the value of evaluating the next expression. If the condition is false, then return nil
(when (> 3 2)\n \"Higher\")\n\n=> \"Higher\"\n
"},{"location":"reference/clojure-syntax/control-flow/#case","title":"Case","text":"When one of these things is true, do this, else default
(case (inc 3)\n 1 \"Not even close\"\n 2 \"I wish I was that young\"\n 3 \"That was my previous value\"\n 4 \"You sunk my battleship\"\n \"I dont think you understood the question\")\n\n=> \"You sunk my battleship\"\n
"},{"location":"reference/clojure-syntax/control-flow/#cond","title":"Cond","text":"Return the associated value of the first condition that is true, or return the default value specified by :otherwise
(cond\n (= 7 (inc 2)) \"(inc 2) is not 7, so this condition is false\"\n (= 16 (* 8 2)) \"This is the first correct condition so its associated expression is returned\"\n (zero? (- (* 8 8) 64)) \"This is true but not returned as a previous condition is true\"\n :otherwise \"None of the above are true\")\n\n;; => \"This is the first correct condition so its associated expression is returned\"\n
"},{"location":"reference/clojure-syntax/control-flow/#for","title":"For","text":"Using the for
function you can Iterate through the values in a collection and evaluate each value in tern with with a condition, using either :when
or :while
.
(for [x (range 10) :when (odd? x)] x)\n\n(for [x (range 10) :while (even? x)] x)\n\n(for [x (range 10)\n y (range 10)]\n [x y])\n
"},{"location":"reference/clojure-syntax/control-flow/#while","title":"While","text":"Do something while the condition is true
(while (condition)\n (do something))\n
Here is a simple while example that uses a (mutable) counter and prints out the results to the repl window.
;; create a counter using a mutable counter\n(def counter (atom 10))\n\n;; While the counter is positive (is a number greater than zero), print out the current value of the counter.\n(while (pos? @counter)\n (do\n (println @counter)\n (swap! counter dec)))\n
This example uses mutable state and causes a side effect by printing to the repl. Both these kinds of things are typically kept to a minimum in Clojure.
TODO An alternative would be to use use the iteration over a collection to control the while condition
"},{"location":"reference/clojure-syntax/defining-functions/","title":"Defining Functions","text":"clojure.core/defn
defines a custom function that can be called from anywhere in the current namespace by just using the name. A defined function can be called from where ever its namespace is required in other namespaces.
Here is a simple function definition that takes a number and divides it by two
(defn half-a-number\n \"Divide a given number by 2\"\n [number]\n (/ number 2))\n
Once you have defined a function, you can call it by using the function name as the first element of a list, ()
. Any other elements in the list are arguments passed to the function.
(half-a-number 4)\n
"},{"location":"reference/clojure-syntax/defining-functions/#understanding-the-defn-syntax","title":"Understanding the defn
syntax","text":"The standard form of defn
:
(defn name doc-string? attr-map? [params*] prepost-map? body)\n
name is a symbol used to call the function.
doc-string? is an optional string used to provide a meaningful description of the function definition. This description is the living documentation of the function and can be accessed via `clojure.repl/doc** functions and Clojure aware editors.
attr-map? is an optional map for pre-conditions for a function.
[params*] is a zero or more vector of symbols that represent the arguments passed to a function. The number of symbols defined must be matched when calling the function, or an exception will occur.
prepost-map? an optional map for post-conditions for a function.
body is the algorithm that will evaluate when the function is called.
There is a second form of the defn
function, one which responds differently based on the number of arguments used to call the function (polymorphic).
(defn name doc-string? attr-map?\n ([params*] prepost-map? body) + attr-map?)\n
Thinking Functionally - Polymorphism has examples of using defn to define polymorphic functions
"},{"location":"reference/clojure-syntax/defining-functions/#breaking-down-the-defn-syntax","title":"Breaking down the defn syntax","text":"The syntax defn
is what we call a macro, it is a simpler way to write clojure code that does the same thing.
You can think of defining a function with defn
as two steps
- Give the function a name - using the
def
syntax - Define the functions behaviour and arguments it takes - using the
fn
syntax
Here is the same function if you typed it out in full
(def half-a-number\n (fn [number]\n (/ number 2)))\n
"},{"location":"reference/clojure-syntax/defining-functions/#hintmacroexpand-functions","title":"Hint::Macroexpand functions","text":"The macroexpand-1
function takes an expression that includes one or more macros and returns the expanded version of Clojure code. The macroexpand-all
will also expand macros into Clojure code, doing so recursively for all macros it finds.
Clojure editors also provide evaluation functions that will macroexpand.
"},{"location":"reference/clojure-syntax/global-definitions/","title":"Global definitions","text":"Fixme work in progress
"},{"location":"reference/clojure-syntax/java-interop/","title":"Java Interoperability","text":"Clojure provides very clear and simple syntax for Java interoperability, using the following functions
import
- add functions from the Java library into the current namespace new
- create a new Java object .
- is the short form of the new
function
As Clojure is hosted on the Java Virtual Machine (JVM), its very easy to include libraries from any other languages that runs on the JVM, for example Java, Groovy, Scala, Jython, JRuby, Jaskell, etc.
The Leiningen build tool provides a simple way to include libraries as dependencies, using the :dependencies
section of the project.clj
file. Any library published to Maven Central is available for download by Leiningen, as both Maven Central and Clojars.org repositories are used by default.
java.lang included
Clojure projects and REPL environments include the java.lang
library automatically. Any methods from that library can be used without having to import
them or include any dependencies
"},{"location":"reference/clojure-syntax/java-interop/#the-syntax","title":"The syntax","text":"Its very easy to call Java methods and objects from clojure using the following syntax
(.instanceMember instance args*)\n(.instanceMember Classname args*)\n(.-instanceField instance)\n(Classname/staticMethod args*)\nClassname/staticField\n
Note Use the instanceMember .toUpperCase to convert a string from lower case to upper case
Call the .toUpperCase
function on any string you like, for example
(.toUpperCase \"I was low, but now I'm up\")\n
The string passed as an argument should now be all uppercase: \"I WAS LOW, BUT NOW I'M UP\"
Note Use the staticField Math/PI
to return the approximate value of Pi
You can treat any static field like any name defined in your Clojure code, so when evaluated the static field simply returns the value it represents
In this case the Math/PI
static field simply returns the approximate value of Pi that fits into a java.lang.Double type.
Math/PI\n-> 3.141592653589793\n
"},{"location":"reference/clojure-syntax/java-interop/#getting-the-java-environment","title":"Getting the Java environment","text":"Earlier we used Clojure functions to find information about our environment. We can also used the getProperty()
method from the java.lang.System
object to ask for the java version and jvm name.
Note Get version of Java & the JVM, returning those values as a meaningful string. Then get the version of the Clojure project
(str \"You are running Java version \" (System/getProperty \"java.version\") \"with the JVM\" (System/getProperty \"java.vm.name\"))\n\n(str \"Latest project version: \" (System/getProperty \"playground.version\"))\n
Note Use System/getenv
to return your system's environment variables as a map
(System/getenv)\n
You may notice that this is a map data structure that we return, so we can use use destructuring or the maps behaviour itself to pull out information.
Hint A full list of properties can be seen in the getProperty() documentation
There are more examples of Java Interoperability in the next few sections.
"},{"location":"reference/clojure-syntax/local-bindings/","title":"Local Bindings","text":"Fixme work in progress
"},{"location":"reference/clojure-syntax/more-java-fun/","title":"More Java fun","text":"Lets try some more examples to show how easy it is to use Java methods and objects. Remember that everything in java.lang is available in your Clojure project by default
"},{"location":"reference/clojure-syntax/more-java-fun/#returning-specific-types","title":"Returning specific types","text":"Clojure has types, after all it runs on the JVM and included java.lang
library in ever project. Types are inferred at runtime, saving you the need to design types yourself.
Sometimes you want to ensure a value is of a particular type and you can use Java to do this.
Note Return a string value as an integer
When you create a new java.lang.Integer
object you can provide a default value by passing either a number or string type.
(new Integer \"123\")\n\n;; Or the more common short-hand forms\n\n(Integer. \"123\")\n
This is the equivalent to the Java code:
Integer myInt = new Integer(\"123\");\n
The .
function essentially instantiates a new object from the class, in this case Integer
, passing any arguments to its constructor.
Hint Example: converting the port number read from an environment variable as a string which needs to be passed to the Jetty server as a number. See the Clojure Webapp workshop an example.
More on types in the section a quick look at types
fixme The following is work in progress
"},{"location":"reference/clojure-syntax/more-java-fun/#using-java-date","title":"Using Java date","text":"Note Use java.util.Date
to explore date and time
(import java.util.Date)\n\n(Date.)\n\n(def now (Date.))\n\n(str now)\n
Its easy to create a local reference to a Java Date object instance and then call methods on that date object
(let [date (java.util.Date.)] (.getHours date))\n
Or using the threading macro, we can make the code a little clearer
(->\n (java.util.Date.)\n (.getHours))\n
"},{"location":"reference/clojure-syntax/more-java-fun/#its-joda-time","title":"Its Joda Time","text":"clj-time
is a Clojure wrapper for Joda time. As this is an external library, you need to add it to your project.clj file as a dependency. To find the latest version, check the clj-time library on Clojars.org
Note Add the clj-time dependency to your project (restart needed), require the clj-time library in your code and use the functions now
, formatters
& unparse
(require '[clj-time.core :as time])\n(require '[clj-time.format :as time-format])\n\n(time/now)\n\n;; ISO 8601 UTC format\n(def time-formatter (time-format/formatters :basic-date-time))\n(time-format/unparse custom-formatter (date-time 2010 10 3))\n
"},{"location":"reference/clojure-syntax/more-java-fun/#swing-coding","title":"Swing coding","text":"Swing GUI coding in Java feels quite messy to me, however using Swing in Clojure feels much cleaner. Using the doto
function allow you to chain function (Java method) calls together.
Note Start with the import
function to add the necessary swing libraries. Then create a button and add it to a panel, adding that panel to a frame.
(import '(javax.swing JFrame JPanel JButton))\n(def button (JButton. \"Click Me!\"))\n(def panel (doto (JPanel.)\n (.add button)))\n(def frame (doto (JFrame. \"Hello Frame\")\n (.setSize 200 200)\n (.setContentPane panel)\n (.setVisible true)))\n
Let\u2019s make our button show a message using an JOptionPane/showMessageDialog widget
(import 'javax.swing.JOptionPane)\n(defn say-hello []\n (JOptionPane/showMessageDialog\n nil \"Hello, World!\" \"Greeting\"\n JOptionPane/INFORMATION_MESSAGE))\n
To connect this function to our button, write a class implementing the ActionListener interface. Clojure\u2019s proxy feature is the easiest way to do this:
(import 'java.awt.event.ActionListener)\n(def act (proxy [ActionListener] []\n (actionPerformed [event] (say-hello))))\n
act
is an instance of an anonymous class implementing the actionPerformed method, so attach this class as a listener the button
(.addActionListener button act)\n
Now evaluate the say-hello
function to see the new button in action.
(say-hello)\n
Hint Seesaw is a really nice library for swing development. Also talk a look at the Seesaw minesweeper series.
"},{"location":"reference/clojure-syntax/more-java-fun/#understanding-the-dot-special-form","title":"Understanding the dot special form","text":"Fixme This section onwards needs reworking
All of these examples (except java.lang.Math/PI) use macros which expand to use the dot special form. In general, you won't need to use the dot special form unless you want to write your own macros to interact with Java objects and classes. Nevertheless, here is each example followed by its macroexpansion:
(macroexpand-1 '(.toUpperCase \"By Bluebeard's bananas!\"))\n; => (. \"By Bluebeard's bananas!\" toUpperCase)\n\n(macroexpand-1 '(.indexOf \"Synergism of our bleeding edges\" \"y\"))\n; => (. \"Synergism of our bleeding edges\" indexOf \"y\")\n\n(macroexpand-1 '(Math/abs -3))\n; => (. Math abs -3)\n
You can think of the general form of the dot operator as:
(. object-expr-or-classname-symbol method-or-member-symbol optional-args*)\n
There are a few more details to the dot operator than that, and if you're interested in exploring it further you can look at clojure.org's documentation on Java interop.
Input/output involves resources, be they files, sockets, buffers, or whatever. Java has separate classes for reading a resource's contents, writings its contents, and for interacting with the resource's properties.
For example, the java.io.File class is used to interact with a file's properties. Among other things, you can use it to check whether a file exists, to get the file's read/write/execute permissions, and to get its filesystem path:
(let [file (java.io.File. \"/\")]\n (println (.exists file))\n (println (.canWrite file))\n (println (.getPath file)))\n; => true\n; => false\n; => /\n
Noticeably missing from this list of capabilities are reading and writing. To read a file, you could use the java.io.BufferedReader class or perhaps java.io.FileReader. Likewise, you can use the java.io.BufferedWriter or java.io.FileWriter class for writing. There are other classes available for reading and writing as well, and which one you choose depends on your specific needs. Reader and Writer classes all have the same base set of methods for their interfaces; readers implement read, close, and more, while writers implement append, write, close, and flush. So, Java gives you a variety of tools for performing IO. A cynical person might say that Java gives you enough rope to hang yourself, and if you find such a person I hope you give them just enough arms to hug them.
Either way, Clojure makes things easier for you. First, there's spit and slurp. Spit writes to a resource, and slurp reads from one. Here's an example of using them to write and read a file:
(spit \"/tmp/hercules-todo-list\"\n\"- wash hair\n - charm the multi-headed snake\")\n\n(slurp \"/tmp/hercules-todo-list\")\n\n; => \"- wash hair\n; => - charm the multi-headed snake\"\n
You can also use these functions with objects representing resources other than files. The next example uses a StringWriter, which allows you to perform IO operations on a string:
(let [s (java.io.StringWriter.)]\n (spit s \"- charm the multi-headed snake\")\n (.toString s))\n; => \"- charm the multi-headed snake\"\n
Naturally, you can also read from a StringReader with slurp:
(let [s (java.io.StringReader. \"- charm the multi-headed snake\")]\n (slurp s))\n; => \"- charm the multi-headed snake\"\n
Of course, you can also use the read and write methods for resources. It doesn't really make much of a difference which you use; spit and slurp are often convenient because they work with just a string representing a filesystem path or a URL.
The with-open macro is another convenience: it implicitly closes a resource at the end of its body. There's also the reader function, a nice utility which, according to the clojure.java.io api docs, \"attempts to coerce its argument to an open java.io.Reader.\" This is convenient when you don't want to use slurp because you don't want to try to read a resource in its entirety, and you don't want to figure out which Java class you need to use. You could use it along with with-open and the line-seq function if you're trying to read a file one line at a time:
(with-open [todo-list-rdr (clojure.java.io/reader \"/tmp/hercules-todo-list\")]\n (doseq [todo (line-seq todo-list-rdr)]\n (println todo)))\n; => \"- wash hair\n; => - charm the multi-headed snake\"\n
That should be enough for you to get started with IO in Clojure. If you're trying to do something more sophisticated, definitely take a look at the clojure.java.io docs, the java.nio.file package docs, or the java.io package docs. 5. Summary
In this chapter, you learned what it means for Clojure to be hosted on the JVM. Clojure programs get compiled to Java bytecode and executed within a JVM process. Clojure programs also have access to Java libraries, and you can easily interact with them using Clojure's interop facilities. 6. Resources
"},{"location":"reference/clojure-syntax/more-java-fun/#from-httpclojureorgjava_interop","title":"From http://clojure.org/java_interop","text":"(.instanceMember instance args*)\n(.instanceMember Classname args*)\n(.-instanceField instance)\n\n(.toUpperCase \"fred\")\n-> \"FRED\"\n(.getName String)\n-> \"java.lang.String\"\n(.-x (java.awt.Point. 1 2))\n-> 1\n(System/getProperty \"java.vm.version\")\n-> \"1.6.0_07-b06-57\"\nMath/PI\n-> 3.141592653589793\n
The preferred idiomatic forms for accessing field or method members are given above. The instance member form works for both fields and methods. The instanceField form is preferred for fields and required if both a field and a 0-argument method of the same name exist. They all expand into calls to the dot operator (described below) at macroexpansion time. The expansions are as follows:
(.instanceMember instance args*) ==> (. instance instanceMember args*)\n(.instanceMember Classname args*) ==>\n (. (identity Classname) instanceMember args*)\n(.-instanceField instance) ==> (. instance -instanceField)\n(Classname/staticMethod args*) ==> (. Classname staticMethod args*)\nClassname/staticField ==> (. Classname staticField)\n
The Dot special form
(. instance-expr member-symbol)\n(. Classname-symbol member-symbol)\n(. instance-expr -field-symbol)\n(. instance-expr (method-symbol args*)) or\n(. instance-expr method-symbol args*)\n(. Classname-symbol (method-symbol args*)) or\n(. Classname-symbol method-symbol args*)\n
Special form.
The '.' special form is the basis for access to Java. It can be considered a member-access operator, and/or read as 'in the scope of'.
If the first operand is a symbol that resolves to a class name, the access is considered to be to a static member of the named class. Note that nested classes are named EnclosingClass$NestedClass, per the JVM spec. Otherwise it is presumed to be an instance member and the first argument is evaluated to produce the target object.
If the second operand is a symbol and no args are supplied it is taken to be a field access - the name of the field is the name of the symbol, and the value of the expression is the value of the field, unless there is a no argument public method of the same name, in which case it resolves to a call to the method. If the second operand is a symbol starting with -, the member-symbol will resolve only as field access (never as a 0-arity method) and should be preferred when that is the intent.
If the second operand is a list, or args are supplied, it is taken to be a method call. The first element of the list must be a simple symbol, and the name of the method is the name of the symbol. The args, if any, are evaluated from left to right, and passed to the matching method, which is called, and its value returned. If the method has a void return type, the value of the expression will be nil. Note that placing the method name in a list with any args is optional in the canonic form, but can be useful to gather args in macros built upon the form.
Note that boolean return values will be turned into Booleans, chars will become Characters, and numeric primitives will become Numbers unless they are immediately consumed by a method taking a primitive.
The member access forms given at the top of this section are preferred for use in all cases other than in macros.
(.. instance-expr member+)\n(.. Classname-symbol member+)\nmember => fieldName-symbol or (instanceMethodName-symbol args*)\n
Macro. Expands into a member access (.) of the first member on the first argument, followed by the next member on the result, etc. For instance:
(.. System (getProperties) (get \"os.name\"))\n
expands to:
(. (. System (getProperties)) (get \"os.name\"))\n
but is easier to write, read, and understand. See also the -> macro which can be used similarly:
(-> (System/getProperties) (.get \"os.name\"))\n
(doto instance-expr (instanceMethodName-symbol args)) Macro. Evaluates instance-expr then calls all of the methods/functions with the supplied arguments in succession on the resulting object, returning it.
(doto (new java.util.HashMap) (.put \"a\" 1) (.put \"b\" 2))\n-> {a=1, b=2}\n
Note the above applies to the latest Clojure SVN revision. If you are using the 20080916 release only method calls are allowed, and the syntax is:
(doto (new java.util.HashMap) (put \"a\" 1) (put \"b\" 2))\n-> {a=1, b=2}\n
(Classname. args) (new Classname args)
Special form. The args, if any, are evaluated from left to right, and passed to the constructor of the class named by Classname. The constructed object is returned.
Alternative Macro Syntax
As shown, in addition to the canonic special form new, Clojure supports special macroexpansion of symbols containing '.':
(new Classname args*)\n
can be written
(Classname. args*)\n;; note trailing dot\n
the latter expanding into the former at macro expansion time.
(instance? Class expr) Evaluates expr and tests if it is an instance of the class. Returns true or false
(set! (. instance-expr instanceFieldName-symbol) expr) (set! (. Classname-symbol staticFieldName-symbol) expr) Assignment special form. When the first operand is a field member access form, the assignment is to the corresponding field. If it is an instance field, the instance expr will be evaluated, then the expr.
In all cases the value of expr is returned.
Note - you cannot assign to function params or local bindings. Only Java fields, Vars, Refs and Agents are mutable in Clojure.
(memfn method-name arg-names*) Macro. Expands into code that creates a fn that expects to be passed an object and any args and calls the named instance method on the object passing the args. Use when you want to treat a Java method as a first-class fn.
(map (memfn charAt i) [\"fred\" \"ethel\" \"lucy\"] [1 2 3]) -> (\\r \\h \\y)
Note it almost always preferable to do this directly now, with syntax like:
(map #(.charAt %1 %2) [\"fred\" \"ethel\" \"lucy\"] [1 2 3]) -> (\\r \\h \\y)
(bean obj) Takes a Java object and returns a read-only implementation of the map abstraction based upon its JavaBean properties.
(bean [[http://java.awt.Color/black|java.awt.Color/black]]) -> {:RGB -16777216, :alpha 255, :transparency 1, :class class java.awt.Color, :green 0, :blue 0, :colorSpace java.awt.color.ICC_ColorSpace@c94b51, :red 0}
Support for Java in Clojure Library Functions
Many of the Clojure library functions have defined semantics for objects of Java types. contains? and get work on Java Maps, arrays, Strings, the latter two with integer keys. count works on Java Strings, Collections and arrays. nth works on Java Strings, Lists and arrays. seq works on Java reference arrays, Iterables and Strings. Since much of the rest of the library is built upon these functions, there is great support for using Java objects in Clojure algorithms.
Implementing Interfaces and Extending Classes
Clojure supports the dynamic creation of objects that implement one or more interfaces and/or extend a class with the proxy macro. The resulting objects are of an anonymous class. You can also generate statically-named classes and .class files with gen-class. As of Clojure 1.2, reify is also available for implementing interfaces.
( proxy [class-and-interfaces] [args] fs+) class-and-interfaces - a vector of class names args - a (possibly empty) vector of arguments to the superclass constructor. f => (name [params] body) or (name ([params] body) ([params+] body) ...)
Macro
Expands to code which creates a instance of a proxy class that implements the named class/interface(s) by calling the supplied fns.
A single class, if provided, must be first. If not provided it defaults to Object.
The interfaces names must be valid interface types. If a method fn is not provided for a class method, the superclass method will be called.
If a method fn is not provided for an interface method, an UnsupportedOperationException will be thrown should it be called.
Method fns are closures and can capture the environment in which proxy is called. Each method fn takes an additional implicit first argument, which is bound to this.
Note that while method fns can be provided to override protected methods, they have no other access to protected members, nor to super, as these capabilities cannot be a proxy.
Arrays
Clojure supports the creation, reading and modification of Java arrays. It is recommended that you limit use of arrays to interop with Java libraries that require them as arguments or use them as return values.
Note that many other Clojure functions work with arrays such as via the seq library. The functions listed here exist for initial creation of arrays, or to support mutation or higher performance operations on arrays.
Create array from existing collection: aclone amap to-array to-array-2d into-array Multi-dimensional array support: aget aset to-array-2d make-array Type-specific array constructors: boolean-array byte-array char-array double-array float-array int-array long-array object-array short-array Primitive array casts: booleans bytes chars doubles floats ints longs shorts Mutate an array: aset Process an existing array: aget alength amap areduce
Type Hints
Clojure supports the use of type hints to assist the compiler in avoiding reflection in performance-critical areas of code. Normally, one should avoid the use of type hints until there is a known performance bottleneck. Type hints are metadata tags placed on symbols or expressions that are consumed by the compiler. They can be placed on function parameters, let-bound names, var names (when defined), and expressions:
(defn len [x] (.length x))
(defn len2 [^String x] (.length x))
user=> (time (reduce + (map len (repeat 1000000 \"asdf\")))) \"Elapsed time: 3007.198 msecs\" 4000000 user=> (time (reduce + (map len2 (repeat 1000000 \"asdf\")))) \"Elapsed time: 308.045 msecs\" 4000000
Once a type hint has been placed on an identifier or expression, the compiler will try to resolve any calls to methods thereupon at compile time. In addition, the compiler will track the use of any return values and infer types for their use and so on, so very few hints are needed to get a fully compile-time resolved series of calls. Note that type hints are not needed for static members (or their return values!) as the compiler always has the type for statics.
There is a warn-on-reflection flag (defaults to false) which will cause the compiler to warn you when it can't resolve to a direct call:
(set! warn-on-reflection true) -> true
(defn foo [s] (.charAt s 1)) -> Reflection warning, line: 2 - call to charAt can't be resolved. -> #user/foo
(defn foo [^String s] (.charAt s 1)) -> #user/foo
For function return values, the type hint can be placed before the arguments vector:
(defn hinted (^String []) (^Integer [a]) (^java.util.List [a & args]))
-> #user/hinted
Aliases
Clojure provides aliases for primitive Java types and arrays which do not have typical representations as Java class names. For example, long arrays (long-array []) have a type of \"[J\".
int - A primitive int\nints - An int array\nlong - A primitive long\nlongs - A long array\nfloat - A primitive float\nfloats - A float array\ndouble - A primitive double\ndoubles - A double array\nvoid - A void return\nshort - A primitive short\nshorts - A short array\nboolean - A primitive boolean\nbooleans - A boolean array\nbyte - A primitive byte\nbytes - A byte array\nchar - A primitive character\nchars - A character array\n
Support for Java Primitives
Clojure has support for high-performance manipulation of, and arithmetic involving, Java primitive types in local contexts. All Java primitive types are supported: int, float, long, double, boolean, char, short, and byte.
let/loop-bound locals can be of primitive types, having the inferred, possibly primitive type of their init-form.\nrecur forms that rebind primitive locals do so without boxing, and do type-checking for same primitive type.\nArithmetic (+,-,*,/,inc,dec,<,<=,>,>= etc) is overloaded for primitive types where semantics are same.\naget/aset are overloaded for arrays of primitives\naclone, alength functions for arrays of primitives\nconstructor functions for primitive arrays: float-array, int-array, etc.\nType hints for primitive arrays - ^ints, ^floats, etc.\nCoercion ops int, float, etc. produce primitives when consumer can take primitive\nThe num coercion function boxes primitives to force generic arithmetic\nArray cast functions ints longs, etc. which produce int[], long[], etc.\nA set of \"unchecked\" operations for utmost performing, but potentially unsafe, integer (int/long) ops: unchecked-multiply unchecked-dec unchecked-inc unchecked-negate unchecked-add unchecked-subtract unchecked-remainder unchecked-divide\nA dynamic var to automatically swap safe operations with unchecked operations: *unchecked-math*\namap and areduce macros for functionally (i.e. non-destructively) processing one or more arrays in order to produce a new array or aggregate value respectively.\n
Rather than write this Java:
static public float asum(float[] xs){ float ret = 0; for(int i = 0; i < xs.length; i++) ret += xs[i]; return ret; }
you can write this Clojure:
(defn asum [^floats xs] (areduce xs i ret (float 0) (+ ret (aget xs i))))
and the resulting code is exactly the same speed (when run with java -server).
The best aspect of this is that you need not do anything special in your initial coding. Quite often these optimizations are unneeded. Should a bit of code be a bottleneck, you can speed it up with minor adornment:
(defn foo [n] (loop [i 0] (if (< i n) (recur (inc i)) i)))
(time (foo 100000)) \"Elapsed time: 0.391 msecs\" 100000
(defn foo2 [n] (let [n (int n)] (loop [i (int 0)] (if (< i n) (recur (inc i)) i))))
(time (foo2 100000)) \"Elapsed time: 0.084 msecs\" 100000
Coercions At times it is necessary to have a value of a particular primitive type. These coercion functions yield a value of the indicated type as long as such a coercion is possible: bigdec bigint boolean byte char double float int long num short
Some optimization tips
All arguments are passed to Clojure fns as objects, so there's no point to putting non-array primitive type hints on fn args. Instead, use the let technique shown to place args in primitive locals if they need to participate in primitive arithmetic in the body.\n(let [foo (int bar)] ...) is the correct way to get a primitive local. Do not use ^Integer etc.\nDon't rush to unchecked math unless you want truncating operations. HotSpot does a good job at optimizing the overflow check, which will yield an exception instead of silent truncation. On a typical example, that has about a 5% difference in speed - well worth it. Also, people reading your code don't know if you are using unchecked for truncation or performance - best to reserve it for the former and comment if the latter.\nThere's usually no point in trying to optimize an outer loop, in fact it can hurt you as you'll be representing things as primitives which just have to be re-boxed in order to become args to the inner call. The only exception is reflection warnings - you must get rid of them in any code that gets called frequently.\nAlmost every time someone presents something they are trying to optimize with hints, the faster version has far fewer hints than the original. If a hint doesn't improve things in the end - take it out.\nMany people seem to presume only the unchecked- ops do primitive arithmetic - not so. When the args are primitive locals, regular + and * etc do primitive math with an overflow check - fast and safe.\nSo, the simplest route to fast math is to leave the operators alone and just make sure the source literals and locals are primitive. Arithmetic on primitives yields primitives. If you've got a loop (which you probably do if you need to optimize) make sure the loop locals are primitives first - then if you accidentally are producing a boxed intermediate result you'll get an error on recur. Don't solve that error by coercing your intermediate result, instead, figure out what argument or local is not primitive.\n
Simple XML Support Included with the distribution is simple XML support, found in the src/xml.clj file. All names from this file are in the xml namespace.
(parse source) Parses and loads the source, which can be a File, InputStream or String naming a URI. Returns a tree of the xml/element struct-map, which has the keys :tag, :attrs, and :content. and accessor fns tag, attrs, and content.
(xml/parse \"/Users/rich/dev/clojure/build.xml\") -> {:tag :project, :attrs {:name \"clojure\", :default \"jar\"}, :content [{:tag :description, ...
Calling Clojure From Java The clojure.java.api package provides a minimal interface to bootstrap Clojure access from other JVM languages. It does this by providing:
- The ability to use Clojure's namespaces to locate an arbitrary var, returning the var's clojure.lang.IFn interface.
- A convenience method read for reading data using Clojure's edn reader
IFns provide complete access to Clojure's APIs. You can also access any other library written in Clojure, after adding either its source or compiled form to the classpath.
The public Java API for Clojure consists of the following classes and interfaces:
clojure.java.api.Clojure\nclojure.lang.IFn\n
All other Java classes should be treated as implementation details, and applications should avoid relying on them.
To lookup and call a Clojure function:
IFn plus = Clojure.var(\"clojure.core\", \"+\"); plus.invoke(1, 2);
Functions in clojure.core are automatically loaded. Other namespaces can be loaded via require:
IFn require = Clojure.var(\"clojure.core\", \"require\"); require.invoke(Clojure.read(\"clojure.set\"));
IFns can be passed to higher order functions, e.g. the example below passes plus to read:
IFn map = Clojure.var(\"clojure.core\", \"map\"); IFn inc = Clojure.var(\"clojure.core\", \"inc\"); map.invoke(inc, Clojure.read(\"[1 2 3]\"));
Most IFns in Clojure refer to functions. A few, however, refer to non-function data values. To access these, use deref instead of fn:
IFn printLength = Clojure.var(\"clojure.core\", \"print-length\"); IFn deref = Clojure.var(\"clojure.core\", \"deref\"); deref.invoke(printLength);
"},{"location":"reference/clojure-syntax/naming/","title":"Naming","text":""},{"location":"reference/clojure-syntax/naming/#naming-when-requiring-other-namespaces","title":"Naming when requiring other namespaces","text":"(require [cheshire.core :refer :all])
is an example of self-inflicted errors, as this library included a contains?
function that will over-write the clojure.core/contains?
function when using :refer :all
or the (use )
expression.
This situation is one example of why :refer :all
and use
are not recommended and can cause lots of debugging headaches.
If a namespace is predominantly about using a specific library, then refer specific functions as they are used within the current namespace
(ns current.namespace\n(:require\n [cheshire.core :refer [function-name another-function etc]))\n
A classic example is a test namespace that uses clojure core (ns practicalli.random-function-test (:require [clojure.test :refer [deftest is testing]] [practicalli.random-function :as sut])) Otherwise use a meaningful alias, ideally referring to what that library is doing (which makes it easer to swap out with a different library later on if required). As Cheshire is a JSON related library, then (ns my.ns (:require [cheshire.core :as json])) This gives a context to all the functions called from that library and makes code easier for humans to understand.
"},{"location":"reference/clojure-syntax/naming/#hintclj-kondo-lint-tool-shows-unused-functions","title":"Hint::clj-kondo lint tool shows unused functions","text":"Using clj-kondo
"},{"location":"reference/clojure-syntax/numbers-maths/","title":"Maths","text":"Fixme Split this into sections ?
Writing some simple mathematics helps you get used to the form of Clojure. Unlike other languages, Clojure does not have operators for mathematics. Instead + - * /
are all functions in their own right.
As Clojure uses pre-fix notation then mathematical expressions are always unambiguous. There is no need for an operator precedence table in Clojure.
Note Write some simple math to help you get used to the form of Clojure
(+ 1 2 3 4 5 6 7)\n(- 2 1)\n(* 3 7)\n(/ 12 4)\n(/ 500 20)\n(+ 1 1 2489 459 2.)\n(+ 1 2 (* 3 4) (- 5 6 -7))\n
"},{"location":"reference/clojure-syntax/numbers-maths/#variable-numbers-of-arguments","title":"Variable numbers of arguments","text":"Mathematic functions show the flexibility of Clojure, as they take a variable number of arguments (variadic functions). Its common for Clojure functions to have zero, one or many arguments (many arguments typically represented as a built-in data structure (map, vector, set or list)
Note Write some more maths to show the variadic nature of mathematic (and manu other) functions
(+)\n(*)\n(* 2)\n(+ 4)\n\n(+ 1 2 3)\n(< 1 2 3)\n(< 1 3 8 4)\n
Note Explore some number related functions
(rem 22 7)\n(mod 20 12)\n(quot 13 4)\n\n(inc 3)\n(dec 4)\n\n(min 1 2 3 5 8 13)\n(max 1 2 3 5 8 13)\n\n(repeat 4 9)\n\n(range 10)\n(range 18 66)\n(range 2 99 2)\n
"},{"location":"reference/clojure-syntax/numbers-maths/#equality","title":"Equality","text":"Equality is represented by the =
function. Yes, =
is a proper function too, not just an operator as with other languages.
Note Explore what equality means in Clojure. Equality is very useful when your data structures are immutable
(= 1 1)\n(= 2 1)\n\n(identical? \"foo\" \"bar\")\n(identical? \"foo\" \"foo\")\n(= \"foo\" \"bar\")\n(= \"foo\" \"foo\")\n\n(identical? :foo :bar)\n(identical? :foo :foo)\n\n(true)\n(false)\n(not true)\n(true? (= 1 1))\n(false (= 1 -1))\n
Equality is very efficient when your data structures are immutable. For example if you have very large data sets, you can simply compare a hash value to see if those data structures are the same.
Of course you also have the not
function for reversing logic too
(not true)\n\n=> false\n
"},{"location":"reference/clojure-syntax/numbers-maths/#boolean-true-and-false","title":"Boolean - True and False","text":";; some truthiness with math functions for you to try
(+)\n(class (+))\n(*)\n(true? +)\n(false? +)\n(true? *)\n(false? *)\n(true? 1)\n(true? -1)\n(true? true)\n(- 2)\n
"},{"location":"reference/clojure-syntax/numbers-maths/#boolean-predicates","title":"Boolean & Predicates","text":"Predicates are functions that take a value and return a boolean result (true | false)
(true? true)\n(true? (not true))\n(true? false)\n(true? (not false))\n(true? nil)\n
"},{"location":"reference/clojure-syntax/numbers-maths/#types","title":"Types","text":"Clojure uses Java's object types for booleans, strings and numbers. Use the class
function to inspect them.
(class 1)\n; Integer literals are java.lang.Long by default\n(class 1.1) ; Float literals are java.lang.Double\n\n(class \"\")\n; Strings always double-quoted, and are java.lang.String\n\n(class false) ; Booleans are java.lang.Boolean\n(class nil) ; The \"null\" value is called nil\n\n(class (list 1 2 3 4))\n\n\n(class true)\n(class ())\n(class (list 1 2 34 5))\n(class (str 2 3 4 5))\n(class (+ 22/7))\n(class 5)\n(class \"fish\")\n(type [1 2 3])\n(type {:a 1 :b 2})\n\n(type (take 3 (range 10)))\n
"},{"location":"reference/clojure-syntax/numbers-maths/#ratios","title":"Ratios","text":"To help maintain the precision of numbers, Clojure has a type called Ratio. So when you are dividing numbers you can keep the as a fraction using whole numbers, rather than constrain the result to a approximate
(/ 2)\n
A classic example is dividing 22 by 7 which is approximately the value of Pi
(/ 22 7)\n\n(class (/ 22 7))\n
If you want to force Clojure to evaluate this then you can specify one of the numbers with a decimal point
(class (/ 22 7.0))\n
"},{"location":"reference/clojure-syntax/parenthesis/","title":"Parenthesis - defining the structure of Clojure code","text":"Clojure uses parenthesis, round brackets ()
, as a simple way to define the structure of the code and provide clear and unambiguous scope. This structure is the syntax of symbolic expressions.
Parenthesis, or parens for short, are used to define and call functions in our code, include libraries and in fact any behavior we wish to express.
Clojure includes 3 other bracket types to specifically identify data: '()
quoted lists and sequences, []
for vectors (arrays) and argument lists, {}
for hash-maps and #{}
for sets of data.
No other terminators or precedence rules are required to understand how to read and write Clojure.
"},{"location":"reference/clojure-syntax/parenthesis/#the-parenthesis-hangup","title":"The Parenthesis hangup","text":"Some raise the concern that there are \"too many brackets\" in Clojure.
Clojure doesn't require any additional parens compared to other languages, it simply moves the open parens to the start of the expression giving a clearly defined structure to the code
With support for higher order functions, functional composition and threading macros, Clojure code typically uses fewer parens than other languages especially as the scope of the problem space grows.
All languages use parens to wrap a part of an expression, requiring additional syntax to identify the boundaries of each expression so it can be parsed by humans and computers alike. Clojure uses a single way to express everything, homoiconicity, where as most other languages require additional syntax for different parts of the code.
Using parens, Clojure has a well defined structure that provides a clearly defined scope to every part of the code. There is no requirement to remember a long list of ad-hoc precedence rules, e.g. JavaScript operator precedence.
This structure of Clojure code is simple to parse for both humans and computers. it is simple to navigate, simple to avoid breaking the syntax of the language and simple to provide tooling that keeps the syntax of the code correct.
After realising the simplicity that parens bring, you have to wonder why other (non-lisp) languages made their syntax more complex.
"},{"location":"reference/clojure-syntax/parenthesis/#working-with-parens","title":"Working with Parens","text":"Clojure aware editors all support structured editing to manage parens and ensure they remain balanced (same number of open and close parens).
A developer only needs to type the open paren and the editor will automatically add the closing paren.
Parens cannot be deleted unless their content is empty.
Code can be pulled into parens (slurp) or pushed out of parens (barf). Code can be split, joined, wrapped, unwrapped, transposed, convoluted and raised, all without breaking the structure.
- Smartparens for Structural editing - a modern update of ParEdit
- The animated guide to ParEdit
"},{"location":"reference/clojure-syntax/parenthesis/#homoiconicity-and-macros","title":"Homoiconicity and Macros","text":"Clojure is a dialect of LISP and naturally was designed to be a homoiconic language. This means the syntax for behavior and data is the same. This greatly simplifies the syntax of Clojure and all LISP style languages.
The Clojure Reader is a parser that reads in data structures as expression, rather than parsing of text required by other languages. The result of parsing is a collection of data structures that can be traversed (asymmetric syntax tree - AST). Compared to most languages the compiler does very little and you can consider Clojure really does not have a syntax.
Code is written as data structures that are accessible to the other parts of the code, providing a way to write code that manipulate those data structures and generate new code. In Clojure this type of code is called a macro, a piece of code that writes new code.
None of this would work as simply as it does without using parens and the symbolic expression syntax.
- Inspired by Beating the Averages by Paul Graham
"},{"location":"reference/clojure-syntax/parenthesis/#example-function-invocation","title":"Example: Function invocation","text":"The choice was made early in the design of Lisp that lists would be used for function invocation in the form:
(function arg1 arg2 arg3)\n;; => value returned\n
The advantages of this design are:
- a function call is just one expression, called a \"form\"
- function calls can be constructed (cons function-symbol list-of-args)
- functions can be arguments to other functions (higher order functions)
- simple syntax to parse - everything between two parentheses is a self-contained expression.
- fewer parens due to high order functions, composition and threading macros
The function name could have been put outside the parentheses:
function (arg1 arg2 arg3) => some result\n
This design has many disadvantages:
- a function call is no longer a single form and have to pass the function name and the argument list.
- syntax is complex and requires additional syntax rules to define a function call
- code generation is very complex
- same number of parens as Clojure or possibly more (no direct support for higher order functions)
"},{"location":"reference/clojure-syntax/private-functions/","title":"Private functions","text":"Fixme work in progress
"},{"location":"reference/clojure-syntax/quick-look-at-types/","title":"A quick look at types","text":"As we mentioned before, underneath Clojure lurks Java byte code so there are going to be types in Clojure. However, Clojure being a dynamic language, most of the time you can just let Clojure manage the types for you.
Hint When you run Clojure on a different host platform, eg. .Net or Javascript (via Clojurescript), Clojure will use the types of that host platform.
Should you want to know the type of something you are working on, you can use two functions, type
and class
.
Note Discover the class or type of some common Clojure code
(class 1)\n(class 1.1)\n(class \"\")\n(class true)\n(class false)\n(class nil)\n\n(class ())\n(class (list 1 2 3 4))\n(class (str 2 3 4 5))\n(class (+ 22/7))\n\n(type [1 2 3])\n(type {:a 1 :b 2})\n(type (take 3 (range 10)))\n
Hint If you cant live without static type checking, look at core.typed, a type system for Clojure all in one library
"},{"location":"reference/clojure-syntax/ratios/","title":"Ratios","text":"In mathematics you need to ensure that you manage precision of your calculations when you are dividing numbers. Once you create a decimal number then everything it touches had a greater potential to becoming a decimal.
Note Calculate a rough approximation to Pi by dividing 22 by 7
(/ 22 7)\n(class (/ 22 7))\n(/ (* 22/7 3) 3)\n
If the result of an integer calculation would be a decimal number, then Clojure holds the value as a Ratio. This is one example of lazy evaluation. Rather than calculate the decimal value at some particular precision (number of decimal points). Clojure is saving the calculation until its needed, at which time the specific precision required should be known.
Note Explore the ratio type further and see how to get a decimal value as the result
(/ 14 4)\n(/ 16 12)\n(/ 2)\n(/ 22 7.0)\n(type (/ 22 7.0))\n(float (/ 22 7))\n(double (/ 22 7))\n
When one or more of the numbers in the division is a decimal, then Clojure will return a decimal value. Or you can coerce a value to a specific decimal type, eg. float or double.
"},{"location":"reference/clojure-syntax/strings/","title":"Strings","text":"Strings in Clojure are actually Java Strings.
Hint Why do you think this design decision was taken for Clojure?
If you think about the state property of String objects, then you realise that String Objects are immutable and cannot be changed. As this is the default approach for other data structures and values in Clojure it makes sense to use Java Strings instead of writing a Clojure implementation.
As Clojure strings are Java strings, then you can use all the same functions you can in Java.
Note Use the Java function println
to output a string
(println \"Hello, whats different with me? What value do I return\")\n
Something different happens when you evaluate this expression. The actual value returned is nil
, not the string. You see the string because println is writing to the console (i.e the REPL).
Hint Avoid code that creates side-effects where possible to keep your software less complex to understand.
You may be used to using println statements to help you debug your code, however, with the fast feedback you get from developing in the REPL then there is usually no need for them.
"},{"location":"reference/clojure-syntax/strings/#strings-the-clojure-way","title":"Strings the Clojure way","text":"Its more common to use the str
function when working with strings, as this function returns the string as its. value when evaluated.
(str \"Hello, I am returned as a value of this expression\")
Note Join strings together with the function str
(str \"I\" \"like\" \"to\" \"be\" \"close\" \"together\"\n(str \"Hello\" \", \" \"Devoxx UK\")\n(str \"Hello \" \"developers\" \", \" \"welcome\" \" \" \"to\" \" \" \"HackTheTower UK\")\n
You can see that there are no side-effects when using str
and the string is returned as the value of the function call.
"},{"location":"reference/clojure-syntax/strings/#using-interpose-with-strings","title":"Using Interpose with Strings","text":"Its easy to join strings together with the str
function, however str
leaves no spaces between words.
"},{"location":"reference/clojure-syntax/strings/#using-regex","title":"Using Regex","text":""},{"location":"reference/clojure-syntax/strings/#java-interop-for-strings","title":"Java Interop for Strings","text":"Note Change the case of strings and other common actions using the String object methods, in the form (.methodName object)
(.toUpperCase \"show me the money\")\n\n(.getName String)\n\n(.indexOf \"Where is the $ in this string\" \"$\")\n
Hint Look at the API docs for java.lang.String for other methods you can call.
"},{"location":"reference/clojure-syntax/syntax/","title":"Clojure syntax","text":"Clojure is perceived as having an abundance of ()
, the symbols that represent a list.
As Clojure is a LISP (List Processing) language then everything is written in the form of a list. This makes Clojure very powerful and also easier to read.
Using a list structure also demonstrates the data-centric nature of Clojure. Every item in the list has a value, with the first item evaluated by a function call.
"},{"location":"reference/clojure-syntax/syntax/#hintparens-everywhere","title":"Hint::Parens everywhere","text":"The seemingly abundance of ()
can be confusing until its realized there are fewer \"special characters\" in Clojure than other languages. Clojure aware editors support matching parens, adding a closed paren when typing an open paren, ensuring it is easy to write correctly formed Clojure.
Syntax differences are a trivial reason to avoid trying Clojure. Syntax aware editors significantly reduce typing by automatically closing parenthesis and eliminating errors due to missing delimiters (ie. no more errors due to missing ; in C-based languages)
"},{"location":"reference/clojure-syntax/syntax/#prefix-notation","title":"Prefix notation","text":"Instead of having a mix of notations like in many other languages, Clojure uses pre-fix notation entirely.
In Clojure operators are applied uniformly and there is no room for ambiguity:
(+ 1 2 3 5 8 13 21)\n (+ 1 2 (- 4 1) 5 (* 2 4) 13 (/ 42 2))\n (str \"Clojure\" \" uses \" \"prefix notation\")\n
In Java and other C-based languages you have to explicitly add operators everywhere and there can be a mixture of notations
(1 + 2 + 3 + 5 + 8 + 13 + 21);\n (1 + 2 + (- 4 1) + 5 + (* 2 4) + 13 + (/ 42 2));\n StringBuffer description = new StringBuffer(\"C-based languages\" + \" mix \" + \"notation\");\n x+=1;\n x++;\n x--;\n x+=y;\n x-=y;\n x*=y;\n x/=y;\n
"},{"location":"reference/clojure-syntax/syntax/#references","title":"References","text":""},{"location":"reference/clojure-syntax/threading-syntactic-sugar/","title":"Threading Syntax Sugar","text":"The previous code is written in classic Lisp style. When you come to read Lisp, you start from the inside out. In this case you start with (slurp ...)
and what it returns is used as the argument to (read-string ...)
and so on...
In our minds we probably constructed the following basic algorithm:
- Get the contents of the project.clj file using
slurp
- Read the text of that file using read-string
- Select just the third string using nth 2 (using an index starting at 0)
Can we rewrite our Clojure code to fit the way we think?
"},{"location":"reference/clojure-syntax/threading-syntactic-sugar/#thread-first-macro","title":"Thread first macro","text":"Using the thread-first macro -> we can chain Clojure functions together with a terser syntax, passing the result of the first evaluation as the first argument to the next function and so on. Using this style, we can write code that matches the algorithm in our head.
To make this really simple lets create a contrived example of the threading macro. Here we use the str
function to join strings together. Each individual str
function joins its own strings together, passing the resulting string as the first argument to the next function.
(->\n (str \"This\" \" \" \"is\" \" \")\n (str \"the\" \" \" \"threading\" \" \" \"macro\")\n (str \"in\" \" \" \"action.\"))\n\n;; => \"This is the threading macro in action\"\n
"},{"location":"reference/clojure-syntax/threading-syntactic-sugar/#thread-last-macro","title":"Thread-last macro","text":"Using the thread-last macro, ->>, the result of a function is passed as the last argument of the next function call. So in another simple series of str function calls, our text comes out backwards.
(->>\n (str \" This\")\n (str \" is\")\n (str \" backwards\"))\n\n;; => backwards is This\"\n
"},{"location":"reference/clojure-syntax/threading-syntactic-sugar/#_1","title":"Threading Syntax Sugar","text":"Note Refactor the Clojure code using the thread-first macro
(->\n \"./project.clj\"\n slurp\n read-string\n (nth 2))\n
Hint The \"project.clj\" is a string, so when you evaluate it as an expression, it simply returns the same string. That string is then passed as an argument to any following functions.
Using the threading macro, the result of every function is passed onto the next function in the list. This can be seen very clearly using ,,, to denote where the value is passed to the next function
(->\n \"project.clj\"\n slurp ,,,\n read-string ,,,\n (nth ,,, 2))\n
Hint Commas in clojure are treated as whitespace, they are simply ignored when it comes to evaluating code. Typically commas are rarely used and only to help human readability of the code
Note Create a new map that contains the project configuration for the current project
(->> \"project.clj\"\n slurp\n read-string\n (drop 2)\n (cons :version)\n (apply hash-map)\n (def project-configs))\n\n;; Evaluate the new map defined as project\nproject\n
We pull out the map of project information using slurp
, tidy the text up using read-string
and drop the first two elements (defproject playground). This returns a list that we want to turn into a map, but first we need to add a key to the version number. Using the cons
function we can add an element to the start of the list, in this case the :version
keyword
Now we can successfully convert the list that is returned into a map, with balanced key-value pairs. Then we simply create a name for this new map, project-configs
, so we can refer to it elsewhere in the code.
Hint The slurp
function holds the contents of the whole file in memory, so it may not be appropriate for very large files. If you are dealing with a large file, consider wrapping slurp in a lazy evaluation or use Java IO (eg. java.io.BufferedReader
, java.io.FileReader.
). See the Clojure I/O cookbook and The Ins & Outs of Clojure for examples.
"},{"location":"reference/clojure-syntax/whats-my-environment/","title":"Whats my environment","text":"Clojure has symbols (names that point to values). Some of these symbols are built into the language and their names start (and usually end with) the *
character.
When symbols are evaluated they return the value that they point to.
Note Check the version of Clojure running in your REPL.
Enter the following code into the Clojure REPL:
*clojure-version*\n
The full clojure version can be used to check you are running a particular version, major or minor of Clojure core. This information is represented as a map containing :major
, :minor
, :incremental
and :qualifier
keys. Feature releases may increment :minor and/or :major, bugfix releases will increment :incremental. Possible values of :qualifier include \"GA\", \"SNAPSHOT\", \"RC-x\" \"BETA-x\"
Hint A map in Clojure is a built in data structure represented by { }
. A map is a key-value pair and there must be a value for every key for the map to be valid. Keys are often defined using :keyword
, a self-referential pointer that can be used to look up values in a map or other data structures.
"},{"location":"reference/clojure-syntax/whats-my-environment/#viewing-the-class-path","title":"Viewing the Class path","text":"Clojure compiles to Java bytecode that runs on the JVM, so that code needs to be available in the Java class path.
Note Look at the class path for your project
The directory where the Clojure compiler will create the .class files for the current project. All .class files must be on the class path otherwise the Clojure run time environment will not know they exist.
*compile-path*\n
"},{"location":"reference/clojure-syntax/whats-my-environment/#namespace","title":"Namespace","text":"A namespace in clojure is a way to separate functions and data structures into logical components (similar to Java packages). A clojure.lang.Namespace
object representing the current namespace.
Note Find out the current namespace
*ns*\n
"},{"location":"reference/clojure-syntax/whats-my-environment/#last-3-values-in-the-repl","title":"Last 3 values in the REPL","text":"You can also get the 3 most most recent values returned in the REPL.
Note Evaluate the following three expressions in the REPL, then pull out the last three results
(+ 1 2 3)\n\n(+ 1 2 (+ 1 2) (+ 2 3))\n\n(str \"Java will be fully functional in version \" (+ 10 (rand-int 20))\n
Now get the last three values returned in the REPL
(str *1 *2 *3)\n
Hint You can cycle through previous expressions entered into the REPL using the Shift-UpArrow
keyboard shortcut
"},{"location":"reference/clojure-syntax/syntax/","title":"Clojure Syntax","text":"The Clojure syntax is very small and is actually a data structure, defined as a list, ()
, with the first element of a list being a function call and all other elements arguments to that function.
Examples are editable (using an embedded REPL) so feel free to experiment and watch as the return value changes as you change the code. Reload the page if you want to reset all the code back to the starting point.
"},{"location":"reference/clojure-syntax/syntax/#edn-based-notation","title":"edn based notation","text":"The core Clojure syntax is defined in the extensible data notation (edn). edn demonstrates that Clojure code is defined as a series of data structures
Clojure adds an execution model on top of edn to make a programming language and is a super-set of edn.
edn is used as a data transfer format, especially for Datomic the Clojure transactional database
- A case for Clojure by James Reeves provides a great introduction to edn
"},{"location":"reference/clojure-syntax/syntax/#calling-functions","title":"Calling functions","text":"The first element in a list, ()
, is treated as a call to a function. The examples show how to call functions with multiple arguments.
(+ 1 2)\n
(+ 3 (* 2 (- 7 2) 4) (/ 16 4))\n
(str \"Clojure is \" (- 2020 2007) \" years old\")\n
(inc 1)\n
(map inc [1 2 3 4 5])\n
(filter odd? (range 11))\n
"},{"location":"reference/clojure-syntax/syntax/#hintprefix-notation-and-parens","title":"Hint::Prefix notation and parens","text":"Hugging code with ()
is a simple syntax to define the scope of code expressions. No additional ;
, ,
or spaces are required.
Treating the first element of a list as a function call is referred to as prefix notation, which greatly simplifies Clojure syntax. Prefix notation makes mathematical expressions completely deterministic, eliminating the need for operator precedence.
"},{"location":"reference/clojure-syntax/syntax/#understanding-functions","title":"Understanding functions","text":"Functions contain doc-strings describing what that function does. The doc
function returns the doc-string of a particular function. Most editors also support viewing of doc-strings as well as jumping to function definitions to view the source code
(doc doc)\n
"},{"location":"reference/clojure-syntax/syntax/#strongly-typed-under-the-covers","title":"Strongly typed under the covers","text":"Clojure is a dynamically typed language so types do not need to be explicitly defined, although type hints can be added for performance where required.
Clojure is strongly typed and everything is a type underneath, relative to the host platform (Clojure uses Java types, ClojureScript uses JavaScript types). The type of anything in Clojure can be returned using the type
function.
(type 42)\n;; (type {:hash \"data\" :map \"more data\"})\n
(type {:hash \"data\" :map \"more data\"})\n
"},{"location":"reference/clojure-syntax/syntax/#modeling-data-with-collection-types","title":"Modeling data with Collection types","text":"Clojure has 4 main collection types, all immutable (cannot change once created) and can contain any Clojure types.
(str \"lists used mainly \" (* 2 2) \" \" :code)\n
[0 \"indexed\" :array (* 2 2) \"random-access\"]\n
{:key \"value\" \"hash-map\" \"also referred to as dictionary\"}\n
#{1 2 3 4 \"unique\" \"set\" \"of\" \"values\" \"unordered\" (* 3 9)}\n
"},{"location":"reference/clojure-syntax/syntax/#hintpersistent-data-types","title":"Hint::Persistent data types","text":"To change data in Clojure new copies are created rather than changing existing values. The copies of data will share values from the original data that are common in both. This sharing is called persistent data types and enables immutable data to be used efficiently.
"},{"location":"reference/clojure-syntax/syntax/#defining-names-for-values-vars","title":"Defining names for values (vars)","text":"Names can be bound to any values, from simple values like numbers, collections or even function calls. Using def
is convenient way to create names for values that are shared in your code.
evaluating a name will return the value it is bound to.
(def public-health-data\n ({:date \"2020-01-01\" :confirmed-cases 23014 :recovery-percent 15}\n {:date \"2020-01-02\" :confirmed-cases 23014 :recovery-percent 15}\n {:date \"2020-01-03\" :confirmed-cases 23014 :recovery-percent 15}))\n\npublic-health-data\n
"},{"location":"reference/clojure-syntax/syntax/#hintdef-for-shared-values-let-for-locally-scoped-values","title":"Hint::def for shared values, let for locally scoped values","text":"let
function is used to bind names to values locally, such as within a function definition. Names bound with def
have namespace scope so can be used with any code in that namespace.
"},{"location":"reference/clojure-syntax/syntax/#using-data-structures","title":"Using data structures","text":"Using the map
and inc
function, increment all the numbers in a vector
(map inc [1 2 3 4 5])\n
The above map
function is roughly equivalent to the following expression
(conj [] (inc 1) (inc 2) (inc 3) (inc 4) (inc 5))\n
The conj
function creates a new collection by combining a collection and one or more values.
map
reduce
filter
are common functions for iterating through a collection / sequence of values
(map * [1 3 5 8 13 21] [3 5 8 13 21 34])\n
(filter even? [1 3 5 8 13 21 34])\n
(reduce + [31 28 30 31 30 31])\n
(empty? [])\n
"},{"location":"reference/clojure-syntax/syntax/#defining-custom-functions","title":"Defining custom functions","text":"(defn square-of\n \"Calculates the square of a given number\"\n [number]\n (* number number))\n\n(square-of 9)\n
Function definitions can also be used within other expressions, useful for mapping custom functions over a collection
(map (fn [x] (* x x)) [1 2 3 4 5])\n
"},{"location":"reference/clojure-syntax/syntax/#host-interoperability","title":"Host Interoperability","text":"The REPL in this web page is running inside a JavaScript engine, so JavaScript functions can be used from within ClojureScript code (ClojureScript is Clojure that runs in JavaScript environments).
In the box below, replace ()
with (js/alert \"I am a pop-up alert\")
()\n
JavaScript libraries can be used with ClojureScript, such as React.js
(defn concentric-circles []\n [:svg {:style {:border \"1px solid\"\n :background \"white\"\n :width \"150px\"\n :height \"150px\"}}\n [:circle {:r 50, :cx 75, :cy 75, :fill \"green\"}]\n [:circle {:r 25, :cx 75, :cy 75, :fill \"blue\"}]\n [:path {:stroke-width 12\n :stroke \"white\"\n :fill \"none\"\n :d \"M 30,40 C 100,40 50,110 120,110\"}]\n [:path {:stroke-width 12\n :stroke \"white\"\n :fill \"none\"\n :d \"M 75,75 C 50,90 50,110 35,110\"}]])\n
"},{"location":"reference/jvm/","title":"Reference: Java Virtual Machine","text":"Understand the configuration options for the Java Virtual machine (JVM) which Clojure is hosted upon.
Overview of tools for monitoring and profiling Clojure applications running on the JVM, to ensure effective running of Clojure applications in production.
- Common JVM Options - for development and deployment
- JVM Profiling tools - understand resources and help diagnose run-time problems
JDK_JAVA_OPTIONS
Environment Variable
JDK_JAVA_OPTIONS
is the official Environment Variable for setting options when calling java
, javac
and other Java commands to start running a Java Virtual Machine (Java version 9 onward).
"},{"location":"reference/jvm/#display-resources-available-to-the-jvm","title":"Display resources available to the JVM","text":"-XshowSettings:system
displays the resources the JVM believes it has access too when running any Java command and is a very simple diagnostic tool to start with.
See the environment resources available to the JVM without running a Clojure or Java application:
java -XshowSettings:system -version\n
Include -XshowSettings:system
when running any Java command to provide simple diagnostics, e.g. when running a Clojure Uberjar
java -XshowSettings:system -jar practicalli-service.jar\n
Print resources in Container systems
-XshowSettings:system
is especially useful for environments which may vary in resources available, such as containers (Docker, Kubernettes, etc.)
"},{"location":"reference/jvm/#jvm-option-types","title":"JVM option types","text":"-X
- nonstandard VM options
-XX
standard VM options
-XX
options are not checked for validity, so are ignored if the VM does not recognize the option. Options can therefore be used across different VM versions without ensuring a particular level of the VM.
-D
a system property for the application running on the JVM using a name=value
"},{"location":"reference/jvm/#java-modules","title":"Java Modules","text":"Java 9 introduced modules to move features out of JVM itself and include them as optional modules.
Before CLJS-2377 issue was resolved, ClojureScript (2017) depended on java.xml.bind.DataTypeConverter
. java.xml.bind package
was deprecated in Java 9 and moved to a non-default module.
At that time, compiling a ClojureScript project without adding the java.xml.bind module would return the error:
<Exception details>\n...\nCaused by: java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverter\n
clojure J--add-modules \"java.xml.bind\"
command line option will include the module
:jvm-opts [\"--add-modules\" \"java.xml.bind\"]
added to Clojure CLI deps.edn or Leiningen project.clj file will include the module.
-Djdk.launcher.addmods=java.xml.bind
added to the JAVA_TOOL_OPTIONS
environment variable (jdk.launcher.addmods
--add-modules
doesn\u2019t work in JAVA_TOOL_OPTIONS
)
"},{"location":"reference/jvm/#unified-logging-sub-system","title":"Unified Logging sub-system","text":"-Xlog
- JEP 158
"},{"location":"reference/jvm/#references","title":"References","text":" - Best practice for JVM Tuning with G1 GC
- Command Line Options - IBM SDK documentation
- Best HotSpot JVM Options and switches for Java 11 through Java 17
"},{"location":"reference/jvm/common-options/","title":"Common JVM Options","text":"Examples of commonly used options for any language on the Java Virtual Machine (JVM).
The JVM is excellent at self-optimising its performance. Introducing specific options should only be done if specific resource or performance issues have been identified.
Understanding memory usage has more options to diagnose out of memory errors, garbage collection pauses and JIT compilation
JDK_JAVA_OPTIONS
Environment Variable
JDK_JAVA_OPTIONS
is the official Environment Variable for setting options when calling java
, javac
and other Java commands to start running a Java Virtual Machine (Java version 9 onward).
"},{"location":"reference/jvm/common-options/#java-heap-size","title":"Java heap size","text":"Java Ergonomics should provide sensible default options. Performance analysis of the running code may show advantages of manually setting memory sizes.
Set the initial heap size if memory usage will quickly grow
-Xms
\u2013 start heap size for JVM, e.g. -Xms2048m
sets an initial heap size of 2 GB
-XX:InitialRAMPercentage=n
sets the initial heap as n
percentage of total RAM
Set the maximum heap size if usage is relatively high under normal conditions
-Xmx
\u2013 maximum heap size of JVM, e.g. -Xmx2048m
-XX:MaxRAMPercentage=n
sets the maximum heap as n
percentage of total RAM
-Xss
- set java thread stack size
-Xms
and -Xmx
are commonly used together (where there is a know fixed value for memory resources).
"},{"location":"reference/jvm/common-options/#heap-and-garbage-collection","title":"Heap and garbage collection","text":"-XX:MaxHeapFreeRatio
\u2013 maximum percentage of heap free after garbage collection to avoid shrinking.
-XX:MinHeapFreeRatio
\u2013 minimum percentage of heap free after GC to avoid expansion
VisualVM or JConsole can monitor the heap usage
"},{"location":"reference/jvm/common-options/#container-environments","title":"Container Environments","text":"-XX:InitialRAMPercentage
and -XX:MaxRAMPercentage
options should be used to set relative limits to the resources available from the host.
Setting specific heap sizes with -Xms
and -Xmx
is strongly discouraged in Container environments, as resources available to the container from the host could change between deployments (e.g. a change in operational configuration in Kubernettes, etc.)
"},{"location":"reference/jvm/common-options/#stack-traces","title":"Stack traces","text":"-XX:-OmitStackTraceInFastThrow
no StackTrace for implicit exceptions thrown by JVM, e.g. NullPointerException, ArithmeticException, ArrayIndexOutOfBoundsException, ArrayStoreException or ClassCastException.
"},{"location":"reference/jvm/common-options/#reflection","title":"Reflection","text":"--illegal-access
option controls how deep reflection warnings are handled.
- permit (default) - generates warning only when the first illegal access was detected
- warn - emit warning after each illegal access detection
- debug - add stack trace to warning
- deny - like debug for the first detection, then killing the program.
Java 16 deprecates --illegal-access
flag, via work done for JEP403 - may still be useful for 3rd party Java libraries.
"},{"location":"reference/jvm/common-options/#enable-class-data-sharing","title":"Enable class data sharing","text":"-Xshareclasses
enables class data sharing in a shared class cache.
The JVM connects to an existing cache (creating a cache if none exist). Multiple caches specified by adding a sub-option to the -Xshareclasses
option.
"},{"location":"reference/jvm/common-options/#handling-outofmemory-error","title":"Handling \u2018OutOfMemory\u2019 Error","text":"Generating a Heap Dump for out of memory (OOM) issues is recommended for production systems, to provide data for a deep analysis of the problem. Generating a heap dump does not add overhead to the running JVM.
-XX:+HeapDumpOnOutOfMemoryError
- trigger heap dump on out of memory failure
-XX:HeapDumpPath=path-to-heap-dump-directory
- sets path to write the heap dump file (defaults to directory in which java command was ran from)
A heap dump file can gigabytes in size, so assure that the target file system has sufficient capacity.
-XX:OnOutOfMemoryError=\"shutdown -r\"
- restart the process immediately after out of memory failure
The option can take multiple commands, separated by a ;
, e.g. -XX:OnOutOfMemoryError=\"< cmd args >;< cmd args >\"
"},{"location":"reference/jvm/common-options/#trace-classloading-and-unloading","title":"Trace classloading and unloading","text":"Identify memory leaks suspected from the JVM Class Loader, e.g. classes are not unloading or garbage collected
-XX:+TraceClassLoading
- log classes loaded into the JVM
-XX:+TraceClassUnloading
- log classes unloaded from the JVM
"},{"location":"reference/jvm/common-options/#profiling","title":"Profiling","text":"Profiling JVM processes provides a fine-grained view of application execution and resource utilization. Monitor parameters including Method Executions, Thread Executions, Garbage Collections and Object Creations.
Consider using a profile tool, such as VisualVM
"},{"location":"reference/jvm/common-options/#skip-byte-code-verification","title":"Skip byte code verification","text":"The byte code for each class loaded by the JVM Class Loader is verified, which is a relatively expensive task at startup. Adding classes on the boot classpath skips the cost of the verification, although also introduces a security risk so should only be used when classes have been previously verified.
-Xbootclasspath
specifies classpath entries to load without verification
Profiling an application is a more suitable long term solution than skipping byte code verification
Checks carried out by the verifier include
- Uninitialized Variables
- Access rules for private data and methods are not violated
- Method calls match the object Reference
- There are no operand stack overflows or under-flows
- Arguments to all JVM instructions are valid types
- Final classes are not subclassed and final methods are not overridden
- field and method references have valid names, valid classes and valid type descriptor
"},{"location":"reference/jvm/common-options/#print-gc","title":"Print GC","text":"Enable the garbage collection logging to capture detailed statistics, e.g. type of garbage collector, how often memory is restored and how much time memory was held for. Garbage collection can last several milliseconds, so logging is useful for latency-sensitive processes.
-verbose:gc
- logs garbage collector runs and how long they're taking. -XX:+PrintGCDetails
- includes the data from -verbose:gc but also adds information about the size of the new generation and more accurate timings. -XX:-PrintGCTimeStamps
- Print timestamps at garbage collection.
Consider using LMAX disruptor for a Garbage Collection free architecture for ultra latency-sensitive applications
"},{"location":"reference/jvm/common-options/#deprecated-permgen-size","title":"Deprecated: PermGen Size","text":"-XX:PermSize
- size of PermGen space where string pool and class metadata is saved.
Option is useful for web servers which load classes of a web application during deployment (e.g. deploying a jar or war to Tomcat).
Metaspace has taken over PermGen space in Java 8 onward
"},{"location":"reference/jvm/experimental-options/","title":"Reference: JVM Experimental Options","text":"The HotSpot JVM provides the opportunity to try features that may appear in future release, although are currently not production-ready.
HotSpot JVM experimental features need to be unlocked by specifying the -XX:+UnlockExperimentalVMOptions
option.
For example, the ZGC garbage collector in JDK 11 can be accessed using
java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC\n
The ZGC collector became a product option in JDK 15, so is no longer experimental.
"},{"location":"reference/jvm/experimental-options/#manageable","title":"Manageable","text":"Show locks held by java.util.concurrent
classes in a HotSpot JVM thread dump:
java -XX:+UnlockExperimentalVMOptions -XX:+PrintConcurrentLocks\n
These options can be set at runtime via the MXBean API or related JDK tools
"},{"location":"reference/jvm/experimental-options/#diagnostic","title":"Diagnostic","text":"Accessing advanced diagnostic information about the HotSpot JVM.
These options require you to use the -XX:+UnlockDiagnosticVMOptions
option before they can be used.
View advance compilation optimisations using the -XX:+LogCompilation
option:
java -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation\n
The HotSpot JVM outputs a log file containing details of all the optimisations made by the JIT compilers. Inspect the output to understand which parts of your program were optimized and to identify parts of the program that might not have been optimized as expected.
The LogCompilation output is verbose but can be visualized in a tool such as JITWatch, which can tell you about method inlining, escape analysis, lock elision, and other optimizations that the HotSpot JVM made to your running code.
"},{"location":"reference/jvm/java-17-flags/","title":"Reference: Java 17 JVM flags","text":"A complete list of all flags available for the JVM, created using the -XX:+PrintFlagsFinal
option and the results written to a file
java -XX:+PrintFlagsFinal > java-flags.md\n
Find specific flags by using grep on the output with a name
java -XX:+PrintFlagsFinal -version | grep MaxHeap\n
Type Name Units ? ? uintx MaxHeapFreeRatio 70 {manageable} {default} size_t MaxHeapSize 8342470656 {product} {ergonomic} size_t SoftMaxHeapSize 8342470656 {manageable} {ergonomic}"},{"location":"reference/jvm/java-17-flags/#full-list-of-jvm-flags","title":"Full list of JVM flags","text":"Type Option Name Default value Product Category int ActiveProcessorCount -1 {product} {default} uintx AdaptiveSizeDecrementScaleFactor 4 {product} {default} uintx AdaptiveSizeMajorGCDecayTimeScale 10 {product} {default} uintx AdaptiveSizePolicyCollectionCostMargin 50 {product} {default} uintx AdaptiveSizePolicyInitializingSteps 20 {product} {default} uintx AdaptiveSizePolicyOutputInterval 0 {product} {default} uintx AdaptiveSizePolicyWeight 10 {product} {default} uintx AdaptiveSizeThroughPutPolicy 0 {product} {default} uintx AdaptiveTimeWeight 25 {product} {default} bool AdjustStackSizeForTLS false {product} {default} bool AggressiveHeap false {product} {default} intx AliasLevel 3 {C2 product} {default} bool AlignVector false {C2 product} {default} ccstr AllocateHeapAt {product} {default} intx AllocateInstancePrefetchLines 1 {product} {default} intx AllocatePrefetchDistance 256 {product} {default} intx AllocatePrefetchInstr 0 {product} {default} intx AllocatePrefetchLines 3 {product} {default} intx AllocatePrefetchStepSize 64 {product} {default} intx AllocatePrefetchStyle 1 {product} {default} bool AllowParallelDefineClass false {product} {default} bool AllowRedefinitionToAddDeleteMethods false {product} {default} bool AllowUserSignalHandlers false {product} {default} bool AllowVectorizeOnDemand true {C2 product} {default} bool AlwaysActAsServerClassMachine false {product} {default} bool AlwaysCompileLoopMethods false {product} {default} bool AlwaysLockClassLoader false {product} {default} bool AlwaysPreTouch false {product} {default} bool AlwaysRestoreFPU false {product} {default} bool AlwaysTenure false {product} {default} ccstr ArchiveClassesAtExit {product} {default} intx ArrayCopyLoadStoreMaxElem 8 {C2 product} {default} size_t AsyncLogBufferSize 2097152 {product} {default} intx AutoBoxCacheMax 128 {C2 product} {default} intx BCEATraceLevel 0 {product} {default} bool BackgroundCompilation true {pd product} {default} size_t BaseFootPrintEstimate 268435456 {product} {default} intx BiasedLockingBulkRebiasThreshold 20 {product} {default} intx BiasedLockingBulkRevokeThreshold 40 {product} {default} intx BiasedLockingDecayTime 25000 {product} {default} intx BiasedLockingStartupDelay 0 {product} {default} bool BlockLayoutByFrequency true {C2 product} {default} intx BlockLayoutMinDiamondPercentage 20 {C2 product} {default} bool BlockLayoutRotateLoops true {C2 product} {default} intx C1InlineStackLimit 5 {C1 product} {default} intx C1MaxInlineLevel 9 {C1 product} {default} intx C1MaxInlineSize 35 {C1 product} {default} intx C1MaxRecursiveInlineLevel 1 {C1 product} {default} intx C1MaxTrivialSize 6 {C1 product} {default} bool C1OptimizeVirtualCallProfiling true {C1 product} {default} bool C1ProfileBranches true {C1 product} {default} bool C1ProfileCalls true {C1 product} {default} bool C1ProfileCheckcasts true {C1 product} {default} bool C1ProfileInlinedCalls true {C1 product} {default} bool C1ProfileVirtualCalls true {C1 product} {default} bool C1UpdateMethodData true {C1 product} {default} intx CICompilerCount 12 {product} {ergonomic} bool CICompilerCountPerCPU true {product} {default} bool CITime false {product} {default} bool CheckJNICalls false {product} {default} bool ClassUnloading true {product} {default} bool ClassUnloadingWithConcurrentMark true {product} {default} bool ClipInlining true {product} {default} uintx CodeCacheExpansionSize 65536 {pd product} {default} bool CompactStrings true {pd product} {default} ccstr CompilationMode default {product} {default} ccstrlist CompileCommand {product} {default} ccstr CompileCommandFile {product} {default} ccstrlist CompileOnly {product} {default} intx CompileThreshold 10000 {pd product} {default} double CompileThresholdScaling 1.000000 {product} {default} intx CompilerThreadPriority -1 {product} {default} intx CompilerThreadStackSize 1024 {pd product} {default} size_t CompressedClassSpaceSize 1073741824 {product} {default} uint ConcGCThreads 3 {product} {ergonomic} intx ConditionalMoveLimit 3 {C2 pd product} {default} intx ContendedPaddingWidth 128 {product} {default} bool CrashOnOutOfMemoryError false {product} {default} bool CreateCoredumpOnCrash true {product} {default} bool CriticalJNINatives false {product} {default} bool DTraceAllocProbes false {product} {default} bool DTraceMethodProbes false {product} {default} bool DTraceMonitorProbes false {product} {default} bool DisableAttachMechanism false {product} {default} bool DisableExplicitGC false {product} {default} bool DisplayVMOutputToStderr false {product} {default} bool DisplayVMOutputToStdout false {product} {default} bool DoEscapeAnalysis true {C2 product} {default} bool DoReserveCopyInSuperWord true {C2 product} {default} bool DontCompileHugeMethods true {product} {default} bool DontYieldALot false {pd product} {default} ccstr DumpLoadedClassList {product} {default} bool DumpReplayDataOnError true {product} {default} bool DumpSharedSpaces false {product} {default} bool DynamicDumpSharedSpaces false {product} {default} bool EagerXrunInit false {product} {default} intx EliminateAllocationArraySizeLimit 64 {C2 product} {default} bool EliminateAllocations true {C2 product} {default} bool EliminateAutoBox true {C2 product} {default} bool EliminateLocks true {C2 product} {default} bool EliminateNestedLocks true {C2 product} {default} bool EnableContended true {product} {default} bool EnableDynamicAgentLoading true {product} {default} size_t ErgoHeapSizeLimit 0 {product} {default} ccstr ErrorFile {product} {default} bool ErrorFileToStderr false {product} {default} bool ErrorFileToStdout false {product} {default} uint64_t ErrorLogTimeout 120 {product} {default} double EscapeAnalysisTimeout 20.000000 {C2 product} {default} bool EstimateArgEscape true {product} {default} bool ExecutingUnitTests false {product} {default} bool ExitOnOutOfMemoryError false {product} {default} bool ExplicitGCInvokesConcurrent false {product} {default} bool ExtendedDTraceProbes false {product} {default} bool ExtensiveErrorReports false {product} {default} ccstr ExtraSharedClassListFile {product} {default} bool FilterSpuriousWakeups true {product} {default} bool FlightRecorder false {product} {default} ccstr FlightRecorderOptions {product} {default} bool ForceTimeHighResolution false {product} {default} intx FreqInlineSize 325 {C2 pd product} {default} double G1ConcMarkStepDurationMillis 10.000000 {product} {default} uintx G1ConcRSHotCardLimit 4 {product} {default} size_t G1ConcRSLogCacheSize 10 {product} {default} size_t G1ConcRefinementGreenZone 0 {product} {default} size_t G1ConcRefinementRedZone 0 {product} {default} uintx G1ConcRefinementServiceIntervalMillis 300 {product} {default} uint G1ConcRefinementThreads 13 {product} {ergonomic} size_t G1ConcRefinementThresholdStep 2 {product} {default} size_t G1ConcRefinementYellowZone 0 {product} {default} uintx G1ConfidencePercent 50 {product} {default} size_t G1HeapRegionSize 4194304 {product} {ergonomic} uintx G1HeapWastePercent 5 {product} {default} uintx G1MixedGCCountTarget 8 {product} {default} uintx G1PeriodicGCInterval 0 {manageable} {default} bool G1PeriodicGCInvokesConcurrent true {product} {default} double G1PeriodicGCSystemLoadThreshold 0.000000 {manageable} {default} intx G1RSetRegionEntries 768 {product} {default} intx G1RSetSparseRegionEntries 32 {product} {default} intx G1RSetUpdatingPauseTimePercent 10 {product} {default} uint G1RefProcDrainInterval 1000 {product} {default} uintx G1ReservePercent 10 {product} {default} uintx G1SATBBufferEnqueueingThresholdPercent 60 {product} {default} size_t G1SATBBufferSize 1024 {product} {default} size_t G1UpdateBufferSize 256 {product} {default} bool G1UseAdaptiveConcRefinement true {product} {default} bool G1UseAdaptiveIHOP true {product} {default} uintx GCDrainStackTargetSize 64 {product} {ergonomic} uintx GCHeapFreeLimit 2 {product} {default} uintx GCLockerEdenExpansionPercent 5 {product} {default} uintx GCPauseIntervalMillis 201 {product} {default} uintx GCTimeLimit 98 {product} {default} uintx GCTimeRatio 12 {product} {default} size_t HeapBaseMinAddress 2147483648 {pd product} {default} bool HeapDumpAfterFullGC false {manageable} {default} bool HeapDumpBeforeFullGC false {manageable} {default} intx HeapDumpGzipLevel 0 {manageable} {default} bool HeapDumpOnOutOfMemoryError false {manageable} {default} ccstr HeapDumpPath {manageable} {default} uintx HeapFirstMaximumCompactionCount 3 {product} {default} uintx HeapMaximumCompactionInterval 20 {product} {default} uintx HeapSearchSteps 3 {product} {default} size_t HeapSizePerGCThread 43620760 {product} {default} bool IgnoreEmptyClassPaths false {product} {default} bool IgnoreUnrecognizedVMOptions false {product} {default} uintx IncreaseFirstTierCompileThresholdAt 50 {product} {default} bool IncrementalInline true {C2 product} {default} uintx InitialCodeCacheSize 2555904 {pd product} {default} size_t InitialHeapSize 490733568 {product} {ergonomic} uintx InitialRAMFraction 64 {product} {default} double InitialRAMPercentage 1.562500 {product} {default} uintx InitialSurvivorRatio 8 {product} {default} uintx InitialTenuringThreshold 7 {product} {default} uintx InitiatingHeapOccupancyPercent 45 {product} {default} bool Inline true {product} {default} ccstr InlineDataFile {product} {default} intx InlineSmallCode 2500 {C2 pd product} {default} bool InlineSynchronizedMethods true {C1 product} {default} intx InteriorEntryAlignment 16 {C2 pd product} {default} intx InterpreterProfilePercentage 33 {product} {default} bool JavaMonitorsInStackTrace true {product} {default} intx JavaPriority10_To_OSPriority -1 {product} {default} intx JavaPriority1_To_OSPriority -1 {product} {default} intx JavaPriority2_To_OSPriority -1 {product} {default} intx JavaPriority3_To_OSPriority -1 {product} {default} intx JavaPriority4_To_OSPriority -1 {product} {default} intx JavaPriority5_To_OSPriority -1 {product} {default} intx JavaPriority6_To_OSPriority -1 {product} {default} intx JavaPriority7_To_OSPriority -1 {product} {default} intx JavaPriority8_To_OSPriority -1 {product} {default} intx JavaPriority9_To_OSPriority -1 {product} {default} size_t LargePageHeapSizeThreshold 134217728 {product} {default} size_t LargePageSizeInBytes 0 {product} {default} intx LiveNodeCountInliningCutoff 40000 {C2 product} {default} bool LoadExecStackDllInVMThread true {product} {default} intx LoopMaxUnroll 16 {C2 product} {default} intx LoopOptsCount 43 {C2 product} {default} intx LoopPercentProfileLimit 30 {C2 pd product} {default} uintx LoopStripMiningIter 1000 {C2 product} {default} uintx LoopStripMiningIterShortLoop 100 {C2 product} {default} intx LoopUnrollLimit 60 {C2 pd product} {default} intx LoopUnrollMin 4 {C2 product} {default} bool LoopUnswitching true {C2 product} {default} bool ManagementServer false {product} {default} size_t MarkStackSize 4194304 {product} {ergonomic} size_t MarkStackSizeMax 536870912 {product} {default} uint MarkSweepAlwaysCompactCount 4 {product} {default} uintx MarkSweepDeadRatio 5 {product} {default} intx MaxBCEAEstimateLevel 5 {product} {default} intx MaxBCEAEstimateSize 150 {product} {default} uint64_t MaxDirectMemorySize 0 {product} {default} bool MaxFDLimit true {product} {default} uintx MaxGCMinorPauseMillis 18446744073709551615 {product} {default} uintx MaxGCPauseMillis 200 {product} {default} uintx MaxHeapFreeRatio 70 {manageable} {default} size_t MaxHeapSize 7818182656 {product} {ergonomic} intx MaxInlineLevel 15 {C2 product} {default} intx MaxInlineSize 35 {C2 product} {default} intx MaxJNILocalCapacity 65536 {product} {default} intx MaxJavaStackTraceDepth 1024 {product} {default} intx MaxJumpTableSize 65000 {C2 product} {default} intx MaxJumpTableSparseness 5 {C2 product} {default} intx MaxLabelRootDepth 1100 {C2 product} {default} intx MaxLoopPad 15 {C2 product} {default} size_t MaxMetaspaceExpansion 5439488 {product} {default} uintx MaxMetaspaceFreeRatio 70 {product} {default} size_t MaxMetaspaceSize 18446744073709551615 {product} {default} size_t MaxNewSize 4689231872 {product} {ergonomic} intx MaxNodeLimit 80000 {C2 product} {default} uint64_t MaxRAM 137438953472 {pd product} {default} uintx MaxRAMFraction 4 {product} {default} double MaxRAMPercentage 25.000000 {product} {default} intx MaxRecursiveInlineLevel 1 {C2 product} {default} uintx MaxTenuringThreshold 15 {product} {default} intx MaxTrivialSize 6 {C2 product} {default} intx MaxVectorSize 32 {C2 product} {default} ccstr MetaspaceReclaimPolicy balanced {product} {default} size_t MetaspaceSize 22020096 {product} {default} bool MethodFlushing true {product} {default} size_t MinHeapDeltaBytes 4194304 {product} {ergonomic} uintx MinHeapFreeRatio 40 {manageable} {default} size_t MinHeapSize 8388608 {product} {ergonomic} intx MinInliningThreshold 250 {product} {default} intx MinJumpTableSize 10 {C2 pd product} {default} size_t MinMetaspaceExpansion 327680 {product} {default} uintx MinMetaspaceFreeRatio 40 {product} {default} uintx MinRAMFraction 2 {product} {default} double MinRAMPercentage 50.000000 {product} {default} uintx MinSurvivorRatio 3 {product} {default} size_t MinTLABSize 2048 {product} {default} intx MultiArrayExpandLimit 6 {C2 product} {default} uintx NUMAChunkResizeWeight 20 {product} {default} size_t NUMAInterleaveGranularity 2097152 {product} {default} uintx NUMAPageScanRate 256 {product} {default} size_t NUMASpaceResizeRate 1073741824 {product} {default} bool NUMAStats false {product} {default} ccstr NativeMemoryTracking off {product} {default} bool NeverActAsServerClassMachine false {pd product} {default} bool NeverTenure false {product} {default} uintx NewRatio 2 {product} {default} size_t NewSize 1363144 {product} {default} size_t NewSizeThreadIncrease 5320 {pd product} {default} intx NmethodSweepActivity 10 {product} {default} intx NodeLimitFudgeFactor 2000 {C2 product} {default} uintx NonNMethodCodeHeapSize 7602480 {pd product} {ergonomic} uintx NonProfiledCodeHeapSize 122027880 {pd product} {ergonomic} intx NumberOfLoopInstrToAlign 4 {C2 product} {default} intx ObjectAlignmentInBytes 8 {product lp64_product} {default} size_t OldPLABSize 1024 {product} {default} size_t OldSize 5452592 {product} {default} bool OmitStackTraceInFastThrow true {product} {default} ccstrlist OnError {product} {default} ccstrlist OnOutOfMemoryError {product} {default} intx OnStackReplacePercentage 140 {pd product} {default} bool OptimizeFill false {C2 product} {default} bool OptimizePtrCompare true {C2 product} {default} bool OptimizeStringConcat true {C2 product} {default} bool OptoBundling false {C2 pd product} {default} intx OptoLoopAlignment 16 {pd product} {default} bool OptoRegScheduling true {C2 pd product} {default} bool OptoScheduling false {C2 pd product} {default} uintx PLABWeight 75 {product} {default} bool PSChunkLargeArrays true {product} {default} int ParGCArrayScanChunk 50 {product} {default} uintx ParallelGCBufferWastePct 10 {product} {default} uint ParallelGCThreads 13 {product} {default} size_t ParallelOldDeadWoodLimiterMean 50 {product} {default} size_t ParallelOldDeadWoodLimiterStdDev 80 {product} {default} bool ParallelRefProcBalancingEnabled true {product} {default} bool ParallelRefProcEnabled true {product} {default} bool PartialPeelAtUnsignedTests true {C2 product} {default} bool PartialPeelLoop true {C2 product} {default} intx PartialPeelNewPhiDelta 0 {C2 product} {default} uintx PausePadding 1 {product} {default} intx PerBytecodeRecompilationCutoff 200 {product} {default} intx PerBytecodeTrapLimit 4 {product} {default} intx PerMethodRecompilationCutoff 400 {product} {default} intx PerMethodTrapLimit 100 {product} {default} bool PerfAllowAtExitRegistration false {product} {default} bool PerfBypassFileSystemCheck false {product} {default} intx PerfDataMemorySize 32768 {product} {default} intx PerfDataSamplingInterval 50 {product} {default} ccstr PerfDataSaveFile {product} {default} bool PerfDataSaveToFile false {product} {default} bool PerfDisableSharedMem false {product} {default} intx PerfMaxStringConstLength 1024 {product} {default} size_t PreTouchParallelChunkSize 4194304 {pd product} {default} bool PreferContainerQuotaForCPUCount true {product} {default} bool PreferInterpreterNativeStubs false {pd product} {default} intx PrefetchCopyIntervalInBytes 576 {product} {default} intx PrefetchFieldsAhead 1 {product} {default} intx PrefetchScanIntervalInBytes 576 {product} {default} bool PreserveAllAnnotations false {product} {default} bool PreserveFramePointer false {pd product} {default} size_t PretenureSizeThreshold 0 {product} {default} bool PrintClassHistogram false {manageable} {default} bool PrintCodeCache false {product} {default} bool PrintCodeCacheOnCompilation false {product} {default} bool PrintCommandLineFlags false {product} {default} bool PrintCompilation false {product} {default} bool PrintConcurrentLocks false {manageable} {default} bool PrintExtendedThreadInfo false {product} {default} bool PrintFlagsFinal true {product} {command line} bool PrintFlagsInitial false {product} {default} bool PrintFlagsRanges false {product} {default} bool PrintGC false {product} {default} bool PrintGCDetails false {product} {default} bool PrintHeapAtSIGBREAK true {product} {default} bool PrintSharedArchiveAndExit false {product} {default} bool PrintSharedDictionary false {product} {default} bool PrintStringTableStatistics false {product} {default} bool PrintTieredEvents false {product} {default} bool PrintVMOptions false {product} {default} bool PrintWarnings true {product} {default} uintx ProcessDistributionStride 4 {product} {default} bool ProfileInterpreter true {pd product} {default} intx ProfileMaturityPercentage 20 {product} {default} uintx ProfiledCodeHeapSize 122027880 {pd product} {ergonomic} uintx PromotedPadding 3 {product} {default} uintx QueuedAllocationWarningCount 0 {product} {default} int RTMRetryCount 5 {ARCH product} {default} bool RangeCheckElimination true {product} {default} bool ReassociateInvariants true {C2 product} {default} bool RecordDynamicDumpInfo false {product} {default} bool ReduceBulkZeroing true {C2 product} {default} bool ReduceFieldZeroing true {C2 product} {default} bool ReduceInitialCardMarks true {C2 product} {default} bool ReduceSignalUsage false {product} {default} intx RefDiscoveryPolicy 0 {product} {default} bool RegisterFinalizersAtInit true {product} {default} bool RelaxAccessControlCheck false {product} {default} ccstr ReplayDataFile {product} {default} bool RequireSharedSpaces false {product} {default} uintx ReservedCodeCacheSize 251658240 {pd product} {ergonomic} bool ResizePLAB true {product} {default} bool ResizeTLAB true {product} {default} bool RestoreMXCSROnJNICalls false {product} {default} bool RestrictContended true {product} {default} bool RestrictReservedStack true {product} {default} bool RewriteBytecodes true {pd product} {default} bool RewriteFrequentPairs true {pd product} {default} bool SafepointTimeout false {product} {default} intx SafepointTimeoutDelay 10000 {product} {default} bool ScavengeBeforeFullGC false {product} {default} bool SegmentedCodeCache true {product} {ergonomic} intx SelfDestructTimer 0 {product} {default} ccstr SharedArchiveConfigFile {product} {default} ccstr SharedArchiveFile {product} {default} size_t SharedBaseAddress 34359738368 {product} {default} ccstr SharedClassListFile {product} {default} uintx SharedSymbolTableBucketSize 4 {product} {default} ccstr ShenandoahGCHeuristics adaptive {product} {default} ccstr ShenandoahGCMode satb {product} {default} bool ShowCodeDetailsInExceptionMessages true {manageable} {default} bool ShowMessageBoxOnError false {product} {default} bool ShrinkHeapInSteps true {product} {default} size_t SoftMaxHeapSize 7818182656 {manageable} {ergonomic} intx SoftRefLRUPolicyMSPerMB 1000 {product} {default} bool SplitIfBlocks true {C2 product} {default} intx StackRedPages 1 {pd product} {default} intx StackReservedPages 1 {pd product} {default} intx StackShadowPages 20 {pd product} {default} bool StackTraceInThrowable true {product} {default} intx StackYellowPages 2 {pd product} {default} uintx StartAggressiveSweepingAt 10 {product} {default} bool StartAttachListener false {product} {default} ccstr StartFlightRecording {product} {default} uint StringDeduplicationAgeThreshold 3 {product} {default} uintx StringTableSize 65536 {product} {default} bool SuperWordLoopUnrollAnalysis true {C2 pd product} {default} bool SuperWordReductions true {C2 product} {default} bool SuppressFatalErrorMessage false {product} {default} uintx SurvivorPadding 3 {product} {default} uintx SurvivorRatio 8 {product} {default} double SweeperThreshold 0.500000 {product} {default} uintx TLABAllocationWeight 35 {product} {default} uintx TLABRefillWasteFraction 64 {product} {default} size_t TLABSize 0 {product} {default} bool TLABStats true {product} {default} uintx TLABWasteIncrement 4 {product} {default} uintx TLABWasteTargetPercent 1 {product} {default} uintx TargetPLABWastePct 10 {product} {default} uintx TargetSurvivorRatio 50 {product} {default} uintx TenuredGenerationSizeIncrement 20 {product} {default} uintx TenuredGenerationSizeSupplement 80 {product} {default} uintx TenuredGenerationSizeSupplementDecay 2 {product} {default} intx ThreadPriorityPolicy 0 {product} {default} bool ThreadPriorityVerbose false {product} {default} intx ThreadStackSize 1024 {pd product} {default} uintx ThresholdTolerance 10 {product} {default} intx Tier0BackedgeNotifyFreqLog 10 {product} {default} intx Tier0InvokeNotifyFreqLog 7 {product} {default} intx Tier0ProfilingStartPercentage 200 {product} {default} intx Tier23InlineeNotifyFreqLog 20 {product} {default} intx Tier2BackEdgeThreshold 0 {product} {default} intx Tier2BackedgeNotifyFreqLog 14 {product} {default} intx Tier2CompileThreshold 0 {product} {default} intx Tier2InvokeNotifyFreqLog 11 {product} {default} intx Tier3BackEdgeThreshold 60000 {product} {default} intx Tier3BackedgeNotifyFreqLog 13 {product} {default} intx Tier3CompileThreshold 2000 {product} {default} intx Tier3DelayOff 2 {product} {default} intx Tier3DelayOn 5 {product} {default} intx Tier3InvocationThreshold 200 {product} {default} intx Tier3InvokeNotifyFreqLog 10 {product} {default} intx Tier3LoadFeedback 5 {product} {default} intx Tier3MinInvocationThreshold 100 {product} {default} intx Tier4BackEdgeThreshold 40000 {product} {default} intx Tier4CompileThreshold 15000 {product} {default} intx Tier4InvocationThreshold 5000 {product} {default} intx Tier4LoadFeedback 3 {product} {default} intx Tier4MinInvocationThreshold 600 {product} {default} bool TieredCompilation true {pd product} {default} intx TieredCompileTaskTimeout 50 {product} {default} intx TieredRateUpdateMaxTime 25 {product} {default} intx TieredRateUpdateMinTime 1 {product} {default} intx TieredStopAtLevel 4 {product} {default} bool TimeLinearScan false {C1 product} {default} ccstr TraceJVMTI {product} {default} intx TrackedInitializationLimit 50 {C2 product} {default} bool TrapBasedNullChecks false {pd product} {default} bool TrapBasedRangeChecks false {C2 pd product} {default} intx TypeProfileArgsLimit 2 {product} {default} uintx TypeProfileLevel 111 {pd product} {default} intx TypeProfileMajorReceiverPercent 90 {C2 product} {default} intx TypeProfileParmsLimit 2 {product} {default} intx TypeProfileWidth 2 {product} {default} intx UnguardOnExecutionViolation 0 {product} {default} bool UseAES true {product} {default} intx UseAVX 2 {ARCH product} {default} bool UseAdaptiveGenerationSizePolicyAtMajorCollection true {product} {default} bool UseAdaptiveGenerationSizePolicyAtMinorCollection true {product} {default} bool UseAdaptiveNUMAChunkSizing true {product} {default} bool UseAdaptiveSizeDecayMajorGCCost true {product} {default} bool UseAdaptiveSizePolicy true {product} {default} bool UseAdaptiveSizePolicyFootprintGoal true {product} {default} bool UseAdaptiveSizePolicyWithSystemGC false {product} {default} bool UseAddressNop true {ARCH product} {default} bool UseBASE64Intrinsics false {product} {default} bool UseBMI1Instructions true {ARCH product} {default} bool UseBMI2Instructions true {ARCH product} {default} bool UseBiasedLocking false {product} {default} bool UseBimorphicInlining true {C2 product} {default} bool UseCLMUL true {ARCH product} {default} bool UseCMoveUnconditionally false {C2 product} {default} bool UseCodeAging true {product} {default} bool UseCodeCacheFlushing true {product} {default} bool UseCompiler true {product} {default} bool UseCompressedClassPointers true {product lp64_product} {ergonomic} bool UseCompressedOops true {product lp64_product} {ergonomic} bool UseCondCardMark false {product} {default} bool UseContainerSupport true {product} {default} bool UseCountLeadingZerosInstruction true {ARCH product} {default} bool UseCountTrailingZerosInstruction true {ARCH product} {default} bool UseCountedLoopSafepoints true {C2 product} {default} bool UseCounterDecay true {product} {default} bool UseDivMod true {C2 product} {default} bool UseDynamicNumberOfCompilerThreads true {product} {default} bool UseDynamicNumberOfGCThreads true {product} {default} bool UseEmptySlotsInSupers true {product} {default} bool UseFMA true {product} {default} bool UseFPUForSpilling true {C2 product} {default} bool UseFastJNIAccessors true {product} {default} bool UseFastStosb false {ARCH product} {default} bool UseG1GC true {product} {ergonomic} bool UseGCOverheadLimit true {product} {default} bool UseHeavyMonitors false {product} {default} bool UseHugeTLBFS false {product} {default} bool UseInlineCaches true {product} {default} bool UseInterpreter true {product} {default} bool UseJumpTables true {C2 product} {default} bool UseLargePages false {pd product} {default} bool UseLargePagesIndividualAllocation false {pd product} {default} bool UseLinuxPosixThreadCPUClocks true {product} {default} bool UseLoopCounter true {product} {default} bool UseLoopInvariantCodeMotion true {C1 product} {default} bool UseLoopPredicate true {C2 product} {default} bool UseMaximumCompactionOnSystemGC true {product} {default} bool UseNUMA false {product} {default} bool UseNUMAInterleaving false {product} {default} bool UseNewLongLShift true {ARCH product} {default} bool UseNotificationThread true {product} {default} bool UseOnStackReplacement true {pd product} {default} bool UseOnlyInlinedBimorphic true {C2 product} {default} bool UseOprofile false {product} {default} bool UseOptoBiasInlining false {C2 product} {default} bool UsePSAdaptiveSurvivorSizePolicy true {product} {default} bool UseParallelGC false {product} {default} bool UsePerfData true {product} {default} bool UsePopCountInstruction true {product} {default} bool UseProfiledLoopPredicate true {C2 product} {default} bool UseRTMDeopt false {ARCH product} {default} bool UseRTMLocking false {ARCH product} {default} bool UseSHA true {product} {default} bool UseSHM false {product} {default} intx UseSSE 4 {ARCH product} {default} bool UseSSE42Intrinsics true {ARCH product} {default} bool UseSerialGC false {product} {default} bool UseSharedSpaces true {product} {default} bool UseShenandoahGC false {product} {default} bool UseSignalChaining true {product} {default} bool UseStoreImmI16 true {ARCH product} {default} bool UseStringDeduplication false {product} {default} bool UseSubwordForMaxVector true {C2 product} {default} bool UseSuperWord true {C2 product} {default} bool UseTLAB true {product} {default} bool UseThreadPriorities true {pd product} {default} bool UseTransparentHugePages false {product} {default} bool UseTypeProfile true {product} {default} bool UseTypeSpeculation true {C2 product} {default} bool UseUnalignedLoadStores true {ARCH product} {default} bool UseVectorCmov false {C2 product} {default} bool UseXMMForArrayCopy true {product} {default} bool UseXMMForObjInit true {ARCH product} {default} bool UseXmmI2D true {ARCH product} {default} bool UseXmmI2F true {ARCH product} {default} bool UseXmmLoadAndClearUpper true {ARCH product} {default} bool UseXmmRegToRegMoveAll true {ARCH product} {default} bool UseZGC false {product} {default} intx VMThreadPriority -1 {product} {default} intx VMThreadStackSize 1024 {pd product} {default} intx ValueMapInitialSize 11 {C1 product} {default} intx ValueMapMaxLoopSize 8 {C1 product} {default} intx ValueSearchLimit 1000 {C2 product} {default} bool VerifySharedSpaces false {product} {default} uintx YoungGenerationSizeIncrement 20 {product} {default} uintx YoungGenerationSizeSupplement 80 {product} {default} uintx YoungGenerationSizeSupplementDecay 8 {product} {default} size_t YoungPLABSize 4096 {product} {default} double ZAllocationSpikeTolerance 2.000000 {product} {default} double ZCollectionInterval 0.000000 {product} {default} double ZFragmentationLimit 25.000000 {product} {default} size_t ZMarkStackSpaceLimit 8589934592 {product} {default} bool ZProactive true {product} {default} bool ZUncommit true {product} {default} uintx ZUncommitDelay 300 {product} {default} bool ZeroTLAB false {product} {default}"},{"location":"reference/jvm/profile-tools/","title":"Profile JVM applications","text":"Profile applications on the JVM, visualising memory and CPU resources, identifying bottlenecks and areas of the code to review to optimise a running application.
Using FlameGraphs To Illuminate The JVM A Simple Approach to the Advanced JVM Profiling
"},{"location":"reference/jvm/profile-tools/#visualvm","title":"VisualVM","text":"VisualVM provides a simplified and robust profiling tool for Java applications, bundled with the Java Development Kit (JDK) and using JConsole, jstat, jstack, jinfo, and jmap.
UbuntuMacOSX Ubuntu / Debian includes VisualVM in the software center
sudo apt install visualvm\n
Download the macOS application bundle and double-click to install.
"},{"location":"reference/jvm/profile-tools/#jdk-flight-recorder","title":"JDK Flight Recorder","text":"JDK Flight Recorder is a production time profiling and diagnostics engine built into the JVM
- Extremely low overhead - no measurable impact on the running application
- High performance flight recording engine and high performance data collection
- Safe and reliable in production, tested on all platforms as part of the JVM/JDK-testing
- Time machine records data before, up to, and right after a problem occurs (even if the JVM process crashes)
jcmd
to access the flight recorder data from the command line
Mission control provides a graphical tool to visualise flight recorder data.
- Continuous Monitoring with JDK Flight Recorder
- JDK11 - Introduction to JDK Flight Recorder
- Production profiling with JDK Flight Recorder & JDK Mission Control
"},{"location":"reference/jvm/profile-tools/#mission-control","title":"Mission Control","text":"Mission Control is an open source desktop tool for visualising production time profiling and diagnostics from the JDK flight recorder tool. JDK Mission Control supports OpenJDK 11 and above.
JDK Mission Control consists of
- A JDK Flight Recorder (JFR) analyser and visualiser
- A Java Management Extensions (JMX) Console
- A heap dump (hprof format) analyzer (JOverflow)
Eclipse Mission Control from Adoptium
Java Mission Control demo - 2014 outated but might be useful if nothing newer
"},{"location":"reference/jvm/profile-tools/#profiling-guides","title":"Profiling guides","text":"Profiling your Java Application - A Beginner\u2019s Guide - Victor Rentea
Explore three of the best free tools for profiling a Java (Spring) application:
- Using Java Flight Recorder to profile method execution times
- Using Micrometer-Prometheus-Grafana to profile connection starvation issues
- Using Glowroot to identify long-running queries
"},{"location":"reference/jvm/profile-tools/#references","title":"References","text":"Java Profilers - Baeldung HotSpot Virtual Machine Garbage Collection Tuning Guide - Oracle
"},{"location":"reference/jvm/understanding-memory-usage/","title":"Memory usage","text":"Adjusting the heap size and Garbage Collection behaviour is often the simplest means to improving application performance and stability. A mismatch between the heap size.
Allocating additional memory to the HotSpot JVM is a relatively cheap way to improve the performance of an application.
Garbage collection cost is in the form of execution pauses while the HotSpot JVM cleans up the no-longer-needed heap allocations.
Report a full breakdown of the HotSpot JVM\u2019s memory usage upon exit using the following option combination:
JVM Memory Usage Report
```shell -XX:+UnlockDiagnosticVMOptions \u2011XX:NativeMemoryTracking=summary \u2011XX:+PrintNMTStatistics.
```
"},{"location":"reference/jvm/understanding-memory-usage/#out-of-memory-errors","title":"Out Of Memory errors","text":"When experiencing OutOfMemory
errors, consider how the HotSpot JVM should behave if the application runs out of memory.
-XX:+ExitOnOutOfMemoryError
- HotSpot JVM exits on the first OutOfMemory error, suitable if the JVM will be automatically restarted (such as in container services)
-XX:+HeapDumpOnOutOfMemoryError
- dump contents of heap to file, <java_pid>.hprof
, to help diagnose memory leaks
-XX:HeapDumpPath
defines the path for the heap dump, default is current directory
"},{"location":"reference/jvm/understanding-memory-usage/#choose-a-garbage-collector","title":"Choose A Garbage Collector","text":"The HotSpot Virtual Machine Garbage Collection Tuning Guide provides advice on selecting a suitable garbage collector (GC)
G1GC collector is the default used by the JDK ergonomics process on most hardware.
Other garbage collectors available include:
-XX:+UseSerialGC
- serial collector, performing all GC work on a single thread
-XX:+UseParallelGC
- parallel (throughput) collector, performs compaction using multiple threads.
-XX:+UseZGC
- ZGC collector scalable low latency garbage collector (experimental in JDK 11, so requires -XX:+UnlockExperimentalVMOptions
).
Enable garbage collection logging
-Xlog:gc
- basic GC logging
-Xlog:gc*
- verbose GC logging
"},{"location":"reference/jvm/understanding-memory-usage/#object-allocation","title":"Object Allocation","text":"Applications that create short-lived objects at a high allocation rates can lead to the premature promotion of short-lived objects to the old-generation heap space. There the objects will accumulate until a full garbage collection is needed
To avoid premature promotion:
-XX:NewSize=n
- initial size for the young generation
-XX:MaxNewSize=n
- maximum size for the young generation
-XX:MaxTenuringThreshold=n
- maximum number of young-generation collections an object can survive before it is promoted to the old generation
"},{"location":"reference/jvm/understanding-memory-usage/#just-in-time-optimisation","title":"Just In Time Optimisation","text":"Understand how the Just In Time (JIT) compiler optimises the code.
Once an application garbage collection pauses are an acceptable level, check the JIT compilers are optimizing the parts of your program you think are important for performance.
Enable compilation logging:
-XX:+PrintCompilation
print basic information about each JIT compilation to the console
-XX:+UnlockDiagnosticVMOptions \u2011XX:+PrintCompilation \u2011XX:+PrintInlining
- information about method in-lining
"},{"location":"reference/performance/","title":"Clojure Performance","text":"Two excellent presentations on Clojure performance
"},{"location":"reference/performance/#optimising-the-critical-path","title":"Optimising the critical path","text":"Using two of Clojure's fundamental building blocks, macros and higher order functions, Clojure code can be sped up significantly without sacrificing common idioms.
Premature optimisation is the root of all evil, this is not a reason to pass up opportunities to improve the critical 3%.
"},{"location":"reference/performance/#naked-performance","title":"Naked Performance","text":"Lessons learned on building Clojure/Script systems that are both ridiculously fast and will fail fast on errors.
Compare the performance of mutable, persistent & zero-copy data structures, showing how to use interpreters and compilers to build beautiful and performant abstractions.
A shot demo on building a simple non-blocking web server that runs idiomatic Clojure to serve millions of requests per second.
"},{"location":"reference/performance/#advanced-types","title":"Advanced types","text":"Clojure support for Java Primitives
Clojure has support for high-performance with Java primitive types in local contexts. All Java primitive types are supported: int, float, long, double, boolean, char, short, and byte. In the extremely rare occasions where this is needed, it is added via metadata and therefore only adds to the existing code without rewriting it.
Rather than write this Java:
static public float asum(float[] xs){\n float ret = 0;\n for(int i = 0; i < xs.length; i++)\n ret += xs[i];\n return ret;\n}\n
you can write this Clojure:
(defn asum [^floats xs]\n (areduce xs i ret (float 0)\n (+ ret (aget xs i))))\n
and the resulting code is exactly the same speed (when run with java -server).
"},{"location":"reference/performance/#optimization-tips-for-types","title":"Optimization tips for types","text":"All arguments are passed to Clojure fns as objects, so there's no point to putting non-array primitive type hints on fn args. Instead, use the let technique shown to place args in primitive locals if they need to participate in primitive arithmetic in the body. (let [foo (int bar)] ...) is the correct way to get a primitive local. Do not use ^Integer etc.
Don't rush to unchecked math unless you want truncating operations. HotSpot does a good job at optimizing the overflow check, which will yield an exception instead of silent truncation. On a typical example, that has about a 5% difference in speed - well worth it. Also, people reading your code don't know if you are using unchecked for truncation or performance - best to reserve it for the former and comment if the latter.
There's usually no point in trying to optimize an outer loop, in fact it can hurt you as you'll be representing things as primitives which just have to be re-boxed in order to become args to the inner call. The only exception is reflection warnings - you must get rid of them in any code that gets called frequently.
Almost every time someone presents something they are trying to optimize with hints, the faster version has far fewer hints than the original. If a hint doesn't improve things in the end - take it out.
Many people seem to presume only the unchecked- ops do primitive arithmetic - not so. When the args are primitive locals, regular + and * etc do primitive math with an overflow check - fast and safe.
So, the simplest route to fast math is to leave the operators alone and just make sure the source literals and locals are primitive. Arithmetic on primitives yields primitives. If you've got a loop (which you probably do if you need to optimize) make sure the loop locals are primitives first - then if you accidentally are producing a boxed intermediate result you'll get an error on recur. Don't solve that error by coercing your intermediate result, instead, figure out what argument or local is not primitive.
"},{"location":"reference/standard-library/","title":"Clojure Standard Library","text":"Examples of using the functions from the clojure.core
namespace and other important functions, macros and special forms that are part of the org.clojure/clojure
library.
There are approximately 700 functions and macros available in the clojure.core
namespace. These are referred to as the Clojure Standard Library.
Counting functions in clojure.core
To get an accurate number of functions, call the ns-publics
function with a namespace name
(count (ns-publics 'clojure.core))\n
Random Function is a simple project that prints out a random function from the given namespace, or from clojure.core by default."},{"location":"reference/standard-library/#functions-macros-and-special-forms","title":"Functions, Macros and Special forms","text":"The majority of times macros and special forms act just like any other defined function (i.e. fn
, defn
)
A macro is a piece of code that evaluates into a function when read by the macro reader, or by the developer using macroexpand
function. An expanded macro may also contain macros, so expansion could take place several levels (macroexpand-all
).
macros are not composable like functions, so functions like apply
reduce
map
cannot use a macro (use a function instead).
Special forms are built into the Clojure runtime, so will not be found in clojure.core
- Special forms:
if
do
let
quote
var
fn
loop
recur
throw
try
- Special forms for Java interop:
.
new
set!
"},{"location":"reference/standard-library/collections/","title":"Standard Library: Collections","text":"Functions to create and work with the Clojure collection types, mainly maps, vectors and sets
See Sequences for functions around lists and (lazy) sequences
"},{"location":"reference/standard-library/cond-thread-macro/","title":"Clojure cond->","text":"cond->
and cond->>
are versatile macros available since version 1.5, although its more of a nieche use, its really useful in that neiche
Usage: (cond-> expr & clauses)
Takes an expression and a set of test/form pairs.
Threads expr (via ->) through each form for which the corresponding test expression is true.
Note that, unlike cond branching, cond-> threading does not short circuit after the first true test expression.
"},{"location":"reference/standard-library/cond-thread-macro/#deconstruct","title":"Deconstruct","text":"(cond-> 10\n false inc)\n;; => 10\n
In the above example 10 is the expr mentioned in the docstring and everything after it are the clauses.
Each clause is a pair made up of a test and a form. There is a single clause with the value false as the test the function inc as the form.
Since the test evaluates to a false value the expression is not threaded into the form. As a result the original expression, 10, is returned.
Let\u2019s look at an example with a truthy test.
(cond-> 10\n true (- 2)\n;;=> 8\n
Once again, 10 is the starting expression. The single clause has a test that evaluates to true so the expression is threaded into the first position of the form (- 2). The result is 8 and this is returned.
An example of a cond->
with multiple clauses. Explanations are inline with the code.
(cond-> 10 ; start with 10\n ;; test evaluates to true, so apply inc to 10. Current value is now 11.\n true inc\n\n ;; (zero? 1) evaluates to false, do not perform action. Current value stays 11.\n (zero? 1) (+ 2)\n\n ;; (pos? 4) evaluates to true, thread 11 into first position of form.\n (pos? 4) (- 5))\n;; => 6 ; The result of (- 11 5) is 6.\n
If you understand the above example then you have a good grasp of cond->. But when is this functionality useful?
"},{"location":"reference/standard-library/cond-thread-macro/#when-to-use-cond-","title":"When to use cond->?","text":"Looking through the codebases I work on, I almost primarily see cond-> being used with the initial expression being a hash-map. It is being used in situations where we want to selectively assoc, update, or dissoc something from a map.
If cond-> did not exist you would accomplish those selective modifications with code similar to below.
(if (some-pred? q)\n (assoc m :a-key :a-value)\n m)\n
Rewrite the above with cond->.
(cond-> m\n (some-pred? q) (assoc :a-key :a-value))\n
If you\u2019re not used to seeing cond-> the above transformation might seem like a step backwards. I know it felt that way to me when I first saw cond->. Give yourself time to get familiar with it and you\u2019ll be glad you\u2019re using it.
A meatier example of using cond-> is demonstrated below. Here we\u2019re manipulating data structures designed for use with honeysql to generate SQL statements. We start with a base-query and selectively modify it based on incoming parameters.
(defn query [req-params]\n (let [and-clause (fnil conj [:and])\n base-query {:select [:name :job]\n :from [:person]}]\n (cond-> base-query\n (:job req-params) (update :where and-clause [:= :job (:job req-params)])\n (:name req-params) (update :where and-clause [:= :name (:name req-params)])\n (:min-age req-params) (update :where and-clause [:> :age (:min-age req-params)]))))\n
Hopefully this gives you a taste of cond->. I\u2019ve found it to be quite useful. It has a place in every Clojure developer\u2019s toolbox.
"},{"location":"reference/standard-library/destructuring/","title":"Destructuring","text":"Destructuring is a form of pattern matching where you return specific elements from a collection and assign those elements names. It is commonly used in function parameter lists or with the let
function.
Destructuring is also known as abstract structural binding
A simple example of destructuring is assigning the values of a collection, in this case a vector.
(def co-ordinates [5 7])\n\n(let [[x y] co-ordinates]\n (str \"x:\" x \"y:\" y))\n;; => x: 5 y: 7\n
;; Sometimes we do not need all the information, so we can just use the elements we need.
(def three-dee-co-ordinates [2 7 4])\n\n(let [[x y] three-dee-co-ordinates]\n (str \"I only need the 2D co-ordinates, X: \" x \" and Y: \" y ))\n;; => \"I only need the 2D co-ordinates, X: 2 and Y: 7\"\n
Its quite common to take the first element as a specific name and use another name for the rest of the elements
(def shopping-list [\"oranges\" \"apples\" \"spinach\" \"carrots\" \"potatoes\" \"beetroot\"])\n\n(defn get-item [items]\n (let [[next-item & other-items] items]\n (str \"The next item to get is: \" next-item)))\n\n(get-item shopping-list)\n;; => \"The next item to get is: oranges\"\n
This example seems a little redundant at first, however if we add recursion then we can iterate through the shopping list and it should make more sense
splitting a vector into a head and a tail. When defining a function with an arglist** you use an ampersand. The same is true in destructuring.
(def indexes [1 2 3])\n\n(let [[x & more] indexes]\n (println \"x:\" x \"more:\" more))\n;; => x: 1 more: (2 3)\n
It's also worth noting that you can bind the entire vector to a local using the :as directive.
(def indexes [1 2 3])\n
"},{"location":"reference/standard-library/destructuring/#userindexes","title":"'user/indexes","text":" (let [[x & more :as full-list] indexes]\n (println \"x:\" x \"more:\" more \"full list:\" full-list))\n;; => x: 1 more: (2 3) full list: [1 2 3]\n
Vector examples are the easiest; however, in practice I find myself using destructuring with maps far more often.
Simple destructuring on a map is as easy as choosing a local name and providing the key.
(def point {:x 5 :y 7})\n
"},{"location":"reference/standard-library/destructuring/#userpoint","title":"'user/point","text":" (let [{the-x :x the-y :y} point]\n (println \"x:\" the-x \"y:\" the-y))\n;; => x: 5 y: 7\n
As the example shows, the values of :x and :y are bound to locals with the names the-x and the-y. In practice we would never prepend \"the-\" to our local names; however, using different names provides a bit of clarity for our first example. In production code you would be much more likely to want locals with the same name as the key. This works perfectly well, as the next example shows.
(def point {:x 5 :y 7})\n
"},{"location":"reference/standard-library/destructuring/#userpoint_1","title":"'user/point","text":"user=> (let [{x :x y :y} point] (println \"x:\" x \"y:\" y)) x: 5 y: 7
While this works perfectly well, creating locals with the same name as the keys becomes tedious and annoying (especially when your keys are longer than one letter). Clojure anticipates this frustration and provides :keys directive that allows you to specify keys that you would like as locals with the same name.
user=> (def point {:x 5 :y 7})
"},{"location":"reference/standard-library/destructuring/#userpoint_2","title":"'user/point","text":"user=> (let [{:keys [x y]} point] (println \"x:\" x \"y:\" y)) x: 5 y: 7
There are a few directives that work while destructuring maps. The above example shows the use of :keys. In practice I end up using :keys the most; however, I've also used the :as directive while working with maps.
The following example illustrates the use of an :as directive to bind a local with the entire map.
user=> (def point {:x 5 :y 7})
"},{"location":"reference/standard-library/destructuring/#userpoint_3","title":"'user/point","text":"user=> (let [{:keys [x y] :as the-point} point] (println \"x:\" x \"y:\" y \"point:\" the-point)) x: 5 y: 7 point: {:x 5, :y 7}
We've now seen the :as directive used for both vectors and maps. In both cases the local is always assigned to the entire expression that is being destructured.
For completeness I'll document the :or directive; however, I must admit that I've never used it in practice. The :or directive is used to assign default values when the map being destructured doesn't contain a specified key.
user=> (def point {:y 7})
"},{"location":"reference/standard-library/destructuring/#userpoint_4","title":"'user/point","text":"user=> (let [{:keys [x y] :or {x 0 y 0}} point] (println \"x:\" x \"y:\" y)) x: 0 y: 7
Lastly, it's also worth noting that you can destructure nested maps, vectors and a combination of both.
The following example destructures a nested map
user=> (def book {:name \"SICP\" :details {:pages 657 :isbn-10 \"0262011530\"}})
"},{"location":"reference/standard-library/destructuring/#userbook","title":"'user/book","text":"user=> (let [{name :name {pages :pages isbn-10 :isbn-10} :details} book] (println \"name:\" name \"pages:\" pages \"isbn-10:\" isbn-10)) name: SICP pages: 657 isbn-10: 0262011530
As you would expect, you can also use directives while destructuring nested maps.
user=> (def book {:name \"SICP\" :details {:pages 657 :isbn-10 \"0262011530\"}})
"},{"location":"reference/standard-library/destructuring/#userbook_1","title":"'user/book","text":"user=> user=> (let [{name :name {:keys [pages isbn-10]} :details} book] (println \"name:\" name \"pages:\" pages \"isbn-10:\" isbn-10)) name: SICP pages: 657 isbn-10: 0262011530
Destructuring nested vectors is also very straight-forward, as the following example illustrates
user=> (def numbers [[1 2][3 4]])
"},{"location":"reference/standard-library/destructuring/#usernumbers","title":"'user/numbers","text":"user=> (let [[[a b][c d]] numbers] (println \"a:\" a \"b:\" b \"c:\" c \"d:\" d)) a: 1 b: 2 c: 3 d: 4
Since binding forms can be nested within one another arbitrarily, you can pull apart just about anything -- http://clojure.org/special_forms\n
The following example destructures a map and a vector at the same time.
user=> (def golfer {:name \"Jim\" :scores [3 5 4 5]})
"},{"location":"reference/standard-library/destructuring/#usergolfer","title":"'user/golfer","text":"user=> (let [{name :name [hole1 hole2] :scores} golfer] (println \"name:\" name \"hole1:\" hole1 \"hole2:\" hole2)) name: Jim hole1: 3 hole2: 5
The same example can be rewritten using a function definition to show the simplicity of using destructuring in parameter lists.
user=> (defn print-status [{name :name [hole1 hole2] :scores}] (println \"name:\" name \"hole1:\" hole1 \"hole2:\" hole2))
"},{"location":"reference/standard-library/destructuring/#userprint-status","title":"'user/print-status","text":"user=> (print-status {:name \"Jim\" :scores [3 5 4 5]}) name: Jim hole1: 3 hole2: 5
There are other (less used) directives and deeper explanations available on http://clojure.org/special_forms and in The Joy of Clojure. I recommend both.
**(defn do-something [x y & more] ... ) Posted by Jay Fields at 7:44 AM Email ThisBlogThis!Share to TwitterShare to FacebookShare to Pinterest Labels: clojure, destructuring 10 comments:
fogus8:26 AM\n\nNice post. One other note that naturally follows from the end of your post is that destructuring forms the basis of Clojure's named arguments:\n\n(defn print-status [& {name :name [hole1 hole2] :scores}]\n(println \"name:\" name \"hole1:\" hole1 \"hole2:\" hole2))\n\n(print-status :name \"Joey\" :scores [42 18])\n\n\nYou can also use pre-conditions to check if certain arguments are passed in:\n\n\n(defn print-status [& {name :name [hole1 hole2] :scores}]\n{:pre [name]}\n(println \"name:\" name \"hole1:\" hole1 \"hole2:\" hole2))\n\n(print-status :scores [42 18])\n; java.lang.AssertionError: Assert failed: name\n\n(print-status :name \"Joey\" :scores [42 18])\n; name: Joey hole1: 42 hole2: 18\n\n\n:f\nReply\nJay Fields9:08 AM\n\nGood stuff Fogus, thanks.\n\nCheers, Jay\nReply\nMatt Todd5:31 PM\n\nCan you combine :as and :or et al?\nReply\nAnonymous7:29 PM\n\nYes, all the directives can be used at the same time.\n\nCheers, Jay\nReply\nLaurent PETIT3:08 AM\n
Hi, one note about using destructuring for function arguments : by doing so, you're quite explicitly establishing a more detailed contract with the consumer of the function. That is, you open the internals of the passed arguments.
Depending on the fact that the user may or may not be aware of the internals of the arguments, it may or may not be a good idea.
So I tend to think about the use of destructuring function arguments directly in the function signature, depending on whether the \"layout\" of the arguments of the function is part of the user API. Reply
"},{"location":"reference/standard-library/destructuring/#clojure-destructuring-tutorial-and-cheat-sheet","title":"Clojure Destructuring Tutorial and Cheat Sheet","text":"(Related blog post)
Simply put, destructuring in Clojure is a way extract values from a data structure and bind them to symbols, without having to explicitly traverse the data structure. It allows for elegant and concise Clojure code.
"},{"location":"reference/standard-library/destructuring/#vectors","title":"Vectors","text":"Syntax: [symbol another-symbol] [\"value\" \"another-value\"]
(def my-vector [:a :b :c :d])\n(def my-nested-vector [:a :b :c :d [:x :y :z]])\n\n(let [[a b c d] my-vector]\n (println a b c d))\n;; => :a :b :c :d\n\n(let [[a _ _ d [x y z]] my-nested-vector]\n (println a d x y z))\n;; => :a :d :x :y :z\n
You don't have to match the full vector.
(let [[a b c] my-vector]\n (println a b c))\n;; => :a :b :c\n
You can use & the-rest
to bind the remaining part of the vector to the-rest
.
(let [[a b & the-rest] my-vector]\n (println a b the-rest))\n;; => :a :b (:c :d)\n
When a destructuring form \"exceeds\" a vector (i.e. there not enough items in the vector to bind to), the excess symbols will be bound to nil
.
(let [[a b c d e f g] my-vector]\n (println a b c d e f g))\n;; => :a :b :c :d nil nil nil\n
You can use :as some-symbol
as the last two items in the destructuring form to bind the whole vector to some-symbol
(let [[:as all] my-vector]\n (println all))\n;; => [:a :b :c :d]\n\n(let [[a :as all] my-vector]\n (println a all))\n;; => :a [:a :b :c :d]\n\n(let [[a _ _ _ [x y z :as nested] :as all] my-nested-vector]\n (println a x y z nested all))\n;; => :a :x :y :z [:x :y :z] [:a :b :c :d [:x :y :z]]\n
You can use both & the-rest
and :as some-symbol
.
(let [[a b & the-rest :as all] my-vector]\n (println a b the-rest all))\n;; => :a :b (:c :d) [:a :b :c :d]\n
"},{"location":"reference/standard-library/destructuring/#optional-arguments-for-functions","title":"Optional arguments for functions","text":"With destructuring and the & the-rest
form, you can specify optional arguments to functions.
(defn foo [a b & more-args]\n (println a b more-args))\n(foo :a :b) ;; => :a :b nil\n(foo :a :b :x) ;; => :a :b (:x)\n(foo :a :b :x :y :z) ;; => :a :b (:x :y :z)\n\n(defn foo [a b & [x y z]]\n (println a b x y z))\n(foo :a :b) ;; => :a :b nil nil nil\n(foo :a :b :x) ;; => :a :b :x nil nil\n(foo :a :b :x :y :z) ;; => :a :b :x :y :z\n
"},{"location":"reference/standard-library/destructuring/#maps","title":"Maps","text":"Syntax: {symbol :key, another-symbol :another-key} {:key \"value\" :another-key \"another-value\"}
(def my-hashmap {:a \"A\" :b \"B\" :c \"C\" :d \"D\"})\n(def my-nested-hashmap {:a \"A\" :b \"B\" :c \"C\" :d \"D\" :q {:x \"X\" :y \"Y\" :z \"Z\"}})\n\n(let [{a :a d :d} my-hashmap]\n (println a d))\n;; => A D\n\n(let [{a :a, b :b, {x :x, y :y} :q} my-nested-hashmap]\n (println a b x y))\n;; => A B X Y\n
Similar to vectors, if a key is not found in the map, the symbol will be bound to nil
.
(let [{a :a, not-found :not-found, b :b} my-hashmap]\n (println a not-found b))\n;; => A nil B\n
You can provide an optional default value for these missing keys with the :or
keyword and a map of default values.
(let [{a :a, not-found :not-found, b :b, :or {not-found \":)\"}} my-hashmap]\n (println a not-found b))\n;; => A :) B\n
The :as some-symbol
form is also available for maps, but unlike vectors it can be specified anywhere (but still preferred to be the last two pairs).
(let [{a :a, b :b, :as all} my-hashmap]\n (println a b all))\n;; => A B {:a A :b B :c C :d D}\n
And combining :as
and :or
keywords (again, :as
preferred to be the last).
(let [{a :a, b :b, not-found :not-found, :or {not-found \":)\"}, :as all} my-hashmap]\n (println a b not-found all))\n;; => A B :) {:a A :b B :c C :d D}\n
There is no & the-rest
for maps.
"},{"location":"reference/standard-library/destructuring/#shortcuts","title":"Shortcuts","text":"Having to specify {symbol :symbol}
for each key is repetitive and verbose (it's almost always going to be the symbol equivalent of the key), so shortcuts are provided so you only have to type the symbol once.
Here are all the previous examples using the :keys
keyword followed by a vector of symbols:
(let [{:keys [a d]} my-hashmap]\n (println a d))\n;; => A D\n\n(let [{:keys [a b], {:keys [x y]} :q} my-nested-hashmap]\n (println a b x y))\n;; => A B X Y\n\n(let [{:keys [a not-found b]} my-hashmap]\n (println a not-found b))\n;; => A nil B\n\n(let [{:keys [a not-found b], :or {not-found \":)\"}} my-hashmap]\n (println a not-found b))\n;; => A :) B\n\n(let [{:keys [a b], :as all} my-hashmap]\n (println a b all))\n;; => A B {:a A :b B :c C :d D}\n\n(let [{:keys [a b not-found], :or {not-found \":)\"}, :as all} my-hashmap]\n (println a b not-found all))\n;; => A B :) {:a A :b B :c C :d D}\n
There are also :strs
and :syms
alternatives, for when your map has strings or symbols for keys (instead of keywords), respectively.
(let [{:strs [a d]} {\"a\" \"A\", \"b\" \"B\", \"c\" \"C\", \"d\" \"D\"}]\n (println a d))\n;; => A D\n\n(let [{:syms [a d]} {'a \"A\", 'b \"B\", 'c \"C\", 'd \"D\"}]\n (println a d))\n;; => A D\n
"},{"location":"reference/standard-library/destructuring/#keyword-arguments-for-function","title":"Keyword arguments for function","text":"Map destructuring also works with lists (but not vectors).
(let [{:keys [a b]} '(\"X\", \"Y\", :a \"A\", :b \"B\")]\n(println a b))\n;; => A B\n
This allows your functions to have optional keyword arguments.
(defn foo [a b & {:keys [x y]}]\n (println a b x y))\n(foo \"A\" \"B\") ;; => A B nil nil\n(foo \"A\" \"B\" :x \"X\") ;; => A B X nil\n(foo \"A\" \"B\" :x \"X\" :y \"Y\") ;; => A B X Y\n
"},{"location":"reference/standard-library/predicate-functions/","title":"Clojure Predicate functions","text":"A predicate function takes a single argument and returns a truthy value, e.g. true
or false
There are over 70 predicate functions provided by the clojure.core
namespace.
clojure.core
predicates Description >0? (^:private) >1? (^:private) any? associative? boolean? bound? bytes? chunked-seq? (^:static) class? coll? contains? counted? decimal? delay? distinct? double? empty? even? every? false? fits-table? (defn-) float? fn? future? future-cancelled? future-done? ident? identical? ifn? indexed? inst? int? integer? isa? is-annotation? (defn-) is-runtime-annotation? (defn-) keyword? libspec? (defn-) list? map-entry? nat-int? neg? neg-int? nil? number? odd? pos? pos-int? qualified-ident? qualified-keyword? qualified-symbol? ratio? rational? reader-conditional? realized? reduced? reversible? seqable? sequential? set? simple-ident? simple-keyword? simple-symbol? some? sorted? special-symbol? symbol? tagged-literal? thread-bound? true? uri? uuid? var? volatile? zero?"},{"location":"reference/standard-library/sequences/","title":"Standard Library: Sequences","text":"Functions to create and work with the Clojure sequences, including lists and sequence generators
"},{"location":"reference/standard-library/sequences/#sequence-access","title":"Sequence access","text":"Function Description first
second
rest
last
butlast
nth
"},{"location":"reference/standard-library/sequences/#infinite-sequence-generators","title":"Infinite sequence generators","text":"Function Description range
cycle
iterate
"},{"location":"reference/standard-library/regular-expressions/","title":"Regular Expressions - regex","text":"Regular expressions are a powerful and compact way to find specific patterns in text strings. Clojure provides a simple syntax for Java regex patterns.
#\"pattern\"
is the literal representation of a regular expressions in Clojure, where pattern
is the regular expression.
Create regular expression pattern
(re-pattern pattern)
will return the Clojure literal representation of a given regex pattern.
A string can become a regular expression pattern, e.g. \":\"
becomes the regex pattern #\":\"
(re-pattern \":\")\n
The regular expression syntax cheatsheet by Mozilla is an excellent reference for regular expression patterns.
"},{"location":"reference/standard-library/regular-expressions/#regular-expressions-overview","title":"Regular expressions overview","text":"Regular expressions in Clojure
Find the most common word in a book using regular expressions
Double escaping not required The Clojure syntax means you do not need to double escape special characters, eg. \\\\
, and keeps the patterns clean and simple to read. In other languages, backslashes intended for consumption by the regex compiler must be doubled.
(java.util.regex.Pattern/compile \"\\\\d\")\n;;=> #\"\\d\"\n
The rules for embedding unusual literal characters or predefined character classes are listed in the Javadoc for Pattern.
"},{"location":"reference/standard-library/regular-expressions/#host-platform-support","title":"Host platform support","text":"Clojure runs on the Java Virtual Machine and uses Java regular expressions.
Regular expressions in Clojure create a java.util.regex.Pattern type
(type #\"pattern\")\n;;=> java.util.regex.Pattern\n
ClojureScript runs on JavaScript engines and uses Javascript regular expressions.
"},{"location":"reference/standard-library/regular-expressions/#option-flags","title":"Option flags","text":"Regular expression option flags can make a pattern case-insensitive or enable multiline mode. Clojure's regex literals starting with (?) set the mode for the rest of the pattern. For example, the pattern #\"(?i)yo\"
matches the strings \u201cyo\u201d
, \u201cyO\u201d
, \u201cYo\u201d
, and \u201cYO\u201d
.
Flags that can be used in Clojure regular-expression patterns, along with their long name and a description of what they do. See Java's documentation for the java.util.regex.Pattern class for more details.
Flag Flag Name Description d UNIX_LINES ., ^, and $ match only the Unix line terminator '\\n'. i CASE_INSENSITIVE ASCII characters are matched without regard to uppercase or lower-case. x COMMENTS Whitespace and comments in the pattern are ignored. m MULTILINE ^ and $ match near line terminators instead of only at the beginning or end of the entire input string. s DOTALL . matches any character including the line terminator. u UNICODE_CASE Causes the i flag to use Unicode case insensitivity instead of ASCII. The re-seq function is Clojure's regex workhorse. It returns a lazy seq of all matches in a string, which means it can be used to efficiently test whether a string matches or to find all matches in a string or a mapped file:
(re-seq #\"\\w+\" \"one-two/three\")\n;;=> (\"one\" \"two\" \"three\")\n
The preceding regular expression has no capturing groups, so each match in the returned seq is a string. A capturing group (subsegments that are accessible via the returned match object) in the regex causes each returned item to be a vector:
(re-seq #\"\\w*(\\w)\" \"one-two/three\")\n([\"one\" \"e\"] [\"two\" \"o\"] [\"three\" \"e\"])\n
"},{"location":"reference/standard-library/regular-expressions/#references","title":"References","text":"4Clojure #37 - regular expressions Regex in Clojure - purelyfunctional.tv
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/","title":"Common Regular Expression patterns","text":"Common string formats used in software development and examples of regular expressions to check their correctness.
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#username-regular-expression-pattern","title":"Username Regular Expression Pattern","text":"A 8 to 24 character passwords that can include any lower case character or digit (number). Only the underscore and dash special characters can be used.
(re-matches #\"^[a-z0-9_-]{8,24}$\" \"good-username\")\n
Breakdown the regex pattern:
^[a-z0-9_-]{8,24}$\n\n^ # Start of the line\n [a-z0-9_-] # Match characters and symbols in the list, a-z, 0-9 , underscore , hyphen\n {8,24} # Length at least 8 characters and maximum length of 24\n$ # End of the line\n
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#password-regular-expression-pattern","title":"Password Regular Expression Pattern","text":"A password should be 8 to 24 character string with at least one digit, one upper case letter, one lower case letter and one special symbol, @#$%
.
(re-matches #\"((?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%]).{8,24})\" \"G00d @ username\")\n
The order of the grouping formulas does not matter
Breakdown the regex pattern:
((?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%]).{8,24})\n\n( # Start of group\n (?=.*\\d) # must contains one digit from 0-9\n (?=.*[a-z]) # must contains one lowercase characters\n (?=.*[A-Z]) # must contains one uppercase characters\n (?=.*[@#$%]) # must contains one special symbols in the list \"@#$%\"\n . # match anything with previous condition checking\n {8,24} # length at least 8 characters and maximum of 24\n) # End of group\n
?=
means apply the assertion condition, which is meaningless by itself and works in combination with others.
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#hexadecimal-color-code-regular-expression-pattern","title":"Hexadecimal Color Code Regular Expression Pattern","text":"The string must start with a #
symbol , follow by a letter from a
to f
, A
to Z
or a digit from 0
to 9
with a length of exactly 3 or 6.` This regular expression pattern is very useful for the Hexadecimal web colors code checking.
(re-matches #\"^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$\" \"#FFAABB\")\n
Breakdown the regex pattern:
^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$\n\n^ #start of the line\n # # must contain a \"#\" symbols\n ( # start of group #1\n [A-Fa-f0-9]{3} # any strings in the list, with length of 3\n | # ..or\n [A-Fa-f0-9]{6} # any strings in the list, with length of 6\n ) # end of group #1\n$ #end of the line\n
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#email-regular-expression-pattern","title":"Email Regular Expression Pattern","text":"The account side of an email address starts with _A-Za-z0-9-\\\\+
optional follow by .[_A-Za-z0-9-]
, ending with an @
symbol.
The domain starts with A-Za-z0-9-
, follow by first level domain, e.g .org
, .io
and .[A-Za-z0-9]
optionally follow by a second level domain, e.g. .ac.uk
, .com.au
or \\\\.[A-Za-z]{2,}
, where second level domain must start with a dot .
and length must equal or more than 2 characters.
(re-matches\n #\"^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$\"\n \"jenny.jenn@jetpack.com.au\")\n
Breakdown the regex pattern:
^[_A-Za-z0-9-]+(\\\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9]+(\\\\.[A-Za-z0-9]+)*(\\\\.[A-Za-z]{2,})$\n\n^ #start of the line\n [_A-Za-z0-9-]+ # must start with string in the bracket [ ], must contains one or more (+)\n ( # start of group #1\n \\\\.[_A-Za-z0-9-]+ # follow by a dot \".\" and string in the bracket [ ], must contains one or more (+)\n )* # end of group #1, this group is optional (*)\n @ # must contains a \"@\" symbol\n [A-Za-z0-9]+ # follow by string in the bracket [ ], must contains one or more (+)\n ( # start of group #2 - first level TLD checking\n \\\\.[A-Za-z0-9]+ # follow by a dot \".\" and string in the bracket [ ], must contains one or more (+)\n )* # end of group #2, this group is optional (*)\n ( # start of group #3 - second level TLD checking\n \\\\.[A-Za-z]{2,} # follow by a dot \".\" and string in the bracket [ ], with minimum length of 2\n ) # end of group #3\n$ #end of the line\n
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#hintdouble-escaping-special-characters","title":"Hint::Double escaping special characters","text":"Double escaping of special characters is not required in the Clojure syntax.
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#image-file-name-and-extension-regular-expression-pattern","title":"Image File name and Extension Regular Expression Pattern","text":"A file extension name is 1 or more characters without white space, follow by dot .
and string end in jpg
or png
or gif
or bmp
. The file name extension is case-insensitive.
Change the combination (jpg|png|gif|bmp)
for other file extension.
(re-matches #\"(?i)([^\\s]+(\\.(jpg|png|gif|bmp))$)\" \"clojure-logo.png\")\n
Breakdown the regex pattern:
([^\\s]+(\\.(?i)(jpg|png|gif|bmp))$)\n\n( #Start of the group #1\n [^\\s]+ # must contains one or more anything (except white space)\n ( # start of the group #2\n \\. # follow by a dot \".\"\n (?i) # ignore the case sensitive checking\n ( # start of the group #3\n jpg # contains characters \"jpg\"\n | # ..or\n png # contains characters \"png\"\n | # ..or\n gif # contains characters \"gif\"\n | # ..or\n bmp # contains characters \"bmp\"\n ) # end of the group #3\n ) # end of the group #2\n $ # end of the string\n) #end of the group #1\n
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#hintin-line-modifiers-not-supported-in-javascript","title":"Hint::in-line modifiers not supported in JavaScript","text":"The REPL above uses ClojureScript, hosted on JavaScript. JavaScript does not support in-line modifier flags such as (?i)
for a case insensitive pattern. In-line flags will be converted by the ClojureScript reader if they are the first element in the literal regular expression pattern, or if the js/RegExp
function is used to create the regular expression.
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#ip-address-regular-expression-pattern","title":"IP Address Regular Expression Pattern","text":"An IP address comprises of 4 groups of numbers between 0 and 255, with each group separated by a dot.
Example IP address are: 192.168.0.1
, 127.0.0.1
, 192.120.240.100
(re-matches\n #\"^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])$\"\n \"192.168.0.1\")\n
Breakdown the regex pattern:
^([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])\\\\.([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])\\\\.\n([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])\\\\.([01]?\\\\d\\\\d?|2[0-4]\\\\d|25[0-5])$\n\n^ #start of the line\n ( # start of group #1\n [01]?\\\\d\\\\d? # Can be one or two digits. If three digits appear, it must start either 0 or 1\n # e.g ([0-9], [0-9][0-9],[0-1][0-9][0-9])\n | # ...or\n 2[0-4]\\\\d # start with 2, follow by 0-4 and end with any digit (2[0-4][0-9])\n | # ...or\n 25[0-5] # start with 2, follow by 5 and end with 0-5 (25[0-5])\n ) # end of group #2\n \\. # follow by a dot \".\"\n.... # repeat with 3 time (3x)\n$ #end of the line\n
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#time-format-regular-expression-pattern","title":"Time Format Regular Expression Pattern","text":"Time in 12-Hour Format Regular Expression Pattern. The 12-hour clock format start between 0-12, then a semi colon, :
, follow by 00-59
. The pattern ends with am
or pm
.
(re-matches #\"(?i)(1[012]|[1-9]):[0-5][0-9](\\s)?(am|pm)\" \"12:59am\")\n
Breakdown the regex pattern:
(1[012]|[1-9]):[0-5][0-9](\\\\s)?(?i)(am|pm)\n\n( #start of group #1\n 1[012] # start with 10, 11, 12\n | # or\n [1-9] # start with 1,2,...9\n) #end of group #1\n : # follow by a semi colon (:)\n [0-5][0-9] # follow by 0..5 and 0..9, which means 00 to 59\n (\\\\s)? # follow by a white space (optional)\n (?i) # next checking is case insensitive\n (am|pm) # follow by am or pm\n
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#time-in-24-hour-format-regular-expression-pattern","title":"Time in 24-Hour Format Regular Expression Pattern","text":"The 24-hour clock format start between 0-23 or 00-23, then a semi colon :
and follow by 00-59.
(re-matches #\"(([01]?[0-9]|2[0-3]):[0-5][0-9])\" \"23:58\")\n
Breakdown the regex pattern:
([01]?[0-9]|2[0-3]):[0-5][0-9]\n\n( #start of group #1\n [01]?[0-9] # start with 0-9,1-9,00-09,10-19\n | # or\n 2[0-3] # start with 20-23\n) #end of group #1\n : # follow by a semi colon (:)\n [0-5][0-9] # follow by 0..5 and 0..9, which means 00 to 59\n
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#date-format-ddmmyyyy-regular-expression-pattern","title":"Date Format (dd/mm/yyyy) Regular Expression Pattern","text":"Date format in the form dd/mm/yyyy
. Validating a leap year and if there is 30 or 31 days in a month is not simple though.
(re-matches #\"(0?[1-9]|[12][0-9]|3[01])/(0?[1-9]|1[012])/((19|20)\\d\\d)\" \"20/02/2020\")\n
Breakdown the regex pattern:
(0?[1-9]|[12][0-9]|3[01])/(0?[1-9]|1[012])/((19|20)\\\\d\\\\d)\n\n( #start of group #1\n 0?[1-9] # 01-09 or 1-9\n | # ..or\n [12][0-9] # 10-19 or 20-29\n | # ..or\n 3[01] # 30, 31\n) #end of group #1\n / # follow by a \"/\"\n ( # start of group #2\n 0?[1-9] # 01-09 or 1-9\n | # ..or\n 1[012] # 10,11,12\n ) # end of group #2\n / # follow by a \"/\"\n ( # start of group #3\n (19|20)\\\\d\\\\d # 19[0-9][0-9] or 20[0-9][0-9]\n ) # end of group #3\n
"},{"location":"reference/standard-library/regular-expressions/common-regex-patterns/#reference","title":"Reference","text":" - https://mkyong.com/regular-expressions/10-java-regular-expression-examples-you-should-know
"},{"location":"reference/standard-library/regular-expressions/matching-sub-sequences/","title":"Matching sub sequences","text":""},{"location":"reference/standard-library/regular-expressions/matching-sub-sequences/#matching-sub-sequences","title":"Matching sub-sequences","text":"re-seq
returns a lazy seq of all of the matches. The elements of the seq are the results that re-find
would return.
(re-seq #\"s+\" \"Helloween\")\n
"},{"location":"reference/standard-library/regular-expressions/matching-sub-sequences/#most-common-word","title":"Most common word","text":"re-seq
is used in the most common word challenge to split a string into individual words.
Extract from Project Guttenburg the text of The importance of being Earnest by Oscar Wilde. This returns a string of the whole book.
The book is broken down into a collection of individual words using re-seq
and a regular expression pattern.
The collection of words is converted to lower case, so that The
and the
are not counted as separate words. frequencies
returns a collection of tuples, each tuple being a word and a value representing how often it occurs. This collection is sorted by the value in descending order to show the word with the most occurrences at the top.
(->> (slurp \"http://www.gutenberg.org/cache/epub/844/pg844.txt\")\n (re-seq #\"[a-zA-Z0-9|']+\")\n (map #(clojure.string/lower-case %))\n frequencies\n (sort-by val dec))\n
TODO: add link to complete example.
"},{"location":"reference/standard-library/regular-expressions/matching-sub-strings/","title":"Matching sub strings","text":""},{"location":"reference/standard-library/regular-expressions/matching-sub-strings/#matching-sub-strings","title":"Matching sub-strings","text":"re-find
returns the first match within the string, using return values similar to re-matches.
nil
is returned when the pattern does not find a match.
(re-find #\"pump\" \"Halloween\")\n
A matching pattern without groups returns the matched string
(re-find #\"e+\" \"Halloween\")\n
Match with groups returns a vector of results
(re-find #\"s+(.*)(s+)\" \"success\")\n
"},{"location":"reference/standard-library/regular-expressions/matching-with-groups/","title":"Matching with regex groups","text":"rematches
takes a pattern and compares it with a string.
If the pattern does not match the string then nil
is returned to show the function returned a false value.
(re-matches #\"pumpkin\" \"Halloween pumpkin\")\n ```\n\nIf there is an exact match and there are no groups (parens) in the regex, then the matched string is returned.\n\n```clojure\n(re-matches #\"pumpkin\" \"pumpkin\")\n
If the pattern matches but there are groups, a vector of matching strings is returned. The first element in the vector is the entire match. The remaining elements are the group matches.
(re-matches #\"Halloween(.*)\" \"Halloween pumpkin\")\n
"},{"location":"reference/standard-library/regular-expressions/string-replace-with-regex/","title":"String replace with regex","text":""},{"location":"reference/standard-library/regular-expressions/string-replace-with-regex/#string-replace-with-regex-pattern","title":"String replace with regex pattern","text":"clojure.string/replace
takes a string, a pattern and a substring that will replace matching patterns.
(clojure.string/replace \"mississippi\" #\"i..\" \"obb\")\n
Groups can be referred to in the substring replacement
(clojure.string/replace \"mississippi\" #\"(i)\" \"$1$1\")\n
Replace with the value of a function applied to the match:
(clojure.string/replace \"mississippi\" #\"(.)i(.)\"\n (fn [[_ b a]]\n (str (clojure.string/upper-case b)\n \"--\"\n (clojure.string/upper-case a))))\n \"M--SS--SS--Ppi\"\n
clojure.string/replace-first
is a variation where just the first occurrence is replaced.
"},{"location":"reference/standard-library/regular-expressions/string-split-with-regex/","title":"String split with regex","text":""},{"location":"reference/standard-library/regular-expressions/string-split-with-regex/#string-splitting-using-a-regex-pattern","title":"String splitting using a regex pattern","text":"clojure.string/split
takes a string to be split and a pattern to split the string with.
(clojure.string/split \"This is a string that I am splitting.\" #\"\\s+\")\n [\"This\" \"is\" \"a\" \"string\" \"that\" \"I\" \"am\" \"splitting.\"]\n
"},{"location":"reference/standard-library/regular-expressions/string-split-with-regex/#most-common-words-example","title":"Most common words example","text":"Extract a list of the most commonly used English words, returned as a string of words that are separated by a comma.
The #\",\"
regex pattern splits the string of words to form a collection of individual words, each word being its own string.
(def common-english-words\n (set\n (clojure.string/split\n (slurp\n \"http://www.textfixer.com/resources/common-english-words.txt\")\n #\",\")))\n
TODO: add link to complete example.
"},{"location":"reference/standard-library/regular-expressions/sub-expression-matches/","title":"Sub-expression Matches","text":"Pattern Description ^ Matches beginning of line. $ Matches end of line. . Matches any single character except newline. Using m option allows it to match newline as well. [...] Matches any single character in brackets. [^...] Matches any single character not in brackets \\A Beginning of entire string \\z End of entire string \\Z End of entire string except allowable final line terminator. re* Matches 0 or more occurrences of preceding expression. re+ Matches 1 or more of the previous thing re? Matches 0 or 1 occurrence of preceding expression. re{ n} Matches exactly n number of occurrences of preceding expression. re{ n,} Matches n or more occurrences of preceding expression. re{ n, m} Matches at least n and at most m occurrences of preceding expression. a b (re) Groups regular expressions and remembers matched text. (?: re) Groups regular expressions without remembering matched text. (?> re) Matches independent pattern without backtracking. \\w Matches word characters. \\W Matches nonword characters. \\s Matches whitespace. Equivalent to [\\t\\n\\r\\f]. \\S Matches nonwhitespace. \\d Matches digits. Equivalent to [0-9]. \\D Matches nondigits. \\A Matches beginning of string. \\Z Matches end of string. If a newline exists, it matches just before newline. \\z Matches end of string. \\G Matches point where last match finished. \\n Back-reference to capture group number \"n\" \\b Matches word boundaries when outside brackets. Matches backspace (0x08) when inside brackets. \\B Matches nonword boundaries. \\n, \\t, etc. Matches newlines, carriage returns, tabs, etc. \\Q Escape (quote) all characters up to \\E \\E Ends quoting begun with \\Q"},{"location":"reference/tagged-literals/","title":"Tagged Literals","text":"Frequently used value types are afforded a \"tagged literal\" syntax. It is similar to a constructor, but this special syntax makes it de/serializable and easier to read at the REPL.
Tagged literals start with a # followed by a symbol and a literal:
#js [...]
- JavaScript array literal
#js {...}
- JavaScript object literal
#inst \"...\"
- JavaScript date literal
#uuid \"...\"
- UUID literal
#queue [...]
- queue literal
"},{"location":"reference/tagged-literals/uuid/","title":"uuid tag literal","text":"A universally unique identifier (UUID).
#uuid \"8-4-4-4-12\" - numbers represent the number of hex digits\n#uuid \"97bda55b-6175-4c39-9e04-7c0205c709dc\" - actual example\n
Representing UUIDs with #uuid rather than just a plain string has the following benefits:
the reader will throw an exception on malformed UUIDs\nits UUID type is preserved and shown when serialized to edn.\n
"},{"location":"reference/tagged-literals/uuid/#creating-uuids-clojure","title":"Creating UUIDs - Clojure","text":"In Clojure, call the randomUUID method of the java.util.UUID class
(java.util.UUID/randomUUID)\n
This returns a UUID tagged literal.
(java.util.UUID/randomUUID)\n;; => #uuid \"44f3ffd7-6702-4b8a-af25-11bee4b5ec4f\"\n
Looking at the type we can see its a Java object from the java.util.UUID class:
(type (java.util.UUID/randomUUID))\n;; => java.util.UUID\n
"},{"location":"reference/tagged-literals/uuid/#creating-uuids-clojurescript","title":"Creating UUIDs - ClojureScript","text":"Randomly generate a UUID in ClojureScript:
cljs.core/random-uuid
To label a value as a UUID:
cljs.core/uuid
"},{"location":"reference/tagged-literals/uuid/#hintuuid-does-not-validate-the-value","title":"Hint::uuid does not validate the value","text":"The ClojureScript documentation states that uuid? does not perform validation.
"},{"location":"reference/tagged-literals/uuid/#testing-for-a-uuid","title":"Testing for a uuid","text":"uuid?
tests a given value and returns true if it is a uuid tagged literal value.
tagged-literal?
is the more general function for any tagged values.
"},{"location":"simple-projects/","title":"Small Projects","text":"An effective way to get comfortable with Clojure is to start writing small projects. In this section several small projects are used to walk the audience through how to create and develop a project, as well as learn some Clojure functions and functional programming techniques along the way.
Project Topics Description Random Clojure Function namespace vars print a random function from the Clojure standard library Encoding and decoding hash-map dictionaries transforming messages between one form and another Data Transformation transforming larger and larger data sets Test Driven Development and Kata Unit testing Unit testing and solving challenges using different approaches Create a Clojure project
Clojure CLI tools and clj-new to create a new Clojure project.
"},{"location":"simple-projects/generate-web-page/","title":"Generate Web Page","text":"Generate a web page from Clojure code, using Hiccup
Generate a full HTML webpage with content.
Add a CSS library (bulma.io, bootstrap) to improve generation
"},{"location":"simple-projects/generate-web-page/#summary","title":"Summary","text":"Generating a web page in Clojure shows how easy it is to structure data and transform that data into other structures.
Although this kind of project is easy enough to just do in a REPL directly, using a Clojure aware editor with a Clojure project makes changes to the code far simpler, without loosing any of the immediate feedback of the REPL.
Most Clojure developers use the REPL by evaluating code in the editor showing the source code from the project.
Practicalli Web Services book shows how to build websites, create self-documented API's, manage Web Application servers and use databases to persist data.
"},{"location":"simple-projects/random-clojure-function/","title":"Random Clojure Function","text":"A simple application that returns a random function from the clojure.core
namespace, along with the function argument list and its description (from the doc-string)
There are 659 functions in clojure.core
namespace and 955 in the standard library (as of June 2020). These functions are learned over time as experience is gained with Clojure.
Project: Random Clojure function practicalli/random-clojure-function repository contains a Clojure project with an example solution
"},{"location":"simple-projects/random-clojure-function/#live-coding-video-walk-through","title":"Live Coding Video walk-through","text":"A Live coding video walk-through of this project shows how this application was developed, using Spacemacs editor and CircleCI for continuous integration.
-M flag superseeds -A flag The -M
flag replaced the -A
flag when running code via clojure.main
, e.g. when an alias contains :main-opts
. The -A
flag should be used only for the specific case of including an alias when starting the Clojure CLI built-in REPL.
"},{"location":"simple-projects/random-clojure-function/#create-a-project","title":"Create a project","text":"Use the :project/create
Practicalli Clojure CLI Config to create a new Clojure project.
clojure -T:project/create :template app :name practicalli/random-function\n
This project has a deps.edn
file that includes the aliases
:test
- includes the test/
directory in the class path so unit test code is found :runner
to run the Cognitect Labs test runner which will find and run all unit tests
"},{"location":"simple-projects/random-clojure-function/#repl-experiments","title":"REPL experiments","text":"Open the project in a Clojure-aware editor or start a Rebel terminal UI REPL
Clojure EditorRebel REPL Open the src/practicalli/random-function.clj
file in a Clojure aware editor and start a REPL process (jack-in)
Optionally create a rich comment form that will contain the expressions as the design of the code evolves, moving completed functions out of the comment forms so they can be run by evaluating the whole namespace.
(ns practicalli.random-function)\n\n(comment\n ;; add experimental code here\n)\n
Open a terminal and change to the root of the Clojure project created, i.e. the directory that contains the deps.edn
file.
Start a REPL that provides a rich terminal UI
clojure -M:repl/reloaded\n
require
will make a namespace available from within the REPL (require '[practicalli.random-function])\n
Change into the random-function
namespace to define functions (in-ns 'practicalli.random-function')\n
Reload changes made to the src/practicalli/random_function.clj
file using the require
function with the :reload
option. :reload
forces the loading of all the definitions in the namespace file, overriding any functions of the same name in the REPL. (require '[practicalli.random-function] :reload)\n
Copy finished code into the source code files
Assuming the code should be kept after the REPL is closed, save the finished versions of function definitions into the source code files. Use Up and Down keys at the REPL prompt to navigate the history of expressions
List all the public functions in the clojure.core
namespace using the ns-publics
function
(ns-publics 'clojure.core)\n
The hash-map keys are function symbols and the values are the function vars
{+' #'clojure.core/+',\n decimal? #'clojure.core/decimal?,\n sort-by #'clojure.core/sort-by,\n macroexpand #'clojure.core/macroexpand\n ,,,}\n
The meta
function will return a hash-map of details about a function, when given a function var.
(meta #'map)\n
The hash-map has several useful pieces of information for the application, including :name
, :doc
, and :arglists
;; => {:added \"1.0\",\n;; :ns #namespace[clojure.core],\n;; :name map,\n;; :file \"clojure/core.clj\",\n;; :static true,\n;; :column 1,\n;; :line 2727,\n;; :arglists ([f] [f coll] [f c1 c2] [f c1 c2 c3] [f c1 c2 c3 & colls]),\n;; :doc\n;; \"Returns a lazy sequence consisting of the result of applying f to\\n the set of first items of each coll, followed by applying f to the\\n set of second items in each coll, until any one of the colls is\\n exhausted. Any remaining items in other colls are ignored. Function\\n f should accept number-of-colls arguments. Returns a transducer when\\n no collection is provided.\"}\n
To use the meta
function, the values from the ns-publics
results should be used.
(vals (ns-publics 'clojure.core))\n
rand-nth
will return a random function var from the sequence of function vars
(rand-nth (vals (ns-publics 'clojure.core)))\n
A single function var is returned, so then the specific meta data can be returned.
(meta (rand-nth (vals (ns-publics 'clojure.core))))\n
"},{"location":"simple-projects/random-clojure-function/#define-a-name-for-all-functions","title":"Define a name for all functions","text":"Edit the src/practicalli/random-function.clj
file and define a name for the collection of all public functions from clojure.core
(def standard-library\n \"Fully qualified function names from clojure.core\"\n (vals (ns-publics 'clojure.core)))\n
"},{"location":"simple-projects/random-clojure-function/#write-unit-tests","title":"Write Unit Tests","text":"From the REPL experiments we have a basic approach for the application design, so codify that design by writing unit tests. This will also highlight regressions during the course of development.
Edit the file test/practicalli/random_function_test.clj
and add unit tests.
The first test check the standard-library-functions contains entries.
The second test checks the -main function returns a string (the function name and details).
src/practicalli/random_function_test.clj(ns practicalli.random-function-test\n (:require [clojure.test :refer [deftest is testing]]\n [practicalli.random-function :as random-fn]))\n\n(deftest standard-library-test\n (testing \"Show random function from Clojure standard library\"\n (is (seq random-fn/standard-library-functions))\n (is (string? (random-fn/-main)))))\n
"},{"location":"simple-projects/random-clojure-function/#update-the-main-function","title":"Update the main function","text":"Edit the src/practicalli/random-function.clj
file. Change the -main
function to return a string of the function name and description.
src/practicalli/random-function.clj(defn -main\n \"Return a function name from the Clojure Standard library\"\n [& args]\n (let [function-details (meta (rand-nth standard-library-functions))]\n (str (function-details :name) \"\\n \" (function-details :doc)))\n )\n
Cognitect Test Runner Run the tests with the Congnitect test runner via the test
function in the build.clj
file. ```shell clojure -T:build test
```
Kaocha Test Runner Run the tests with the Kaocha test runner using the alias :test/run
from Practicalli Clojure CLI config ```shell clojure -M:test/run
```
"},{"location":"simple-projects/random-clojure-function/#running-the-application","title":"Running the application","text":"Use the clojure command with the main namespace of the application. Clojure will look for the -main function and evaluate it.
clojure -M -m practicalli.random-function\n
This should return a random function name and its description. However, nothing is returned. Time to refactor the code.
"},{"location":"simple-projects/random-clojure-function/#improving-the-code","title":"Improving the code","text":"The tests pass, however, no output is shown when the application is run.
The main function returns a string but nothing is sent to standard out, so running the application does not return anything.
The str
expression could be wrapped in a println, although that would make the result harder to test and not be very clean code. Refactor the -main
to a specific function seems appropriate.
Replace the -main-test
with a random-function-test
that will be used to test a new function of the same name which will be used for retrieving the random Clojure function.
(deftest random-function-test\n (testing \"Show random function from Clojure standard library\"\n (is (seq SUT/standard-library-functions))\n (is (string? (SUT/random-function SUT/standard-library-functions)))))\n
Create a new function to return a random function from a given collection of functions, essentially moving the code from -main
.
The function extracts the function :name
and :doc
from the metadata of the randomly chosen function.
(defn random-function\n [function-list]\n (let [function-details (meta (rand-nth function-list))]\n (str (function-details :name) \"\\n \" (function-details :doc) \"\\n \")))\n
Update the main function to call this new function.
(defn -main\n \"Return a function name from the Clojure Standard library\"\n [& args]\n (println (random-function standard-library-functions)))\n
Run the tests again.
If the tests pass, then run the application again
clojure -M -m practicalli.random-function\n
A random function and its description are displayed.
"},{"location":"simple-projects/random-clojure-function/#adding-the-function-signature","title":"Adding the function signature","text":"Edit the random-function
code and add the function signature to the string returned by the application.
Format the code so it is in the same structure of the output it produces, making the code clearer to understand.
(defn random-function\n [function-list]\n (let [function-details (meta (rand-nth function-list))]\n (str (function-details :name)\n \"\\n \" (function-details :doc)\n \"\\n \" (function-details :arglists))))\n
"},{"location":"simple-projects/random-clojure-function/#add-more-namespaces","title":"Add more namespaces","text":"All current namespaces on the classpath can be retrieved using the all-ns
function. This returns a lazy-seq, (type (all-ns))
(all-ns)\n
Using the list of namespace the ns-publics
can retrieve all functions across all namespaces.
Create a helper function to get the functions from a namespace, as this is going to be used in several places.
(defn function-list\n [namespace]\n (vals (ns-publics namespace)))\n
This function can be mapped over all the namespaces to get a sequence of all function vars. Using map
creates a sequence for each namespace, returned as a sequence of sequences. Using mapcat
will concatenate the nested sequences and return a flat sequence of function vars.
(mapcat #(vals (ns-publics %)) (all-ns))\n
Bind the results of this expression to the name all-public-functions
.
(def available-namespaces\n (mapcat #(vals (ns-publics %)) (all-ns)))\n
"},{"location":"simple-projects/random-clojure-function/#control-which-namespaces-are-consulted","title":"Control which namespaces are consulted","text":"There is no way to control which library we get the functions from, limiting the ability of our application.
Refactor the main namespace to act differently based on arguments passed:
-
If no arguments are passed then all public functions are used to pull a random function from.
-
If any argument is passed, the argument should be used as the namespace to pull a random function from. The argument is assumed to be a string.
ns-publics
function needs a namespace as a symbol, so the symbol
function is used to convert the argument.
(symbol \"clojure.string\")\n
The -main
function uses [& args]
as a means to take multiple arguments. All arguments are put into a vector, so the symbol function should be mapped over the elements in the vector to create symbols for all the namespaces.
Use an anonymous function to convert the arguments to symbols and retrieve the list of public functions from each namespace. This saves mapping over the arguments twice.
mapcat
the function-list function over all the namespaces, converting each namespace to a symbol.
(mapcat #(function-list (symbol %)) args)\n
Update the main function with an if statement. Use seq
as the condition to test if a sequence (the argument vector) has any elements (namespaces to use).
If there are arguments, then get the functions for the specific namespaces.
Else return all the functions from all the namespaces.
(defn -main\n \"Return a function name from the Clojure Standard library\"\n [& args]\n (if (seq args)\n (println (random-function (mapcat #(function-list (symbol %)) args)))\n (println (random-function standard-library-functions))))\n
"},{"location":"simple-projects/random-clojure-function/#use-the-fully-qualified-name-for-the-namespace","title":"Use the fully qualified name for the namespace","text":"Now that functions can come from a range of namespaces, the fully qualified namespace should be used for the function, eg. domain/namespace
(:ns (meta (rand-nth standard-library-functions)))\n
Update the random function to return the domain part of the namespace, separated by a /
(defn random-function\n [function-list]\n (let [function-details (meta (rand-nth function-list))]\n (str (function-details :ns) \"/\" (function-details :name)\n \"\\n \" (function-details :arglists)\n \"\\n \" (function-details :doc))))\n
"},{"location":"simple-projects/random-clojure-function/#use-all-available-namespaces-by-default","title":"Use all available namespaces by default","text":"Define a name to represent the collection of all available namespaces, in the context of the running REPL.
(def all-public-functions\n \"Fully qualified function names from available\"\n (mapcat #(vals (ns-publics %)) (all-ns)))\n
Update the -main
function to use all available namespaces if no arguments are passed to the main function.
(defn -main\n \"Return a random function and its details\n from the available namespaces\"\n [& args]\n (if (seq args)\n (println (random-function (mapcat #(function-list (symbol %)) args)))\n (println (random-function all-public-functions))))\n
"},{"location":"simple-projects/random-clojure-function/#follow-on-idea-convert-to-a-web-service","title":"Follow-on idea: Convert to a web service","text":"Add http-kit server and send information back as a plain text, html, json and edn
"},{"location":"simple-projects/random-clojure-function/#follow-on-idea-convert-to-a-library","title":"Follow-on idea: Convert to a library","text":"Convert the project to a library so this feature can be used as a development tool for any project.
Add functionality to list all functions from all namespaces or a specific namespace, or functions from all namespaces of a particular domain, e.g practicalli
or practicalli.app
"},{"location":"simple-projects/split-the-bill/","title":"Split the bill","text":"In a restaurant a group of friends and relatives are having a reunion dinner after a year of not seeing each other.
Once the meal comes to an end, its time to pay the bill. So how would you write code to split the bill?
Start with the simplest possible approach, with everyone paying the same.
"},{"location":"simple-projects/split-the-bill/#create-a-new-clojure-project","title":"Create a new Clojure project","text":" Pracitcalli Clojure CLI Config provides the :project/create
alias to create projects using deps-new project.
clojure -T:project/create :template app :name practicalli/split-the-bill\n
(str \"Create code to calculate the bill, including what each person should pay\")\n
Tke a look at the Who am I section for ideas on how to model the bill. Also look at More Than Average for ideas on how to write code to work out how to pay the bill.
"},{"location":"simple-projects/split-the-bill/#paying-what-was-ordered","title":"Paying what was ordered","text":"As not everyone had eaten the same amount of food or arrived at the same time, then there was an ask for everyone to pay just what they ordered.
So create a collection to capture what each person ordered and create an itemised bill so each person knows what they should pay.
Define a detailed bill based on what each person ordered, then create an itemised bill based on each persons order
Now it was realised that what everyone ordered is not what everyone ate. So now we need to take the order and create an itemised bill based on what everyone actually ate (lets suspend believe here a little and assume everyone knows exactly what they ate, and is honest about it).
Define a detailed bill based on what each person ordered, then create an itemised bill based on each person actually ate
"},{"location":"simple-projects/split-the-bill/#spliting-the-bill-with-a-social-group","title":"Spliting the bill with a Social Group","text":"Extend the exercise by splitting bills over multiple events and activities with multiple people.
"},{"location":"simple-projects/tripple-lock/","title":"Triple Lock","text":"A new safe too keep all the richest you will gain from becoming a Clojure developer (hopefully). The safe has a 3 combination lock to protect your new found wealth, but just how safe is the safe?
"},{"location":"simple-projects/tripple-lock/#create-a-new-clojure-project","title":"Create a new Clojure project","text":" Pracitcalli Clojure CLI Config provides the :project/create
alias to create projects using deps-new project.
clojure -T:project/create :template app :name practicalli/triple-lock\n
"},{"location":"simple-projects/tripple-lock/#designing-the-combination-lock","title":"Designing the combination lock","text":"Lets consider how we would create such a combination lock in Clojure.
- The combination is managed by three tumbler wheels
- Each tumbler wheel has the same range of numbers on then, 0 to 9
Each tumbler wheel could have all the numbers it contains within a Collection in Clojure. The simplest approach would be to put the numbers 0 to 9 into a Vector (an array-like collection).
[0 1 2 3 4 5 6 7 8 9]\n
As the numbers on the tumbler wheel are just a range between 0 and 9, then rather than type out all the numbers we can use the range
function to generate all the numbers for us.
When we give the range function one argument, it will create all the whole numbers from 0 to the number before that of the argument. In the following example, we give range
the argument of 10 and we receive the numbers from 0 to 9.
(range 10)\n
You can also give range
two arguments, such as '(range 5 15)'.
Be careful not to call the range
function by itself, or it will try and generate an infinite range of numbers (until your computer memory is all used up).
"},{"location":"simple-projects/tripple-lock/#create-all-the-combinations","title":"Create all the Combinations","text":"Complete the following code (replacing the ,,,) to generate all the possible combinations of the lock
(for [tumbler-1 (range 10)\n ,,, ,,,\n ,,, ,,,]\n [tumbler-1 ,,, ,,,])\n
Instead of showing all the possible combinations, count all the combinations and return the total number of combinations
Take the code from the combinations and wrap it in the count function
;; now count the possible combinations\n(count\n\n )\n
To make our lock harder to break into, we should only allow the combinations where each tumbler wheel has a different number. So you cannot have combinations like 1-1-1, 1-2-2, 1-2-1, etc.
How many combinations does that give us?
Complete the following code to create a 3-tumbler wheel combination lock, where none of the numbers are the same
Hint: Beware not to enter (range) without an argument as Clojure may try and evaluate infinity
(count (for [tumbler-1 (range 10)\n ,,, ,,,\n ,,, ,,,\n :when (or (= tumbler-1 tumbler-2)\n ,,,\n ,,,)]\n [tumbler-1 ,,, ,,,]))\n
Suggested solution Suggested solution to the completed 3-lock challenges.
;; a 3 combination padlock\n\n;; model the combinations\n(for [tumbler-1 (range 10)\n tumbler-2 (range 10)\n tumbler-3 (range 10)]\n [tumbler-1 tumbler-2 tumbler-3])\n\n\n;; now count the possible combinations\n(count (for [tumbler-1 (range 10)\n tumbler-2 (range 10)\n tumbler-3 (range 10)]\n [tumbler-1 tumbler-2 tumbler-3]))\n\n\n(count (for [tumbler-1 (range 10)\n tumbler-2 (range 10)\n tumbler-3 (range 10)\n :when (or (= tumbler-1 tumbler-2)\n (= tumbler-2 tumbler-3)\n (= tumbler-3 tumbler-1))]\n [tumbler-1 tumbler-2 tumbler-3]))\n\n;; lets look at the combinations again, we can see that there is always at least 2 matching values. This is probably the opposite of what we want in real life.\n(for [tumbler-1 (range 10)\n tumbler-2 (range 10)\n tumbler-3 (range 10)\n :when (or (= tumbler-1 tumbler-2)\n (= tumbler-2 tumbler-3)\n (= tumbler-3 tumbler-1))]\n [tumbler-1 tumbler-2 tumbler-3])\n
"},{"location":"simple-projects/data-transformation/","title":"Data Transformation","text":"In a sense all Clojure project are about data transformation, however, these projects will introduce you to many techniques used to transform larger and larger data sets.
Project Topics Overview Most common word regex filter re-seq sort-by Find the most common word in a give book that is not a common English word"},{"location":"simple-projects/data-transformation/most-common-word/","title":"Most common word","text":"In this challenge we would like you to find the most used word in a book. The word should not be part of the common English words (i.e. the, a, i, is).
This functionality is useful for generating word maps or identifying patterns across data sets.
Copyright free books for use are available via Project Guttenburg, e.g. \u201cThe Importance of Being Earnest\u201d by Oscar Wilde.
"},{"location":"simple-projects/data-transformation/most-common-word/#suggested-approach","title":"Suggested approach","text":"A suggested approach to find the most common word:
- Pull the content of the book into a collection
- Use a regular expression to create a collection of individual words - eg.
#\u201d[a-zA-Z0-9|\u2019]+\u201d
- Remove the common English words used in the book
- Convert all the words to lower case so they match with common words source
- Count the occurrences of the remaining words (eg. each word is associated with the number of times it appears in the book)
- Sort the words by the number of the occurrences
- Reverse the collection so the most commonly used word is shown first
"},{"location":"simple-projects/data-transformation/most-common-word/#create-a-project","title":"Create a project","text":" Pracitcalli Clojure CLI Config provides the :project/create
alias to create projects using deps-new project.
clojure -T:project/create :template app :name practicalli/common-words\n
"},{"location":"simple-projects/data-transformation/most-common-word/#get-the-book-contents","title":"Get the book contents","text":"clojure.core/slurp
will read in a local file or a remote resource (file, web page, etc) and return a single string of the contents.
(slurp \"https://www.gutenberg.org/files/844/844-0.txt\")\n
Wrap the slurp
expression in a def
to bind a name to the book.
(def being-earnest (slurp \"https://www.gutenberg.org/files/844/844-0.txt\"))\n
Project Gutenberg now compresses the books with GZip, so a stream can be created to read the file and decompress it. Then slurp is used to read in the uncompressed text of the book into a string.
(def being-earnest\n (with-open [uncompress-text (java.util.zip.GZIPInputStream.\n (clojure.java.io/input-stream\n \"https://www.gutenberg.org/cache/epub/844/pg844.txt\"))]\n (slurp uncompress-text)))\n ```\n\n## Individual words from the book\n\nThe book contents should be broken down into individual words.\n\nA regular expression can be used to identify word boundaries, especially where there are apostrophes and other characters.\n\n`clojure.core/re-seq` returns a new lazy sequence containing the successive matches of a pattern from a given string. So given a sentence\n\nUsing `re-seq` to convert the first sentence of the `being-earnest` book using a regex word boundary pattern, `\\w+`.\n\n```clojure\n(re-seq #\"\\w+\" \"Morning-room in Algernon's flat in Half-Moon Street.\")\n\n;; => (\"Morning\" \"room\" \"in\" \"Algernon\" \"s\" \"flat\" \"in\" \"Half\" \"Moon\" \"Street\")\n
The result is a sequence of the individual words, however, the hyphenated words and the apostrophes have been split into separate words.
Extending the regex pattern the results can be refined.
(re-seq #\"[\\w|'-]+\" \"Morning-room in Algernon's flat in Half-Moon Street.\")\n\n;; => (\"Morning-room in Algernon's flat in Half-Moon Street\")\n
(re-seq #\"[\\w|'-]+\" being-earnest)\n
The #\"[\\w|'-]+\" is the same pattern as the more explicit pattern #\"[a-zA-Z0-9|'-]+\"
"},{"location":"simple-projects/data-transformation/most-common-word/#removing-common-english-words","title":"Removing common English words","text":"In any book the most common word its highly likely to be a common English word (the, a, and, etc.). To make the most common word in any book more specific, the common English words should be removed.
common-english-words.csv
contains comma separate words.
Using slurp and a regular expression the individual words can be extracted into a collection.
Rather than re-seq
the clojure.string/split
can be used. This is a more specific function for splitting a string using a regular expression pattern, in this case the pattern for a comma, #\",\"
.
(clojure.string/split (slurp \"common-english-words.csv\") #\",\")\n
An additional step is to place the common English words into a Clojure set, a data structure which contains a unique set of values.
(set (clojure.string/split (slurp \"common-english-words.csv\") #\",\"))\n
The advantage of using a set for the common English words is that the data structure can be used as a predicate to remove matching words. So a common English words set can be used to remove the common English words from being-earnest
book.
Define a name for the common English words set.
(def common-english-words (set (clojure.string/split (slurp \"common-english-words.csv\") #\",\")))\n
This can also be written using the threading macro, to show the sequential nature of the data transformation.
(def common-english-words\n (-> (slurp \"common-english-words.csv\")\n (clojure.string/split #\",\")\n set))\n
The common-english-words
set can now be used with the being-earnest
book.
(remove common-english-words (re-seq #\"[\\w|'-]+\" being-earnest))\n
"},{"location":"simple-projects/data-transformation/most-common-word/#counting-occurrences","title":"Counting Occurrences","text":"clojure.core/frequencies
takes a collection and returns a map where the keys are the unique elements from the collection and the value for each key is the number of times that element occurred in the collection.
(filter (remove common-english-words (re-seq #\"[\\w|'-]+\" being-earnest)))\n
The resulting hash-map is not in any order. clojure.core/sort-by
will return the same results but sorted by a given function. To sort a hash-map the key
and val
functions are function that will sort by key and value respectively. As it is the value that has the number of occurrences, then val
is the function to use.
(sort-by val (filter (remove common-english-words (re-seq #\"[\\w|'-]+\" being-earnest))))\n
The result is sorted from smallest to largest value. The result could be reversed using clojure.core/reverse
or by supplying an extra function to the sort-by
expression. Using greater-than, >
the result will be returned in descending order.
(sort-by val dec (filter (remove common-english-words (re-seq #\"[\\w|'-]+\" being-earnest))))\n
"},{"location":"simple-projects/data-transformation/most-common-word/#assembling-the-most-common-word-function","title":"Assembling the most-common-word function","text":"Define a function called most-common-word
that assembles all the previous steps. The function should take all the values it needs for the calculation as arguments, creating a pure function without side effects.
(defn most-common-word\n [book common-words]\n (sort-by val >\n (frequencies\n (remove common-words\n (map #(clojure.string/lower-case %)\n (re-seq #\"[\\w|'-]+\" book))))))\n
This may seem a little hard to parse, so the function definition can be re-written using a threading macro.
(defn most-common-word\n [book common-words]\n (->> book\n (re-seq #\"[\\w|'-]+\" ,,,)\n (map #(clojure.string/lower-case %))\n (remove common-words)\n frequencies\n (sort-by val >)))\n
Call this function with the being-earnest
book and the common-english-words
(most-common-word being-earnest common-english-words)\n
"},{"location":"simple-projects/data-transformation/most-common-word/#running-from-the-command-line","title":"Running from the command line","text":"Update the code to take the book reference from the command line.
Remove the def
that hard-coded the being-earnest book.
In the most-common-word
wrap the book with slurp
to read the book reference in and convert it to a string, to be processed by the rest of the expressions.
Add a -main
function that takes a reference for the source of the book and the source of the common words.
(ns practicalli.common-word)\n\n(defn decode-book\n [book-gzip]\n (with-open\n [uncompress-text (java.util.zip.GZIPInputStream.\n (clojure.java.io/input-stream book-gzip))]\n (slurp uncompress-text)))\n\n(defn common-words\n [csv]\n (-> (slurp csv)\n (clojure.string/split #\",\")\n set))\n\n(defn most-common-word\n [book-gzip common-words]\n (->> (decode book-gzip)\n (re-seq #\"[\\w|'-]+\")\n (map #(clojure.string/lower-case %))\n (remove common-words)\n frequencies\n (sort-by val >)))\n\n(defn -main\n [book-gzip common-word-csv]\n (most-common-word book-gzip (common-words common-word-csv)))\n
Now call the code on the command line.
clojure -m practicalli.common-word \"https://www.gutenberg.org/cache/epub/844/pg844.txt\" \"common-english-words.csv\"\n
"},{"location":"simple-projects/encode-decode/","title":"Encoding and Decoding with Clojure","text":"Projects that use a range of ciphers, from simple to more complex, to encode and decode text.
A common approach to encoding and decoding text is to use a dictionary lookup, defined in Clojure as a hash-map. Each key-value pair provides a mapping for encoding and decoding. Looking up a a character as a key in the map provides a value that is the encrypted character.
These projects show several ways to transform data in Clojure.
Project Topics Description Boolean names to 0 or 1 hash-map get Convert boolean values to classic 1 or 0 values Caesar cipher - ROT13 seq cycle zipmap A simple alphabet rotation cipher RNA / DNA converter Convert between DNA and RNA Clacks telegram Encoding and decoding messages with Clacks"},{"location":"simple-projects/encode-decode/#examples-of-encoding","title":"Examples of Encoding","text":" - Portable Network Graphics for image compression
- Vorbis for music and video compression plus several commercial compression encoders
- Enigma machine - encrypted communications
"},{"location":"simple-projects/encode-decode/caesar-cipher-rot13/","title":"Caesar Cipher ROT13","text":"ROT13 is one of the simplest ciphers which uses an alphabet as a circle of characters, swapping each character with a character 13 positions later in the alphabet, assuming 26 character of an English alphabet.
A dictionary can be generated to translate between the original alphabet and the rotated alphabet, providing a simple way to generate an encrypted message.
"},{"location":"simple-projects/encode-decode/caesar-cipher-rot13/#create-a-project","title":"Create a project","text":" Pracitcalli Clojure CLI Config provides the :project/create
alias to create projects using deps-new project.
clojure -T:project/create :template app :name practicalli/caesar-cipher\n
"},{"location":"simple-projects/encode-decode/caesar-cipher-rot13/#define-an-alphabet","title":"Define an alphabet","text":"Define an alphabet to use as a basis for conversion. Take the string of all characters and convert to a sequence of character types.
src/practicalli/caesar-cipher.clj(def english-alphabet\n (seq \"abcdefghijklmnopqrstuvwxyz\"))\n
"},{"location":"simple-projects/encode-decode/caesar-cipher-rot13/#generate-a-cypher","title":"Generate a cypher","text":"To convert a character, first build up a cypher. A cypher in this case is simply a hash-map that creates a dictionary lookup defining what each character should be changed to.
cycle
creates a lazy sequence of the alphabet that continually cycles. This provides an 'infinite' sequence from which we will take only the characters needed.
(cycle \"abcdefghijklmnopqrstuvwxyz\")\n
The dictionary is composed of the original alphabet and a new alphabet that is offset by 13 characters, half the number of characters in the dictionary.
(drop 13 (cycle alphabet))
will drop the first 13 characters. As cycle
creates an 'infinite' alphabet, there are still plenty of characters to make a second alphabet.
(take 26 (drop 13 (cycle alphabet)))
will get a new alphabet of 26 characters, starting from the 14th character, n
.
zipmap
is used to join two collections together to form a hash-map, e.g. the lookup dictionary. In this case the original alphabet and the new alphabet.
(zipmap alphabet (take 26 (drop 13 (cycle alphabet))))
This expression is nested and can be harder to parse by those new to Clojure. The code can be written using a threading marco, that demonstrated the flow of data transformation.
Using the thread last macro, ->>
, the result of each expression becomes the last argument for the next expression.
(->> (cycle alphabet)\n (drop 13)\n (take 26)\n (zipmap alphabet))\n
Using the clojure.core/replace function with the cypher hash-map and a string of text returns a converted string of text.
"},{"location":"simple-projects/encode-decode/caesar-cipher-rot13/#define-a-function","title":"Define a function","text":"Define a rot13
function with the algorithm created. The function takes the alphabet and the text to be encrypted. Passing both pieces of data as arguments ensures that the function is pure, i.e. free from side effects.
src/practicalli/caesar-cipher.clj(defn rot13 [alphabet text]\n (let [cipher (->> (cycle alphabet)\n (drop 13)\n (take 26)\n (zipmap alphabet))]\n (apply str (replace cipher text))))\n
Call the rot13 function with the english-alphabet
and a sentence as a string.
(rot13 english-alphabet \"The Quick Brown Fox Jumped Over The Lazy Dog!\")\n
An encrypted copy of the sentence is returned.
"},{"location":"simple-projects/encode-decode/caesar-cipher-rot13/#idiomatic-improvements","title":"Idiomatic improvements","text":"clojure.string
library is more idiomatic approach when working with string types.
In the practicalli.cypher-rot13
solution apply str
was used to join a sequence of characters into a string. clojure.string/join
combines a sequence of characters into a string.
Require the clojure.string
namespace to use the functions contained within. Add the require to the namespace definition of practicalli.cypher-rot13
src/practicalli/caesar-cipher.clj(ns practicalli.ceaser-cypher\n (:gen-class)\n (:require [clojure.string :as string]))\n
Update the rot13
function to use clojure.string/join
rather than apply str
.
(defn rot13 [alphabet text]\n (let [cipher (->> (cycle alphabet)\n (drop 13)\n (take 26)\n (zipmap alphabet))]\n (string/join (replace cipher text))))\n
"},{"location":"simple-projects/encode-decode/clacks/","title":"Clacks Messages","text":"In the 33rd Discworld novel, Going Postal, messages are sent faster than a speeding horse via the Clacks system. The Clacks system composes of a series of towers spread across a continent with each tower sending a light signal to the next tower using combinations of lights for each character in the message. Each tower sees a grid of lights from a distant tower and sends the message on to the next tower.
The Clacks system was introduced in the 24th Discworld novel called \"The Fifth Elephant\". \"Going Postal\" elaborates the full history of the Clacks system.
"},{"location":"simple-projects/encode-decode/clacks/#the-challenge","title":"The Challenge","text":"Create a Clacks encoder that converts any English language message into its corresponding clacks signal, based on the Clacks alphabet as defined by the board game of the same name.
The board game defines the alphabet as a 2 by 3 grid (although in the Discworld its actually 8 large squares). Naturally, the interpreter also converts the Clacks signal back into an English message too.
"},{"location":"simple-projects/encode-decode/clacks/#create-a-project","title":"Create a project","text":" Pracitcalli Clojure CLI Config provides the :project/create
alias to create projects using deps-new project.
clojure -T:project/create :template app :name practicalli/clacks-messenger\n
This project has a deps.edn
file that includes the aliases
:test
- includes the test/
directory in the class path so unit test code is found :runner
to run the Cognitect Labs test runner which will find and run all unit tests
"},{"location":"simple-projects/encode-decode/clacks/#representing-a-clack","title":"Representing a Clack","text":"For each clack, the light pattern is read from the top of the first column to the bottom, then from the top of the second column to the bottom. A light in a position represents a 1 value and no light represents a 0 value. This gives us our 6 number pattern for each clack in the alphabet.
The initial data structure chosen was essentially just modelling each individual clack. Since a clack is a 2x3 structure, the simplest way to represent a clacks is to have a vector that contains 2 vectors, each with three elements.
So a simple expression of the letter a in the clacks alphabet would be:
[[0 1 0][0 0 1]]\n
Therefore we could define a single letter of our alphabet as follows:
(def a [[0 1 0][0 0 1]])\n
Before defining the complete alphabet using this data structure, test this is the right data structure for the conversion process.
"},{"location":"simple-projects/encode-decode/clacks/#test-simple-conversion","title":"Test simple conversion","text":"Define a function to convert a single character into a clack:
(defn character->clack [character]\n (if (= character \"a\")\n a\n (str \"Sorry, character is not yet in the alphabet, please create a pull request\")))\n
Calling the function converts a string into the corresponding clack
(character->clack \"a\")\n
Clojure function naming
->
is a naming convention to indicate a function is specifically transforming to a data format. For example, json->clj-map
would be a generic function for transforming json to Clojure hash-map
The code is simple for a single character, however, would require a lot of redundant code to convert the whole alphabet. We would need either a deeply nested set of if statements or a very long cond
function, neither of which seems to be a particularly functional approach or idiomatic Clojure.
If a cond
statement was used, how would a clacks be converted back into a character?
So perhaps we need to change the data structure, one that provides an easy way to map to values together.
Also, there seems no value in mapping values to a 2x3 grid as long as we consistently express a clack.
"},{"location":"simple-projects/encode-decode/clacks/#define-the-alphabet","title":"Define the alphabet","text":"A hash map associates each key with a value and are used to create self-describing data. For example a person could be described as a hash-map
{:name \"Jenny Jetpack\" :age \"21\" :twitter \"jenjetpack\"}\n
Clojure keywords are often used for the keys because keywords can be used as a function with a map as an argument. This will return the value associated with the keyword in the map.
The new design for the clacks data structure associates a keyword of each letter of the alphabet with its corresponding clacks light pattern code
{:a [0 1 0 0 0 1]}\n
Test the design by defining enough letters for the clacks alphabet to convert some simple words, i.e bat
(def alphabet {:a [0 1 0 0 0 1]\n :b [0 0 1 0 1 0]\n :t [1 0 0 1 1 1]})\n
"},{"location":"simple-projects/encode-decode/clacks/#testing-the-map-design","title":"Testing the map design","text":"Use a keyword to lookup the value of its clack code
(:a alphabet)\n\n;; => [0 1 0 0 0 1]\n
Create a simple function to convert a single character to its Clacks representation, referred to a clack.
(defn character->clack [character]\n ((keyword character) alphabet))\n
The ->
character is part of the function name. This is a Clojure naming convention used when the function you are defining converts from one type to another.
And call the function as follows
(character->clack \"a\")\n\n;; => [0 1 0 0 0 1]\n
"},{"location":"simple-projects/encode-decode/clacks/#converting-a-word","title":"Converting a word","text":"Now we want to convert a whole word to a clacks sequence. It seemed the easiest way to convert a whole word was to convert each letter at a time using the map to look up each clack code, returning all the clacks codes in a sequence.
So we redefined the string->clacks
function to take in a whole word.
We used the map
function to apply a conversion function over each element in the word (each element of the string). This conversion function called clacksify
.
(defn clacksify [letter]\n (let [character (str letter)]\n (alphabet (keyword character))))\n\n(defn string->clacks [word]\n (map clacksify word))\n
Now we could convert any word that used the letters of our limited alphabet. We chose bat as a simple word.
(string->clacks \"bat\")\n
As we are passing a string and not a keyword to the clacksify
function, then we first convert the string to a keyword using the keyword
function.
"},{"location":"simple-projects/encode-decode/clacks/#converting-the-clack-to-a-string","title":"Converting the clack to a string","text":"Is there a simple way to look up a key given a value that is unique in the map?
All Clack codes are unique in the map, but there did not seem to be a simple expression to find the key when given a value.
We could have created a second mapping, however having two maps seemed redundant and a potential cause for silly bugs.
The answer was simple once we found it. As the clack codes are unique, they could be used as keys for the letter values, we just needed to swap the map around. Swapping a map's keys and values was done by writing a reverse-map
function.
(defn reverse-map\n \"Reverse the keys and value pairs in a map.\n Allows the map to be used to convert from a clack to a letter without defining a second map\"\n [m]\n (into {} (map (fn [[a b]] [b a]) m)))\n
So we defined the function declacksify
which takes a clack code and returns its corresponding character. The clack code returns the corresponding keyword rather than a character, so we use the name
function to convert the keyword into a character name.
(defn declacksify [clack]\n (name ((reverse-map alphabet) clack)))\n\n(defn clacks->string [clacks]\n (map declacksify clacks))\n
So calling these functions with a clacks
(declacksify [1 0 0 1 1 1])\n;; => \"t\"\n\n(clacks->string [[0 0 1 0 1 0] [0 1 0 0 0 1] [1 0 0 1 1 1]])\n;; => (\"b\" \"a\" \"t\")\n
At this point you may be thinking that using keywords to represent the characters of the alphabet may not be the most effective. Using keywords has required more code to be written, adding to the complexity of the solution.
"},{"location":"simple-projects/encode-decode/clacks/#tidy-output","title":"Tidy output","text":"clacks->string
function returns the right result, but not quite in the format required. Rather than a single string a sequence of characters is returned.
Using the map
function we can apply the str
function over the resulting characters to give a single string.
(defn clacks->string [clacks]\n(map str (map declacksify clacks)))\n
Using clojure.string/join
is a more idiomatic approach to converting a sequence of characters to a string
(require '[clojure.string :as string])\n\n(defn clacks->string [clacks]\n (string/join (map declacksify clacks)))\n
"},{"location":"simple-projects/encode-decode/clacks/#refactor-dictionary-design","title":"Refactor dictionary design","text":"Converting characters to keywords and back again seem redundant when characters themselves can be used as keys in a hash-map.
Using keywords is problematic when it comes to the space character as a keyword cannot be a space. Using :-
to represent a space required the clacksification and declacksification functions to convert between :-
and the space character. This also prevents hyphenated words working in the Clacks system.
Refactor the dictionary to use a string for each character as the keys in the map, instead of Clojure keywords. This solves the issue with space and other special characters.
(def dictionary\n {\"a\" [0 1 1 0 0 0 0 1]\n \"b\" [0 1 1 0 0 0 1 0]\n \"c\" [0 1 1 0 0 0 1 1]\n \"d\" [0 1 1 0 0 1 0 0]\n \"e\" [0 1 1 0 0 1 0 1]\n ,,,})\n
"},{"location":"simple-projects/encode-decode/clacks/#refactor-namespace","title":"Refactor namespace","text":"As the dictionary can be quite large to represent in code, move the dictionary definition to its own namespace.
Use a more specific name for the dictionary, describing what languages the dictionary is used for
(def english->clacks\n {\"a\" [0 1 1 0 0 0 0 1]\n \"b\" [0 1 1 0 0 0 1 0]\n \"c\" [0 1 1 0 0 0 1 1]\n \"d\" [0 1 1 0 0 1 0 0]\n \"e\" [0 1 1 0 0 1 0 1]\n ,,,})\n
A dictionary is required to translate from Clacks to English to decode the messages. Rather than write the reverse mappings for each character in the dictionary, in effect creating a second directory for the same two languages, use a function to invert the map by swapping keys and values.
clojure.set/map-invert
will swap each key/value pair in the map so the key becomes the value and the value becomes the key.
Define a clacks->english
dictionary that holds the result of the map-invert
function call
(ns practicalli.clacks-dictionary\n (:require [clojure.set]))\n\n(def clacks->english { ,,, })\n\n(def clacks->english (clojure.set/map-invert english->clacks))\n
Require the dictionary namespace using the alias dictionary
to give the dictionary names context when used in the main namespace.
Also require clojure.string
using the alias string
to use the join function.
(ns practicalli.clacks-messenger\n (:require [practicalli.clacks-dictionary :as dictionary]\n [clojure.string :as string]))\n
"},{"location":"simple-projects/encode-decode/clacks/#removing-side-effects","title":"Removing side effects","text":"Designing pure functions, those that receive all their data via arguments, is a common way to remove side effects.
Include the dictionary as an argument to each of the functions. This ensures that each function is pure and prevents side effects (side causes).
(defn character->clack [char dictionary]\n (let [character (str char)]\n (get dictionary character)))\n\n(defn message->clacks [message dictionary]\n (map #(character->clack % dictionary) message))\n\n(defn clack->character [clack dictionary]\n (get (clojure.set/map-invert dictionary) clack))\n\n(defn clack->character [clack dictionary]\n (get dictionary-inverted clack))\n\n;; Create a clacks code back to a message\n\n(defn clacks->message [clacks dictionary]\n (string/join (map #(clack->character % dictionary) clacks)))\n
Test the updated functions by calling them via the REPL
(message->clacks \"cab\" dictionary/english->clacks)\n;; => ([0 1 1 0 0 0 1 1] [0 1 1 0 0 0 0 1] [0 1 1 0 0 0 1 0])\n\n(message->clacks \"cab cab\" dictionary/english->clacks)\n;; => ([0 1 1 0 0 0 1 1] [0 1 1 0 0 0 0 1] [0 1 1 0 0 0 1 0] [0 0 0 0 0 0 0 0] [0 1 1 0 0 0 1 1] [0 1 1 0 0 0 0 1] [0 1 1 0 0 0 1 0])\n\n;; Create a character from a clack code\n\n;; test data\n(clacks->message '([0 1 1 0 0 0 1 1] [0 1 1 0 0 0 0 1] [0 1 1 0 0 0 1 0]) dictionary/english->clacks)\n\n(clacks->message\n '([0 1 1 0 0 0 1 1] [0 1 1 0 0 0 0 1] [0 1 1 0 0 0 1 0] [0 0 0 0 0 0 0 0] [0 1 1 0 0 0 1 1] [0 1 1 0 0 0 0 1] [0 1 1 0 0 0 1 0]) dictionary)\n
"},{"location":"simple-projects/encode-decode/clacks/#use-alternative-dictionaries","title":"Use alternative dictionaries","text":"Thanks to a flexible design with no side effects or side causes then its really easy to replace the English language alphabet with another language that can be encoded into Clack codes.
All that is required is to define a dictionary for another language. So languages based on the greek, latin or cyrillic alphabet could be send if a suitable alphabet with clack codes is supplied.
"},{"location":"simple-projects/encode-decode/convert-boolean-values/","title":"Convert boolean values","text":""},{"location":"simple-projects/encode-decode/convert-boolean-values/#convert-boolean-true-false-to-1-and-0","title":"Convert boolean true false to 1 and 0","text":"A very simple example of encoding and decoding is converting the Clojure values of true
and false
to 1
and 0
respectively.
Using 1
for true and 0
for false has been a common idiom in programming languages, especially where a language did not include true
and false
syntax.
"},{"location":"simple-projects/encode-decode/convert-boolean-values/#define-an-association-between-values","title":"Define an association between values","text":"Define a Clojure hash-map
to associate the Clojure boolean true
an false
values to 1
and 0
respectively
{false 0\n true 1}\n
"},{"location":"simple-projects/encode-decode/convert-boolean-values/#find-an-associated-value-for-the-conversion","title":"Find an associated value for the conversion","text":"Using the get
function the boolean-value
is used to find a matching key in the map and if found the value that key is associated is returned.
(get {false 0 true 1} boolean-value)\n
Example:
(get {false 0 true 1} true)\n
A map can be called, just like a function. the boolean-value
is passed to the map as a function argument. As with the get
expression, if the map contains the key the associated value is returned.
({false 0 true 1} boolean-value)\n
Example:
({false 0 true 1} true)\n
"},{"location":"simple-projects/encode-decode/convert-boolean-values/#convert-multiple-boolean-values","title":"Convert multiple boolean values","text":"If there are a collection of boolean values to convert, the map
function can be used to convert them all to 1 or 0.
Map this over a collection of values
(map {false 0 true 1} [collection-of-boolean-values])\n
Example:
(map {false 0 true 1} [true false false true true true false false true false true false false true])\n
"},{"location":"simple-projects/encode-decode/convert-boolean-values/#how-does-this-work","title":"How does this work?","text":"The map
function takes two arguments, a function and a collection. The map
function calls the function given as an argument and calls it with each element of the collection in turn. The result of each call is remembered by the map
function and when the last element of the collection has been used, a new collection of all the results is returned.
In the above example, the hash-map {false 0 true 1} acts as a function.
({false 0 true 1} true)\n
A hash-map acts as a function in that it can return an associated value when given a key as an argument.
Calling {false 0 true 1}
with true
as an argument returns the value 1
.
"},{"location":"simple-projects/encode-decode/rna-dna/","title":"RNA to DNA transcription","text":"Given a DNA strand, return its RNA complement (RNA transcription).
Both DNA and RNA strands are a sequence of nucleotides.
The four nucleotides found in DNA are adenine (A), cytosine (C), guanine (G) and thymine (T).
The four nucleotides found in RNA are adenine (A), cytosine (C), guanine (G) and uracil (U).
Given a DNA strand, its transcribed RNA strand is formed by replacing each nucleotide with its complement:
G -> C\nC -> G\nT -> A\nA -> U\n
Inspired by Exercism.io challenge This project was inspired by the RNA Transcription exercise on Exercism.io. Related exercises include Nucleotide Count and Hamming.
"},{"location":"simple-projects/encode-decode/rna-dna/#create-a-project","title":"Create a project","text":" Pracitcalli Clojure CLI Config provides the :project/create
alias to create projects using deps-new project.
clojure -T:project/create :template app :name practicalli/rna-transcription\n
"},{"location":"simple-projects/encode-decode/rna-dna/#define-unit-tests","title":"Define unit tests","text":"Open the test/practicalli/rna-transcription.clj
and add the following tests
(ns practicalli.rna-transcription-test\n (:require [clojure.test :refer [deftest is testing]]\n [rna-transcription :as SUT]))\n\n(deftest rna-transcription-test\n (testing \"transcribe cytosine to guanine\"\n (is (= \"G\" (SUT/to-rna \"C\"))))\n\n (testing \"transcribe guanine to cytosine\"\n (is (= \"C\" (SUT/to-rna \"G\"))))\n\n (testing \"transcribe adenine to uracil\"\n (is (= \"U\" (SUT/to-rna \"A\"))))\n\n (testing \"transcribe thymine to adenine\"\n (is (= \"A\" (SUT/to-rna \"T\"))))\n\n (testing \"transcribe all nucleotides\"\n (is (= \"UGCACCAGAAUU\" (rna-transcription/to-rna \"ACGTGGTCTTAA\"))))\n\n (testing \"validate dna strands\"\n (is (thrown? AssertionError (rna-transcription/to-rna \"XCGFGGTDTTAA\")))))\n
"},{"location":"simple-projects/encode-decode/rna-dna/#code-the-rna-transcription","title":"Code the RNA transcription","text":"Edit the src/practicalli/rna-transcription.clj
file and require the clojure.string
library. The library is part of the Clojure standard library, so does not need to be added as a project dependency.
(ns practicalli.rna-transcription\n (:require [clojure.string :as string]))\n
Define a dictionary to convert from DNA nucleotide to its RNA complement
(def dictionary-dna->rna\n \"Convert DNA to RNA\"\n {\"G\" \"C\"\n \"C\" \"G\"\n \"T\" \"A\"\n \"A\" \"U\"}\n )\n
Define a function to convert a single DNA nucleotide (one of G
, C, T, A) into its RNA complement, using the dictionary.
The algorithm is a simple hash-map lookup using the DNA nucleotide as the Key and returning the RNA complement as the value.
(defn convert-nucleotide\n \"Convert a specific nucleotide from a DNA strand,\n into a nucleotide for an RNA strand\"\n [dictionary nucleotide]\n (get dictionary (str nucleotide)))\n
Now a single nucleotide can be converted, another function can be defined to convert all DNA nucleotides in a given sequence.
(defn to-rna [dna-sequence]\n (if (clojure.string/includes? dna-sequence \"X\")\n (throw (AssertionError.))\n (apply str\n (map #(convert-nucleotide dictionary-dna-rna %) dna))))\n
Although apply str
provides the correct answer, it is more idiomatic to use the clojure.string/join
function.
(defn to-rna [dna-sequence]\n (if (clojure.string/includes? dna-sequence \"X\")\n (throw (AssertionError.))\n (string/join\n (map #(convert-nucleotide dictionary-dna-rna %) dna))))\n
The functions provide the correct answer, however, to-rna
is not a pure function as the dictionary is pulled in as a side cause.
Update all the tests in test/practicalli/rna-transcription.clj
to call SUT/to-rna
with a dictionary included in the argument.
(ns practicalli.rna-transcription-test\n (:require [clojure.test :refer [deftest is testing]]\n [rna-transcription :as SUT]))\n\n(deftest rna-transcription-test\n (testing \"transcribe cytosine to guanine\"\n (is (= \"G\" (SUT/to-rna SUT/dictionary-dna->rna \"C\"))))\n\n (testing \"transcribe guanine to cytosine\"\n (is (= \"C\" (SUT/to-rna SUT/dictionary-dna->rna \"G\"))))\n\n (testing \"transcribe adenine to uracil\"\n (is (= \"U\" (SUT/to-rna SUT/dictionary-dna->rna \"A\"))))\n\n (testing \"transcribe thymine to adenine\"\n (is (= \"A\" (SUT/to-rna SUT/dictionary-dna->rna \"T\"))))\n\n (testing \"transcribe all nucleotides\"\n (is (= \"UGCACCAGAAUU\" (SUT/to-rna SUT/dictionary-dna->rna \"ACGTGGTCTTAA\"))))\n\n (testing \"validate dna strands\"\n (is (thrown?\n AssertionError\n (SUT/to-rna SUT/dictionary-dna->rna \"XCGFGGTDTTAA\")))))\n
Update to-rna
to be a pure function by including the dictionary as an argument and also pass the updated tests.
(defn to-rna [dictionary dna-sequence]\n (if (clojure.string/includes? dna-sequence \"X\")\n (throw (AssertionError.))\n (string/join\n (map #(convert-nucleotide dictionary %) dna))))\n
"},{"location":"simple-projects/encode-decode/rna-dna/#idiomatic-improvements","title":"Idiomatic improvements","text":"The to-rna
function is not pure, as it relies on a shared value in the namespace, the dictionary-dna-rna
transcription map.
Passing dictionary-dna-rna
as an argument to the to-rna
function as well as the dna sequence would make to-rna
a pure function. It would also allow use of a range of transcription maps.
(defn to-rna\n \"Transcribe each nucleotide from a DNA strand into its RNA complement\n Arguments: string representing DNA strand\n Return: string representing RNA strand\"\n [transcription dna]\n (string/join\n (map #(or (transcription %)\n (throw (AssertionError. \"Unknown nucleotide\")))\n dna )))\n
The change to the to-rna
function will break all the tests.
Updated unit tests that call to-rna
with both arguments
(ns rna-transcription-pure-test\n (:require [clojure.test :refer [deftest is]]\n [rna-transcription-pure :as SUT]\n [rna-transcription :as data]))\n\n(deftest transcribes-cytosine-to-guanine\n (is (= \"G\" (SUT/dna->rna data/dna-nucleotide->rna-nucleotide \"C\"))))\n\n(deftest transcribes-guanine-to-cytosine\n (is (= \"C\" (SUT/dna->rna data/dna-nucleotide->rna-nucleotide \"G\"))))\n\n(deftest transcribes-adenine-to-uracil\n (is (= \"U\" (SUT/dna->rna data/dna-nucleotide->rna-nucleotide \"A\"))))\n\n(deftest it-transcribes-thymine-to-adenine\n (is (= \"A\" (SUT/dna->rna data/dna-nucleotide->rna-nucleotide \"T\"))))\n\n(deftest it-transcribes-all-nucleotides\n (is (= \"UGCACCAGAAUU\" (SUT/dna->rna data/dna-nucleotide->rna-nucleotide \"ACGTGGTCTTAA\"))))\n\n(deftest it-validates-dna-strands\n (is (thrown? AssertionError (SUT/dna->rna data/dna-nucleotide->rna-nucleotide \"XCGFGGTDTTAA\"))))\n
"},{"location":"simple-projects/encode-decode/rna-dna/#hintexercisim-project-and-the-pure-function","title":"Hint::Exercisim project and the pure function","text":"If you wish to keep the Exercisim project passing, then add a new namespace to the project by create a new file called rna-transcript-pure.clj
. Add the new design of the to-rna
function to that namespace. Copy the tests into a new namespace by creating a file called rna-transcription-pure.clj
and update the tests to use two arguments when calling to-rna
"},{"location":"simple-projects/encode-decode/rna-dna/#summary","title":"Summary","text":"This exercise has covered the concept of using a Clojure hash-map structure as a dictionary lookup.
"},{"location":"simple-projects/mutating-state/","title":"Mutating State in a Controlled way","text":"Mutating state should be used carefully and sparingly in Clojure (and all other programming languages).
atom
is a mutable container that can manage any value. The atom ensures that only one call at a time can affect the value it manages. This is part of the software transactions memory system in Clojure.
As the atom is mutable in that the value it manages can be changed, however, this must be done with special commands (swap!, reset!, compare-and-set!, swap-vals!).
Even though the atom is mutable, the values it manages are not. They are normal immutable (unchangeable) Clojure values.
ref
is similar to atom
and can manage transactions, ensuring that all changes happen or no changes happen.
Project Topics Overview Mutants assemble atom swap! reset! Using an atom to manage state changes Undo/Redo atom add-watch Traversing the history of an atom Poker game atom swap! reset! ref Simple transaction management using atom and ref in a card game, using constraints on an atom"},{"location":"simple-projects/mutating-state/#references","title":"References","text":" - Atoms - clojure.org
- Refs and Transactions - clojure.org
- Agents - clojure.org
"},{"location":"simple-projects/mutating-state/mutants-assemble/","title":"Mutants Assemble","text":"In this section you will apply changes to values, how to define your own simple functions.
We will also introduce the following functions for the first time:
function Description atom
create an anonymous function, one without a name deref
, @
assign a name to a function"},{"location":"simple-projects/mutating-state/mutants-assemble/#create-a-new-clojure-project","title":"Create a new Clojure project","text":" Pracitcalli Clojure CLI Config provides the :project/create
alias to create projects using deps-new project.
clojure -T:project/create :template app :name practicalli/mutants-assemble\n
Open the src/practicalli/mutants_assemble.clj
file in a Clojure aware editor and start the REPL.
"},{"location":"simple-projects/mutating-state/mutants-assemble/#define-an-atom","title":"Define an atom","text":"Use the def
function to bind a name to an atom.
The atom wraps data, initially an empty vector.
(def mutants (atom []))\n
The vector remains an immutable value, even though it is contained within a mutable atom container
Define a function using defn
which takes a mutant as an argument and updates the value managed by the atom. The reference to the atom is also an argument, making this a pure function and more generic as any given atom can be updated with this function.
(defn add-mutant [mutants mutant]\n (swap! mutants conj mutant))\n
swap!
uses a function to create a new value for the atom to manage. In this case the conj
function is used to join the value of mutant with the existing mutants atom value, creating a new vector.
swap!
is a macro so the syntax is a little different. Essentially this is the same as an expression (conj mutants mutant)
, with the value this returns swapped into the atom.
Call the function with the mutants
atom and a mutant to add, which is a string containing the name of a mutant character.
(add-mutant mutants \"Black Widow\")\n
The value the atom is managing has been swapped for a new value. The original value was not modified (vectors are immutable) so the atom now points to a new value, a vector containing a string.
"},{"location":"simple-projects/mutating-state/mutants-assemble/#viewing-the-value-managed-by-the-atom","title":"Viewing the value managed by the atom","text":"Use the deref
function to see the value the atom is managing.
(deref mutants)\n
It is idiomatic to use @
which is a syntax alias for the deref
function, rather than explicitly using deref
.
@mutants\n
"},{"location":"simple-projects/mutating-state/mutants-assemble/#reset-the-atom-value","title":"Reset the atom value","text":"reset!
will change the value managed by the atom by providing the new value. This is simpler than using swap!
as it does not use the existing value in the atom.
(reset! mutants [])\n
Now all the mutants are gone (and we can start looking for new ones to add).
"},{"location":"simple-projects/tdd-kata/","title":"Kata challenges","text":"A kata is a small challenge that you attempt to solve in different ways, so experiment with your solutions to these challenges.
Kata are often coupled with Test Driven Development approach.
Project Topics Overview Recent song-list TDD Keep a list of recent songs played, without duplicates Salary Slip Generator TDD Generate play slips for an employee Code Kata Website
"},{"location":"simple-projects/tdd-kata/recent-song-list/","title":"TDD Kata Recent Song-list","text":"Create a recent song list to hold a unique set of songs that have been played.
The most recently played song is at the start of the list, the least recently played song is the last in the list.
- A recently-used-list is initially empty.
- Songs in the list are unique, so repeatedly played songs should only appear once in the list
- Songs can be looked up by index, which counts from zero.
- The song list can be transitory (starting from empty each time) or persistent within a REPL session (examples use a transitory approach)
Optional extras:
- Empty song names are not allowed.
- Add a limit to the number of songs the list contains, with the least recently added items dropped when that limit is reached.
"},{"location":"simple-projects/tdd-kata/recent-song-list/#create-project","title":"Create Project","text":"Create a new project using clj-new
clojure -T:project/create practicalli/song-list\n
"},{"location":"simple-projects/tdd-kata/recent-song-list/#run-repl","title":"Run REPL","text":"Start a Clojure REPL via a Clojure editor or via the command line from the root of the project directory
Start rich terminal UI Clojure REPL
clojure -M:repl/rebel\n
"},{"location":"simple-projects/tdd-kata/recent-song-list/#unit-tests","title":"Unit Tests","text":"clojure.test
library is part of Clojure standard library and is the most common way to write unit tests in Clojure
Open test/playground/song_list_test.clj
file in your editor and update the namespace definition to include clojure.test
Require clojure.test namespace
test/playground/song_list_test.clj(ns practicalli.song-list-test\n (:require [clojure.test :refer [deftest is testing]]\n [playground.song-list :as song-list]))\n
"},{"location":"simple-projects/tdd-kata/recent-song-list/#run-tests","title":"Run Tests","text":"Evaluate the practicalli.song-list
and practicalli.song-list-test
namespaces to load their code into the REPL
Call the run-tests
function in the REPL to get a report back on all of the tests in our current namespace (song-list
)
Kaocha test runnerclojure.test runner Practicall Clojure CLI Config provides the :test/run
alias to run the Kaocha test runner.
Kaocha test runner
Open a command line in the root directory of the project and run the following command.
clojure -X:test/run\n
Kaocha runs all the tests, stopping should a test fail.
Kaocha test runner with file watch
Use the :test/watch
alias to automatically run tests when ever a file is saved
clojure -X:test/run\n
Evaluate the project code and evaluate the run-tests
function from clojure.test
from within the REPL
clojure.test runner
(run-tests)\n
"},{"location":"simple-projects/tdd-kata/recent-song-list/#test-song-list-exists","title":"Test song-list exists","text":"Write a test to see if a recent song list exists.
This is an opportunity to think about what kind of data structure you want to use to hold your recent song list.
Try write the code first and then check that code with the examples provided (click to expand each code example box)
Test song-list exists A simple test that checks for a recent-songs
list src/playground/song_list.clj
(deftest song-list-exists-test\n (testing \"Does a recent song list exist\"\n (is (vector? song-list/recent-songs))))\n
recent-songs
should be defined in src/playground/recent-song-list.clj
before running the test, otherwise a compilation error will be returned."},{"location":"simple-projects/tdd-kata/recent-song-list/#define-a-recent-song-list","title":"Define a recent song list","text":"Edit src/playground/song_list.clj
and define a name for the collection of recent songs
Use an empty collection to start with. Which collection type will you use though (hash-map {}
, list ()
, set #{}
, vector []
)?
recent-songs collection Define a recent-song name for an empty vector src/playground/song_list.clj
(def recent-songs [])\n
Test First Approach For a strict test first approach, a recent-songs
name (symbol) would be defined that returns false
or a falsy value, e.g. nil
A name (symbol) must be defined for use in the test so that the Clojure code can compile
"},{"location":"simple-projects/tdd-kata/recent-song-list/#test-song-list-is-empty","title":"Test song-list is empty","text":"The recent song list should be empty to start with.
Check song list is empty A simple test that compares an empty vector with the value of recent-songs
src/playground/song_list.clj
(deftest song-list-empty-test\n (testing \"Is song list empty if we haven't added any songs\"\n (is\n (= [] song-list/recent-songs))))\n
Here is the same test using the empty?
function instead of the =
function.
src/playground/song_list.clj
(deftest song-list-empty-test\n (testing \"Is song list empty if we haven't added any songs\"\n (is\n (empty? song-list/recent-songs))))\n
Either of these tests could replace the test that the song list exists, as these tests would fail if the song list did not exist."},{"location":"simple-projects/tdd-kata/recent-song-list/#test-adding-a-song-to-the-list","title":"Test adding a song to the list","text":"Add a song to the collection, for example Tubular Bells - Mike Oldfield
Test adding a song to the list test/playground/song_list_test.clj(deftest add-songs-test\n\n (testing \"add song returns a song list with entries\"\n (is\n (not (empty?\n (add-song \"Barry Manilow - Love on the rocks\" song-list/recent-songs)))))\n\n (testing \"add multiple song returns a song list with entries\"\n (is\n (not (empty?\n (->> song-list/recent-songs\n (add-song \"Mike Oldfield - Tubular Bells Part 1\")\n (add-song \"Barry Manilow - Love on the rocks\")\n (add-song \"Phil Colins - Sususudio\" )))))))\n
Other songs are avialbe and Practicalli makes no recommendation as to what songs should be used or listened too.
"},{"location":"simple-projects/tdd-kata/recent-song-list/#function-to-add-song","title":"Function to add song","text":"Create a function to add a song to the start of the song list.
Function to add song to list The add-song
function takes the name of a song and the song list to which it will be added.
A Thread-last macro ->>
is used to pass the song list over two functions.
The song-list
is first passed to the remove
expression as its last argument. This expression will remove any occurrence of the new song we want to add from the song-list
.
The results of the remove
expression are then passed to the cons
expression as its last argument. The cons
expression simply adds the new song to the start of the list, making it the most recent song.
src/playground/song_list.clj(def recent-songs [])\n\n(defn add-song [song song-list]\n (cons song song-list))\n
recent-songs
is passed into the add-song
function as an argument, song-list
to keep the design of add-song
function pure (no side-effects). This design also provides greater scope to using the add-song
function, as any song list can be added to, rather than hard-coding recent-songs
list.
"},{"location":"simple-projects/tdd-kata/recent-song-list/#test-song-added-to-top-of-list","title":"Test song added to top of list","text":"As the song list shows recently played songs, new songs added should be at the top of the list.
The list should not contain duplicate entries for a song.
Test songs added to top of list test/playground/song_list_test.clj(deftest recently-added-song-first-test\n\n (testing \"most recent song should be first in the list when empty list\"\n (is (=\n (first (add-song \"Daft Punk - Get Lucky\" recent-songs))\n \"Daft Punk - Get Lucky\")))\n\n (testing \"most recent song should be first in list when adding multiple songs\"\n (is (=\n (first\n (->> recent-songs\n (add-song \"Daft Punk - Get Lucky\")\n (add-song \"Pharrell Williams - Happy\")))\n \"Pharrell Williams - Happy\")))\n\n (testing \"most recent song should be first in list when adding a repeated song\"\n (is (=\n (first\n (->> recent-songs\n (add-song \"Pharrell Williams - Happy\")\n (add-song \"Daft Punk - Get Lucky\")\n (add-song \"Pharrell Williams - Happy\")))\n \"Pharrell Williams - Happy\")))\n\n (testing \"most recent song should be first in list when adding a repeated song\"\n (is (not=\n (last\n (->> recent-songs\n (add-song \"Pharrell Williams - Happy\")\n (add-song \"Daft Punk - Get Lucky\")\n (add-song \"Pharrell Williams - Happy\")))\n \"Pharrell Williams - Happy\"))))\n
"},{"location":"simple-projects/tdd-kata/recent-song-list/#add-song-to-start-of-list","title":"Add song to start of list","text":"Create a function to add a song to the start of the song list.
Function to add song to list The add-song
function takes the name of a song and the song list to which it will be added.
A Thread-last macro ->>
is used to pass the song list over two functions.
The song-list
is first passed to the remove
expression as its last argument. This expression will remove any occurrence of the new song we want to add from the song-list
.
The results of the remove
expression are then passed to the cons
expression as its last argument. The cons
expression simply adds the new song to the start of the list, making it the most recent song.
src/playground/song_list.clj(def recent-songs [])\n\n(defn add-song [song song-list]\n (->> song-list\n (remove #(= song %))\n (cons song)))\n
recent-songs
is passed into the add-song
function as an argument, song-list
to keep the design of add-song
function pure (no side-effects). This design also provides greater scope to using the add-song
function, as any song list can be added to, rather than hard-coding recent-songs
list.
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/","title":"Salary Slip Kata","text":" Pracitcalli Clojure CLI Config provides the :project/create
alias to create projects using deps-new project.
clojure -T:project/create :template app :name practicalli/salary-calculator\n
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/#problem-description","title":"Problem description","text":"A typical salary slip contains employee details like employee id, employee name and their monthly salary details like their gross salary, national insurance contributions, tax-free allowance, taxable income and tax payable.
Salary slips are generated each month for every employee.
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/#acceptance-criteria","title":"Acceptance criteria","text":" - Salary slip generator should receive an employee with its Employee Id, Employee Name and Annual Gross Salary
- Salary slip should contain the Employee ID, Employee Name, Gross Salary, National Insurance contributions, Tax-free allowance, Taxable income and Tax payable for the month
- The entry point should be the following public function API
(defn salary-slip-generator\n \"\"\n [employee]\n ,,,)\n
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/#iterations","title":"Iterations","text":"Each iteration adds more rules to the calculation. Some iterations also introduce new fields to the salary slip.
In a given iteration, all the salary slips contain the same number fields for each employee (if a tax or contribution does not apply for a given employee, just put \u00a30.00).
This means that for each iteration you will need to add fields to the SalarySlip
class. In the first iteration, SalarySlip
only contains the Employee ID, Employee Name and Monthly Gross Salary.
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/#iteration-1-for-an-annual-salary-of-500000","title":"Iteration 1: for an annual salary of \u00a35,000.00","text":"This is the most basic case.
Calculation rules:
- Monthly Gross Salary: The monthly gross salary is the employee's annual gross salary divided by 12
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/#iteration-2-for-an-annual-gross-salary-of-906000","title":"Iteration 2: for an annual gross salary of \u00a39,060.00","text":"Here we introduce the National Insurance contribution
The monthly salary slip should contain the below:
Employee ID: 12345\n Employee Name: John J Doe\n Gross Salary: \u00a3755.00\n National Insurance contributions: \u00a310.00\n
Calculation rules:
- National Insurance contributions: Any amount of money earned above a gross annual salary of \u00a38,060.00 is subject to a National Insurance contribution of 12%
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/#iteration-3-for-an-annual-gross-salary-of-1200000","title":"Iteration 3: for an annual gross salary of \u00a312,000.00","text":"This employee also needs to pay taxes
The monthly salary slip should contain the below:
Employee ID: 12345\n Employee Name: John J Doe\n Gross Salary: \u00a31,000.00\n National Insurance contributions: \u00a339.40\n Tax-free allowance: \u00a3916.67\n Taxable income: \u00a383.33\n Tax Payable: \u00a316.67\n
Calculation rules:
- Taxable income: Any amount of money earned above a gross annual salary of \u00a311,000.00 is taxed at 20%
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/#iteration-4-for-an-annual-gross-salary-of-4500000","title":"Iteration 4: for an annual gross salary of \u00a345,000.00","text":"This employee pays a higher band of National Insurance and Income Tax.
The monthly salary slip should contain the below:
Employee ID: 12345\n Employee Name: John J Doe\n Gross Salary: \u00a33,750.00\n National Insurance contributions: \u00a3352.73\n Tax-free allowance: \u00a3916.67\n Taxable income: \u00a32,833.33\n Tax Payable: \u00a3600.00\n
Calculation rules:
- Taxable income (higher rate): Any amount of money earned above a gross annual salary of \u00a343,000.00 is taxed at 40%
- National Insurance (higher contributions): Any amount of money earned above a gross annual salary of \u00a343,000.00 is only subject to a 2% NI contribution
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/#iteration-5-for-annual-gross-salaries-of-10100000-11100000-12200000-and-15000000","title":"Iteration 5: for annual gross salaries of \u00a3101,000.00; \u00a3111,000.00; \u00a3122,000.00 and \u00a3150,000.00","text":"For high earners, the tax-free allowance decreases.
The monthly salary slips should contain the below (respectively):
Employee ID: 12345\n Employee Name: John J Doe\n Gross Salary: \u00a38,416.67\n National Insurance contributions: \u00a3446.07\n Tax-free allowance: \u00a3875.00\n Taxable income: \u00a37,541.67\n Tax Payable: \u00a32,483.33\n\n\n Employee ID: 12345\n Employee Name: John J Doe\n Gross Salary: \u00a39,250.00\n National Insurance contributions: \u00a3462.73\n Tax-free allowance: \u00a3458.33\n Taxable income: \u00a38,791.67\n Tax Payable: \u00a32,983.33\n\n\n Employee ID: 12345\n Employee Name: John J Doe\n Gross Salary: \u00a310,166.67\n National Insurance contributions: \u00a3481.07\n Tax-free allowance: \u00a30.00\n Taxable income: \u00a310,166.67\n Tax Payable: \u00a33,533.33\n\n\n Employee ID: 12345\n Employee Name: John J Doe\n Gross Salary: \u00a312,500.00\n National Insurance contributions: \u00a3527.73\n Tax-free allowance: \u00a30.00\n Taxable income: \u00a312,500.00\n Tax Payable: \u00a34,466.67\n
Calculation rules:
- Tax-free allowance: When the Annual Gross Salary exceeds \u00a3100,000.00, the tax-free allowance starts decreasing. It decreases by \u00a31 for every \u00a32 earned over \u00a3100,000.00. And this excess is taxed at the Higher rate tax.
"},{"location":"simple-projects/tdd-kata/salary-slip-generator/#iteration-6-for-an-annual-gross-salary-of-16000000","title":"Iteration 6: for an annual gross salary of \u00a3160,000.00","text":"The employee goes into the additional rate band.
The monthly salary slip should contain the below:
Employee ID: 12345\n Employee Name: John J Doe\n Gross Salary: \u00a313,333.33\n National Insurance contributions: \u00a3544.40\n Tax-free allowance: \u00a30.00\n Taxable income: \u00a313,333.33\n Tax Payable: \u00a34,841.67\n
Calculation rules:
- Income tax (additional rate band) : Any amount of money earned above a gross annual salary of \u00a3150,000.00 is taxed at 45%
Practicalli Salary Slip Kata Salary slip kata - Devoxx 2019
(ns salary-slip-kata.core\n \"Developer Anarchy by Fred George\n - made devs write the same solution in different languages\n -- helps devs master the business domain\n -- helps devs master technology domain\")\n\n(defn- national-insurance-contribution\n \"Calculate the national insurance contribution due for a given annual salary.\n\n ---------------------+-------------------------+--------\n Band | NI deductible income | NI Rate\n ---------------------+-------------------------+--------\n No contributions | Up to \u00a38,060.00 | 0%\n Basic contributions | \u00a38,060.00 to \u00a343,000.00 | 12%\n Higher contributions | over \u00a343,000.00 | 2%\n ---------------------+-------------------------+-------- \"\n\n [annual-gross-salary]\n ;; add a cond statement to return the calculate the value with respect to the band.\n (* annual-gross-salary 0.12))\n\n\n;; taxable income\n;; ---------------------+---------------------------+---------\n;; Band | Taxable income | Tax rate\n;; ---------------------+---------------------------+---------\n;; Personal Allowance* | Up to \u00a311,000.00 | 0%\n;; Basic rate | \u00a311,000.00 to \u00a343,000.00 | 20%\n;; Higher rate | \u00a343,000.00 to \u00a3150,000.00 | 40%\n;; Additional rate | over \u00a3150,000.00 | 45%\n;; ---------------------+---------------------------+---------\n\n\n(defn salary-slip\n \"Creates a salary slip for a person\n\n Specifically for employee of 24K annual salary\"\n\n [{:keys [employee-id\n employee-name\n annual-gross-salary]}]\n (let [tax-free-allowance 11000\n taxable-income (- annual-gross-salary\n tax-free-allowance)]\n {:employee-id employee-id\n :employee-name employee-name\n :gross-salary (/ annual-gross-salary 12)\n :national-insurance (national-insurance-contribution annual-gross-salary)\n :tax-free-allowance tax-free-allowance\n :taxable-income taxable-income\n :tax-payable (* taxable-income 0.20)}))\n
"},{"location":"testing/","title":"Testing in Clojure","text":"Testing is supported in Clojure with a range of testing libraries and test runners.
"},{"location":"testing/#unit-test","title":"Unit Test","text":"The unit of test in Clojure is the function, so functions are defined that test other functions.
clojure.test namespace is part of the Clojure standard library and provides assertion based testing and functions to run tests.
Practicalli Unit Testing Guide Using Test Runners
"},{"location":"testing/#generative-testing","title":"Generative testing","text":"Define specifications for values to confirm information coming into and leaving a Clojure service are of the correct form.
Generate test data from value specifications to verify the behaviour of functions, creating diverse sets of data for extensive testing.
Define specifications for functions to validate the correct form of values are passed and returned from a function.
Define value and function specifications with Cloure Spec Generative Testing with Clojure Spec
"},{"location":"testing/#performance-testing","title":"Performance testing","text":"Test individual expressions through to application and load testing one or more services.
- time - simple results of evaluating an expression
- criterion - a realistic measure of performance for clojure expressions
- Gatling - open source & commercial load test tool for web applications
- clj-gatling - wrapper around Gatling which enables tests to be expressed in Clojure.
"},{"location":"testing/#behaviour-driven-development-frameworks","title":"Behaviour Driven Development frameworks","text":"Although not widely used in the Clojure community, there are several approaches to develop and test software using Behaviour Driven Development.
BDD: Given, When, Then and scenario approach to outside in software testing
- Scenari - executable specification / BDD in Clojure
- kaocha-cucumber - support for Cucumber tests in the gerkin format
- speclj - TDD/BDD framework for Clojure and ClojureScript based on RSpec.
Alternative BDD libraries are discussed at https://github.com/gphilipp/bdd-guide-clojure
"},{"location":"testing/#articles-on-testing-in-clojure","title":"Articles on testing in Clojure","text":" - Clojure test runner of my dreams
- Example based unit testing in Clojure
- TDD in Clojure at Funding Circle
- Bolth - a more humane test runner
- Announcing kaocha a new and improved clojure test runner
- Scenarios as code - Clojure Remote presentation
- Load testing with Gatling and Clojure - JUXT.pro
"},{"location":"testing/clojure-test/","title":"Unit Testing with clojure.test
","text":"clojure.test
is a test library that is already part of Clojure and test package hierarchy is typically created (e.g. when generating Clojure projects with Leiningen).
As with other unit testing libraries you use clojure.test
to write test. These tests are defined as functions that contain one or more assertions.
As a general guideline, a Clojure test function should test a specific Clojure function.
"},{"location":"testing/clojure-test/#hintwhat-to-test","title":"Hint::What to test","text":"Define a deftest
for every public functions within a namespace, so the contract / api for each namespace is testable and will highlight obvious regressions. The testing
function can be used to group assertions for a particular deftest
, so different aspects of the tests can be grouped together.
Test reports contain only the names of the deftest
functions, as there are no names with testing
clojure.spec
provides another way to define a contract around your functions and data structures. It also includes generative testing approach to broaden the test data used to test your functions.
"},{"location":"testing/clojure-test/#test-namespaces","title":"Test namespaces","text":"clojure.test
needs to be included in the namespace in order to use the functions that namespace provides.
The recommended syntax is to :refer
the specific functions which makes those functions available as if they were defined in the current namespace.
The namespace that is under test also needs to be included and and its recommended that you use the alias SUT
for system under test. The test namespace matches the namespace you are testing, with the addition of -test
to the name.
(ns my-clojure-app.core-test\n (:require [clojure.test :refer [deftest deftest- testing is]]\n [my-clojure-app.core :as SUT ]))\n
"},{"location":"testing/clojure-test/#writing-an-assertion","title":"Writing an assertion","text":"An assertion is where you compare an expected result with the result of calling a function. If the assertion is true, then then it is a pass. If the assertion is false, then its a fail.
The form of an assertion takes a form (is (comparator expected-value function-call))
Some simple examples include
(is (= 42 (* 6 7)))\n\n(is (not= 24 (* 6 7)))\n
"},{"location":"testing/clojure-test/#defining-a-test","title":"Defining a test","text":"deftest
is used to define a function that will test a similarly named function from the src
tree. The test function name should match the function it is testing with -test
added to the end.
testing
function allows you to group one or more assertions
is
defines an assertion
(deftest adder-test\n (testing \"Using a range of numbers to test the adder\"\n #_(is (= 0 1))\n (is (= (+ 1 2) (adder 1 2)) \"Adding 1 and 2\")\n (is (= (+ 1 -2) (adder 1 -2)) \"Adding 1 and -2\")\n #_(is (not (= (+ 1 2)) (adder \"a\" \"b\")) \"Adding strings as negative test\")\n (is (false? (= 0 1)) \"A simple failing test\")\n (is (false? (= 0 (adder 3 4))) \"Purposefully using failing data\")))\n
"},{"location":"testing/integration-testing/","title":"Integration Testing","text":"See the continuous integration section
"},{"location":"testing/test-runners/","title":"Test Runners","text":"A test runner is used to run one or more unit tests in a project and report the results. When there are failing tests, a test runners show details of how the failing test, showing actual and expected values.
Test runners may support specification testing with clojure.spec, checking data and functions conform to their specifications.
Test runners are called from either a Clojure editor, as a command line tool or within a continuous integration workflow.
Regularly run tests in a project to ensure implementations and design decisions made so far have not regressed.
Test runner Type Summary cognitect-labs test runner clj Simple test runner cljs-test-runner cljs Run all ClojureScript tests with one simple command. Kaocha clj, cljs Full featured test runner CIDER test runner clj CIDER built in test runner CIDER test runner is ideal if using Emacs for Clojure development, as its build into CIDER.
Practicalli Recommends Kaocha test runner
Kaocha is a very feature rich test runner for Clojure and ClojureScript, BDD style cucumber tests, coverage and junit style reporting.
Practicalli Clojure CLI Config - test runner aliases
Practicalli Clojure CLI Config contains several aliases for test runners
:test/env
adds the test
directory to the class path and org.clojure/test.check
library clojure -X:test/run
run Kaocha test runner clojure -X:test/watch
run Kaocha test runner in watch mode, running on file changes clojure -M:test/cljs
run Kaocha ClojureScript test runner clojure -X:test/cognitect
simple to use Cognitect test runner :lib/kaocha
adds Kaocha as a library to the class path, enabling Kaocha to run from an editor, e.g. Emacs Cider with Kaocha test runner
Practicalli REPL Reloaded aliases :repl/reloaded
& :dev/reloaded
also support Kaocha test runner
"},{"location":"testing/test-runners/aero/","title":"Aero","text":"juxt/aero is used to read the kaocha configuration, so reader literals such as #env, #merge, #ref, and #include can be used.
Set up profiles for different stages of the development workflow, dev, test, prod, etc. Each profile has a different configuration making it very easy to switch
{:port 8000\n :database #profile {:prod \"datomic:dev://localhost:4334/my-prod-db2\"\n :test \"datomic:dev://localhost:4334/my-test-db\"\n :default \"datomic:dev://localhost:4334/my-db\"}\n :known-users [{:name \"Alice\"} {:name \"Betty\"}]}\n
Then in application startup function or a component lifecycle library (mount, component, integrant) read in a specific profile
(aero.core/read-config \"config.edn\" {:profile :prod})\n
"},{"location":"testing/test-runners/congnitect-labs-test-runner/","title":"Cognitect Labs Test Runner","text":"Cognitect Labs test-runner is a test runner for Clojure projects defined with deps.edn
and using clojure.test
library which is part of the Clojure standard library.
test-runner aims to provide a standard way to discover and run unit and property-based tests, in a simple to use and lightweight tool.
"},{"location":"testing/test-runners/congnitect-labs-test-runner/#add-test-runner","title":"Add test-runner","text":"Make test-runner available to all projects by adding it to ~/.clojure/deps.edn
. Or add test-runner to specific projects by adding an alias to the project deps.edn
file. Include :extra-paths
configuration to include the standard test
directory so that the runner has access to the test code.
Practicalli Clojure CLI ConfigAlias Definition Practicalli Clojure CLI Config provides aliases for test runner tools, including :test/congnitect
for running Cognitect Labs test runner
Add an alias to run Cognitect Labs test runner, either in the project or user deps.edn
configuration file. ```clojure :test/cognitect {:extra-paths [\"test\"] :extra-deps {com.cognitect/test-runner {:git/url \"https://github.com/cognitect-labs/test-runner.git\" :sha \"f7ef16dc3b8332b0d77bc0274578ad5270fbfedd\"}} :main-opts [\"-m\" \"cognitect.test-runner\"]}
```
"},{"location":"testing/test-runners/congnitect-labs-test-runner/#run-test-runner","title":"Run test runner","text":"Run the Cognitect Labs test runner via the command line
clojure -M:test/cognitect\n
The cognitect.test-runner/-main
function is called which scans the test
directory of the current project tests defined using clojure.test
, running all tests found.
A summary is returned with the results of running the tests.
"},{"location":"testing/test-runners/congnitect-labs-test-runner/#command-line-options","title":"Command line options","text":"Flag Description -d, --dir DIRNAME Name of the directory containing tests. Defaults to \"test\". -n, --namespace SYMBOL Symbol indicating a specific namespace to test. -r, --namespace-regex REGEX Regex for namespaces to test. Defaults to #\".*-test$\" (i.e, only namespaces ending in '-test' are evaluated) -v, --var SYMBOL Symbol indicating the fully qualified name of a specific test. -i, --include KEYWORD Run only tests that have this metadata keyword. -e, --exclude KEYWORD Exclude tests with this metadata keyword. -H, --test-help Display this help message Options can be used multiple times in one command, for a logical OR effect. For example, the following command runs all tests in the practicalli.data.survey
and practicalli.services.survey-report
namespaces that are found in the src
and test
directories
clojure -M:test/cognitect -d test -d src -n practicalli.data.survey -n practicalli.services.survey-report\n
"},{"location":"testing/test-runners/congnitect-labs-test-runner/#test-selectors","title":"Test Selectors","text":"Selectively running tests by including and excluding test categories, from the meta-data added to test definitions, i.e. deftest
.
(deftest ^:integration test-live-system\n (is (= 200 (:status (http/get \"http://example.com\")))))\n
Use the --include
flag with the test runner to specify specific categories of tests
clojure -M:test-runner-cognitect --include :integration\n
The --include
flag can be used multiple times, defining a test category with each include.
clojure -M:test-runner-cognitect --include :integration --include :persistence\n
Clojure Unit Test - categories example integration and develop tests
--exclude
flag runs all tests except those in the given category
clojure -M:test/cognitect --exclude :integration\n
Exclusions take priority over inclusions if both flags are included.
"},{"location":"testing/test-runners/example-projects/","title":"Example projects","text":" - TDD Kata: Recent Song-list - simple tests examples
- Codewars: Rock Paper Scissors (lizard spock) solution -
and
examples - practicalli/numbers-to-words - overly verbose example, ripe for refactor
- practicalli/codewars-guides - deps.edn projects
- practicalli/exercism-clojure-guides - Leiningen projects
"},{"location":"testing/test-runners/example-projects/#sean-corfield-user-manager","title":"Sean Corfield - user manager","text":"User manager has unit tests that also include an embedded database. Tests can run with the Cognitect Labs test runner.
:test
alias includes the test path and a dependency for the H2 database
Cognitect Labs test runner included in the project deps.edn
file as :runner
clojure -M:test:runner
will run the Cognitect Labs runner and include the dependency to run the in-memory database used for the tests.
"},{"location":"testing/test-runners/example-projects/#using-koacha-with-sean-corfield-user-manager","title":"Using koacha with Sean Corfield user manager","text":"Adding a test.edn
file is not sufficient for testing this project with lambdaisland/kaocha, as the H2 dependency is also needed.
Create a bin/koacha
script and add the extra alias
#!/usr/bin/env bash\nclojure -M:test:test-runner-kaocha \"$@\"\n
"},{"location":"testing/test-runners/example-projects/#status-monitor","title":"Status Monitor","text":"Status monitor is a Leiningen project.
Include a :kaocha
profile in the project.clj
file, adding the koacha dependency. The :kaocha
alias sets the main namespace and uses the kaocha profile.
{:dev {:dependencies [[javax.servlet/servlet-api \"2.5\"]\n [ring/ring-mock \"0.3.2\"]]}\n :kaocha {:dependencies [[lambdaisland/kaocha \"1.0.632\"]]}}\n :aliases {\"kaocha\" [\"with-profile\" \"+kaocha\" \"run\" \"-m\" \"kaocha.runner\"]}\n
lein kaocha
will run all the tests
"},{"location":"testing/test-runners/kaocha-test-runner/","title":"LambdaIsland Kaocha Test Runner","text":" lambdaisland/kaocha (cow-cha) is a comprehensive test runner that support unit testing and clojure.spec
generative testing. Clojure and ClojureScript languages are supported.
Kaocha is highly configurable via a tests.edn
configuration file in the root of the project.
"},{"location":"testing/test-runners/kaocha-test-runner/#clojure-cli-config","title":"Clojure CLI Config","text":"Practicalli Clojure CLI ConfigAlias Definition Practicalli Clojure CLI Config configuration contains aliases to run kaocha test runner, using either the -X
or -M
execution flag.
:test/run
- run all tests in the project, stopping on first failing test :test/watch
- watching for file changes and run all tests in the project, stopping on first failing test :test/env
- add supporting paths and libraries for testing projects
Each alias includes :extra-paths [\"test\"]
to include the test
directory on the class path, enabling Koacha test runner to find the unit test code.
Define an alias in the project or user deps.edn
configuration.
For CI services such as CircleCI or GitLabs, add an alias for kaocha to the project deps.edn
file.
Alias definitions for LambdaIsland/Kaocha test runner
Aliases support the -M
(clojure.main) and -X
(clojure.exec) execution options with Clojure CLI.
:test/run\n{:extra-paths [\"test\"]\n :extra-deps {lambdaisland/kaocha {:mvn/version \"1.77.1236\"}}\n :main-opts [\"-m\" \"kaocha.runner\"]\n :exec-fn kaocha.runner/exec-fn\n :exec-args {:fail-fast? true\n :randomize? false}}\n\n;; Kaocha test runner in watch mode\n;; clojure -X:test/watch\n:test/watch\n{:extra-paths [\"test\"]\n :extra-deps {lambdaisland/kaocha {:mvn/version \"1.77.1236\"}}\n :main-opts [\"-m\" \"kaocha.runner\" \"--watch\" \"--fail-fast\" \"--skip-meta\" \":slow\"]\n :exec-fn kaocha.runner/exec-fn\n :exec-args {:watch? true\n :randomize? false\n :fail-fast? true}}\n
Libraries and directories containing code to support testing projects can be added to the :test/env
alias
:test/env\n{:extra-paths [\"test\"]\n :extra-deps {org.clojure/test.check {:mvn/version \"1.1.1\"}}}\n
Alias definitions should include :extra-paths [\"test\"]
to add the test
directory on the class path, enabling Koacha test runner to find the unit test code.
"},{"location":"testing/test-runners/kaocha-test-runner/#run-kaocha","title":"Run Kaocha","text":"Kaocha can be run via make tasks, Clojure CLI, or by creating a kaocha
script.
Babashka task runner could also be used to develop tasks to run kaocha
MakeClojure CLIKaocha script Practialli Makefile contains tasks for testing Clojure projects with Kaocha (and many other common Clojure development tasks)
Practicalli Makefile targets for unit testing Practicalli Makefile includes the following targets for Kaocha test runner Makefile
# ------- Testing -------------------- #\n\ntest-config: ## Run unit tests - stoping on first error\n $(info --------- Runner Configuration ---------)\n clojure -M:test/env:test/run --print-config\n\ntest-profile: ## Profile unit test speed, showing 3 slowest tests\n $(info --------- Runner Profile Tests ---------)\n clojure -M:test/env:test/run --plugin kaocha.plugin/profiling\n\ntest: ## Run unit tests - stoping on first error\n $(info --------- Runner for unit tests ---------)\n clojure -X:test/env:test/run\n\n\ntest-all: ## Run all unit tests regardless of failing tests\n $(info --------- Runner for all unit tests ---------)\n clojure -X:test/env:test/run :fail-fast? false\n\ntest-watch: ## Run tests when changes saved, stopping test run on first error\n $(info --------- Watcher for unit tests ---------)\n clojure -X:test/env:test/run :watch? true\n\ntest-watch-all: ## Run all tests when changes saved, regardless of failing tests\n $(info --------- Watcher for unit tests ---------)\n clojure -X:test/env:test/run :fail-fast? false :watch? true\n\n# ------------------------------------ #\n
Run all tests using the following command from the root of the Clojure project. Kaocha stops if there is a failing task, saving time on running the whole test suite.
make test\n
Use the test-all
target to run all unit tests regardless of failures (execept compiler errors)
make test-all\n
Continually run tests by watching for changes using the :test/watch
alias. If a test fails, Koacha will stop the test run and restart from the failing test when a change is detected. Use watch-all
if all tests should run regardless of failure.
make test-watch\n
Practicalli Clojure CLI Config configuration contains aliases to run kaocha test runner, using either the -X
or -M
execution flag.
Run Kaocha using the clojure
command in a terminal, using the :test/run
which runs all the tests in a project unless a test fails, then kaocha will stop.
clojure -X:test/run\n
Pass :fail-fast? false
as an argument to run all tests regardless of test failure.
clojure -X:test/run :fail-fast? false\n
Continually run tests by watching for changes using the :test/watch
alias. If a test fails, Koacha will stop the test run and restart from the failing test when a change is detected.
clojure -X:test/watch\n
Kaocha recommends adding a bin/kaocha
script to each project, although this is optional. The script calls clojure
with a suitable alias and allows for arguments to be passed to the command using \"$@\"
. Command line options will over-ride the same options in the tests.edn
file.
bin/kaocha
#!/usr/bin/env bash\nclojure -M:test/runner \"$@\"\n
Use the -M
execution option to pass command line flags to the Kaocha test runner. kaocha --fail-fast\n
"},{"location":"testing/test-runners/kaocha-test-runner/#configuring-kaocha","title":"Configuring Kaocha","text":"Kaocha can be configure by options in a tests.edn
configuration file and options passed via the command line (typically added to the bin/kaocha
script).
Create a tests.edn
file in the root of the project directory and add the default configuration.
tests.edn#kaocha/v1 {}\n
The tests.edn
file and command line options combine to make the complete configuration for the projects in the test.
make test-config
runs clojure -M:test/run --print-config
to print out the current kaocha configuration.
Use the default configuration as the basis for customising kaocha test runner for the current project.
Alternative kaocha configuration with aero juxt/aero reader literals such as #env, #merge, #ref, and #include can be used to provide different options to the kaocha configuration. For example, a file change watcher can be configured to run unless kaocha is running in CI server environment.
:kaocha/watch #profile {:default true :ci false}
"},{"location":"testing/test-runners/kaocha-test-runner/#plugins","title":"Plugins","text":"Much of the functionality of Kaocha is provide by plugins
- profiling - lists the slowest tests for each test category
- cucumber - bdd style test
- junit-xml reports - format used by Continuous Integration servers to display results
"},{"location":"testing/test-runners/kaocha-test-runner/#profiling","title":"Profiling","text":"Show the 3 slowest tests for each category of test, after the test results
MakeClojure CLIKaocha Script As a command line option:
make test-profile\n
Pass the profiling plugin as an argument to the Clojure CLI alias using the -M
(clojure.main) execution option
clojure -M:test/env:test/run --plugin kaocha.plugin/profiling\n
As a command line option:
bin/kaocha --plugin kaocha.plugin/profiling\n
Or add the profile plugin to the test.edn
configuration
#kaocha/v1\n{:plugins [:kaocha.plugin/profiling]}\n
"},{"location":"testing/test-runners/kaocha-test-runner/#example-testsedn","title":"Example tests.edn","text":" Practicalli Banking-on-Clojure project is a web application backed by a relational database, using kaocha as the test runner.
:kaocha/tests
defines two types of tests. The hash-map containing :kaocha.testable/id :unit
defines the configuration for unit tests using clojure.test
. The hash-map containing :kaocha.testable/id :generative-fdef-checks
are generative tests using clojure spec.
:kaocha/color?
and :kaocha/watch
use a value dependent on the #profile
kaocha is run under.
Banking on Clojure project - Kaocha test.edn configuration
#kaocha/v1\n{:kaocha/tests\n [{:kaocha.testable/id :unit\n :kaocha.testable/type :kaocha.type/clojure.test\n :kaocha/ns-patterns [\"-test$\"],\n :kaocha/source-paths [\"src\"],\n :kaocha/test-paths [\"test\"],\n :kaocha.filter/skip-meta [:kaocha/skip]}\n\n {:kaocha.testable/id :generative-fdef-checks\n :kaocha.testable/type :kaocha.type/spec.test.check\n :kaocha/source-paths [\"src\"]\n :kaocha.spec.test.check/checks [{:kaocha.spec.test.check/syms :all-fdefs\n :clojure.spec.test.check/instrument? true\n :clojure.spec.test.check/check-asserts? true\n :clojure.spec.test.check/opts {:num-tests 10}}]}\n ]\n\n :kaocha/reporter [kaocha.report/documentation]\n\n :kaocha/color? #profile {:default true\n :ci false}\n\n ;; Run tests of file changes, unless running in CI server\n :kaocha/watch #profile {:default true :ci false}\n\n :kaocha/fail-fast? true\n\n :kaocha.plugin.randomize/randomize? false\n\n :kaocha/plugins\n [:kaocha.plugin/randomize\n :kaocha.plugin/filter\n :kaocha.plugin/capture-output\n :kaocha.plugin.alpha/spec-test-check]\n\n :kaocha.plugin.capture-output/capture-output? true\n }\n
The configuration shows how to explicitly configure different sections, although configuration could be streamlined by using more default values.
"},{"location":"testing/unit-testing/","title":"Clojure Unit Testing","text":"The function is the unit under test in Clojure. All public functions that form the API of their respective namespace should have a matching test, i.e. (deftest)
definition.
clojure.test
namespace provides functions for defining and running unit tests and is available in the Clojure library for any project to use.
"},{"location":"testing/unit-testing/#unit-test-principles","title":"Unit Test Principles","text":" - A
test
namespace for each src
namespace under test - A
deftest
function for each function under test, named after the function its testing with -test
at the end of the name - Multiple assertions (
is
are
) for one function is
defines an assertion returning true (test pass) or false (test fail), typically a comparison between a known value and the result of a function call are
to testing similar functionality with different data sets (or use generative testing) testing
to logically group assertions and provide a meaningful description of that grouping (easier to identify tests when they fail) use-fixtures
to call fixture functions that setup and tear down any state required for test(s) to run - Test API rather than implementation
- test generic helper or private functions through public functions of each namespace (minimise test churn and time to run all tests)
^:helper
meta-data on deftest
for more generic functions, to skip those tests via a test selector - Use generative testing to create more maintainable test code with more extensive range of data
- Use test selectors with a test runner to selectively run tests and optimise speed of test runs
- Limit mocking of systems to integration tests (although mocking data is good everywhere)
Code should evaluate or have line comments All Clojure code should be valid syntax and able to be evaluated (compiled), even code within a (comment )
expression or after a #_
reader comment.
Code commented with a line comment, ;;
, will not be read by Clojure and cannot cause compilation errors when evaluated
"},{"location":"testing/unit-testing/#running-tests","title":"Running tests","text":"Test runners can run be run in the REPL used for development or run separately via the command line and continuous integration tasks.
"},{"location":"testing/unit-testing/#run-tests-in-editor-connected-repl","title":"Run tests in Editor connected REPL","text":"Using an editor connected REPL keeps the workflow in one tool and helps maintain focus. Using editor commands to run the tests and navigable error reports provides an effective flow to run and debug issues.
Ensure test
directory is on the class path when evaluating tests in the REPL, otherwise the (deftest)
test definitions may not be found.
If functions or their associated tests are changed, they should be evaluated in the REPL before running tests to ensure those changes are loaded into the REPL.
If renaming a function or deftest
, the original name should be removed from the REPL to avoid phantom tests (older definitions of tests that were evaluated in the REPL and still run, even though those tests are no longer in the source code).
Editors may include a command to remove function or test definitions, e.g. CIDER has undef
command
The original name can also be removed using Clojure (ns-unmap 'namespace 'name)
, where namespace is where the name of the function or test is defined and name is the name of the function or test.
(ns practicalli.system-monitor) ; namespace definition\n(defn dashboard [] ,,,) ; original function name\n(defn dashboard-page [] ,,,) ; new function name\n(undef 'practicalli.system-monitor 'dashboard) ; remove original function name\n
Stop and start the REPL process ensures all function and tests are correctly loaded
"},{"location":"testing/unit-testing/#command-line-test-runners","title":"Command line test runners","text":"Command line test runners (i.e. koacha, Cognitect Labs) load function and test definitions from the source code files each time, ensuring tests are run and a clean REPL state is created on each run. This clearly defined REPL state is especially valuable for running repeatable integration tests.
Automate running the tests using a watch process, giving instant fast feedback, especially when displaying both the editor and test runner command line.
test runner can be configure to run only selective tests (i.e kaocha)
Run all tests (including integration tests) via the command line before pushing commits to ensure all changes to the code have been tested.
If tests are not running in the REPL or are returning unexpected errors, a command line test runner is a useful way to diagnose if it is the test code or test tools causing the error.
The CLI approach is also more robust for longer running tests than running within an editor.
Avoid stale tests
Running tests via a command line test runner will never experience stale tests, as long as all relevant changes are saved to the source code files.
"},{"location":"testing/unit-testing/#run-tests-in-the-repl","title":"Run tests in the REPL","text":"clojure.test
includes the run-tests
function that runs tests (deftest
definitions) in given namespaces and run-all-tests
which runs all tests in all namespaces.
(run-all-tests) ; run all tests in all namespaces\n\n(run-tests 'practicalli.system-monitor-test) ; run all tests in practicalli.system-monitor-test\n
run-tests
and run-all-tests
are a less common approach as the command line and editor driven test runners provide a rich set of features
"},{"location":"testing/unit-testing/#project-structure-with-tests","title":"Project structure with tests","text":"For each source code file in src
there should be a corresponding file in test
directory with the same name and _test
postfix.
For example, code to test the src/codewars/rock_paper_scissors.clj
is saved in the file src/codewars/rock_paper_scissors_test.clj
file.
Example project: CodeWars: Rock Paper Scissors
"},{"location":"testing/unit-testing/#source-and-test-namespaces","title":"Source and Test Namespaces","text":"As with file names, the namespaces for each test code file is the same as the source code it is testing, with a -test
postfix.
codewars/rock-paper-scissors
source code namespace will have a matching codewars/rock-paper-scissors-test
namespace.
Create Projects from templates Templates typically include a parallel test
and src
directory structure. The clj-new
tool has build it templates (app, lib) and will create src
and test
directories in the projects it creates.
clojure -T:project/new :template app :name practicalli/rock-paper-scissors-lizard-spock
"},{"location":"testing/unit-testing/#project-examples-code-challenges-with-unit-tests","title":"Project Examples: Code challenges with unit tests","text":" - TDD Kata: Recent Song-list - simple tests examples
- Codewars: Rock Paper Scissors (lizard spock) solution -
and
examples - practicalli/numbers-to-words - overly verbose example, ripe for refactor
- practicalli/codewars-guides - deps.edn projects
- practicalli/exercism-clojure-guides - Leiningen projects
"},{"location":"testing/unit-testing/#references","title":"References","text":" - Example based unit testing in Clojure - PurelyFunctional.tv
"},{"location":"testing/unit-testing/clojure-test-expectations/","title":"Clojure test Expectations","text":"clojure.test.expectations
uses the same tooling as clojure.test
and only depends on that library.
"},{"location":"testing/unit-testing/clojure-test-expectations/#using-a-depsedn-alias","title":"Using a deps.edn alias","text":" Practicalli Clojure CLI Config
"},{"location":"testing/unit-testing/clojure-test-expectations/#add-dependency","title":"Add dependency","text":"Edit the deps.edn file for the current project
"},{"location":"testing/unit-testing/configure-projects-for-tests/","title":"Configure Unit Testing for deps.edn projects","text":"clojure.test
namespace is part of the Clojure standard library, so the Clojure library is the only dependency required in the project.
{:deps {org.clojure/clojure {:mvn/version \"1.10.3\"}}}\n
Unit tests code should reside under the test
directory of a project. The test
directory should not be part of the main classpath, otherwise test classes would be included in the project packaging and deployed to production.
Use an alias to add the test
directory, either from a user level configuration or the Clojure project deps.edn
configuration file.
{% tabs practicalli=\"practicalli/clojure-deps-edn\", deps=\"Manual deps.edn projects\" %}
{% content \"practicalli\" %}
"},{"location":"testing/unit-testing/configure-projects-for-tests/#adding-test-path","title":"Adding test path","text":" Practicalli Clojure CLI Config user-level configuration contains several aliases for Clojure and ClojureScript test runners, each alias includes the test
directory as an :extra-path
.
:test/env
alias is also provided, which simply adds the test
directory to the class path. The :test/env
alias is useful in concert with other aliases or for editors that have their own built in test runners (e.g. CIDER).
"},{"location":"testing/unit-testing/configure-projects-for-tests/#using-kaocha-test-runner","title":"Using kaocha test runner","text":"lambdaisland/kaocha is a fast and comprehensive test runner for Clojure and ClojureScript.
:test/run
alias runs all tests from the source code files, called with the clojure
command in the root of the Clojure project. The alias includes test
as an extra path and calls the Kaocha test runner.
clojure -X:test/run\n
Kaocha can also watch for changes saved to file and re-run the tests.
clojure -X:test/watch\n
Both kaocha aliases are configured to stop if a test fails. When re-running kaocha, only failed tests and tests that have changed are run (including tests where the code they are testing has changed).
"},{"location":"testing/unit-testing/configure-projects-for-tests/#alias-to-include-the-test-directory","title":"Alias to include the test directory","text":"Add the following aliases to the Clojure CLI tools user wide configuration, (e.g. ~/.clojure/deps.edn
), or to the project deps.edn
file.
To use a test runners with a deps.edn
projects, the test
directory should be on the classpath.
practicalli/clojure-deps-edn defines an environment alias to include the test path.
:aliases\n{\n :test/env\n {:extra-paths [\"test\"]}\n}\n
"},{"location":"testing/unit-testing/configure-projects-for-tests/#cognitect-labs-clojure-test-runner","title":"Cognitect labs Clojure test runner","text":":test/cognitect
is a simple to use test runner for Clojure projects.
clojure -X:test/cognitect\n
"},{"location":"testing/unit-testing/configure-projects-for-tests/#kaocha-unit-test-and-clojure-spec-runner","title":"Kaocha unit test and clojure spec runner","text":":test/kaocha
alias unit test runner that also supports Clojure Spec functional tests. the kaocha test runner on the current project. Add a test.edn
file to configure which tests are run by kaocha.
clojure -X:test/kaocha\n
"},{"location":"testing/unit-testing/configure-projects-for-tests/#references","title":"References","text":" - Practicalli Spacemacs - Unit testing with Cider and Kaocha in Emacs
- lambdaisland/kaocha is a test runner that supports Clojure CLI, Leiningen and Boot project configuration.
- Leiningen project configuration for unit testing
"},{"location":"testing/unit-testing/fixtures/","title":"Test Fixtures","text":"Unit tests may require the system to be in a particular state before running a test. The state may need to be reset after running a test such as a database
Fixtures allow you to run code before and after tests, to set up the context in which tests should be run. Consider when fixtures should be run, especially fixtures that take a noticeable time to setup or tear down.
Slow running unit tests lead to unit tests not being run so often and therefore limit their value.
Organise tests with test selectors
Tests with fixtures may be slower to run so separate them by using a test selector, a piece of meta data attached to a deftest
definition. For example, add the ^:persistence
meta data to test that require database fixtures (deftest ^:database db-bulk-upload). The test runner can be instructed to skip or focus on tests with specific meta data.
"},{"location":"testing/unit-testing/fixtures/#defining-a-fixture","title":"Defining a fixture","text":"Require the use-fixtures
function in the require expression for clojure.test
(ns domain.application-test\n (:require [clojure.test :refer [deftest is testing use-fixtures]]))\n
A fixture is a standard Clojure function which takes a function as an argument. The function passed as an argument is either an individual test or all tests in the namespace, depending on how the fixture is used.
(defn my-fixture [test-run]\n ;; Setup: define bindings, create state, etc.\n\n (test-run) ;; Run the relevant tests for the fixture (see `use-fixtures`)\n\n ;; Tear-down: reset state to a known value\n )\n
"},{"location":"testing/unit-testing/fixtures/#when-to-run-fixtures","title":"When to run fixtures","text":"The use-fixtures
function defines when a fixture should be called when running the unit tests in each namespace. All Clojure unit test runners should support the use-fixtures
definitions when running the tests.
When Description (use-fixtures :once fixture1 fixture2)
Run the fixtures once for the namespace. (use-fixtures :each fixture1 fixture2)
Run the fixtures for each deftest
in the namespace Once
The setup in the fixture is run, followed by all the deftest
functions in the namespace, then the fixture tear-down is run.
Running a fixture once per namespace is useful for establishing a database connection or creating a particular state of data for all the unit tests to use.
Each
The fixture setup is run before each deftest
function in the namespace. The fixture tear-down is run after each deftest
function.
"},{"location":"testing/unit-testing/fixtures/#anonymous-function-fixture","title":"Anonymous function fixture","text":"The use-fixtures
function can also include anonymous function as well as a namespace scoped functions (deftest
).
(use-fixtures :each (fn [f] #_setup... (f) #_teardown))\n
defn
functions are usually recommended unless the fixture code is relatively terse.
Development database
Define a fixture to reset the database before running a test and clear the database after each test.
The create-database
and delete-database
are helper functions that are part of the namespace under test.
(defn database-reset-fixture\n \"Setup: drop all tables, creates new tables\n Teardown: drop all tables\n SQL schema code has if clauses to avoid errors running SQL code.\n Arguments:\n test-function - a function to run a specific test\"\n [test-function]\n (SUT/create-database)\n (test-function)\n (SUT/delete-database))\n
The fixture should be used for each unit test (deftest
) that is defined in the namespace the database-reset-fixture
is defined in.
(use-fixtures :each database-reset-fixture)\n
"},{"location":"testing/unit-testing/fixtures/#references","title":"References","text":" use-fixtures
- Clojuredocs.org - Kaocha - focusing and skipping
- Clojure Test Fixtures - astrocaribe
"},{"location":"testing/unit-testing/test-selectors/","title":"Test Selectors","text":"As a project grows in scope its important that tests continue to run quickly. Test runs which take a noticeable time to complete diminish the motivation to run tests frequently.
Divide tests into categories to run selective tests, continuing to provide fast feedback. Longer running tests can be run less often without loosing quality in the feedback from tests.
Test runners use test selectors to run a specific categories, or exclude test selectors so all tests except that category runs.
kaocha focus and skipping
kaocha can group tests into categories in the tests.edn
configuration, providing a way to focus or exclude different types of tests (e.g. :unit
and :spec
)
"},{"location":"testing/unit-testing/test-selectors/#test-category-metadata","title":"Test category metadata","text":"Add metadata to deftest
functions to provide categories of tests, e.g. integration
, persistence
, etc.
(deftest ^:integration register-customer\n (is ,,,))\n
Example from Banking on Clojure
(deftest ^:persistence new-customer-test\n (testing \"New customer generative testing\")\n (is (spec/valid?\n :customer/id\n (:customer/id (SUT/new-customer\n (spec-gen/generate (spec/gen :customer/unregistered)))))))\n
"},{"location":"testing/unit-testing/test-selectors/#test-selectors_1","title":"Test Selectors","text":"Start a test selective category of tests running by specifying test selectors to include or exclude.
KaochaSpacemacsCiderCognitect kaocha supports meta data on deftest
expressions and has its own metadata tag for skipping tests, ^:koacha/skip
Examples of tests with and without test selectors
(deftest simple-test\n (is (= 1 1)))\n\n(deftest ^:integration system-update-test\n (is (spec/valid? :system/update (long-running-function))))\n\n(deftest ^:kaocha/skip under-development-test\n (is (= 3 21/7)))\n
Tests with test selector metadata can be skipped using a tests.edn
configuration
#kaocha/v1\n{:tests [{:kaocha.filter/skip-meta [:integration]}]}\n
Running kaocha will only run the simple-test
, skipping the other two tests.
Specifying --skip-meta
on the command line gives the same results
bin/kaocha --skip-meta :metadata-name\n
Running tests with the universal argument will prompt for test selector filters and only Run those tests that match the selector inclusions/exclusions.
SPC u , t a
runs all tests, prompting for tests selector names to include (space separated)
Then prompting for the test selectors to exclude. A warning displays if CIDER does not find the test selector name.
Invoke the CIDER test runner commands with the universal argument and CIDER will prompt for test selector filters, running only those tests that match the selector inclusions/exclusions.
C-c C-t p
runs all the tests in a project.
C-u C-c C-t p
prompts for test selectors and runs the matching tests in a project.
C-c C-t l
runs all tests currently evaluated in the REPL.
C-u C-c C-t l
prompts for test selectors and runs the matching tests currently evaluated in the REPL.
CIDER first prompts for the test selectors to include:
Then prompts for the test selectors to exclude. A warning displays if CIDER does not find the test selector name.
The Cognitect Labs test runner uses command line options to specify test selectors, --include
and --exclude
.
Practicalli Clojure CLI Config configuration provides the :test/congnitect
alias.
clojure -M:test/cognitect --include :database
only runs tests with the ^:database
test selector
clojure -M:test/cognitect --exclude :integration
runs all tests except those with the ^:integration
test selector
"},{"location":"testing/unit-testing/test-selectors/#references","title":"References","text":" - Kaocha - Focus and skipping tests with test selectors
- Convoluted Magic of Leiningen Test Selectors
- How to use Leiningen test selectors to filter by test name
- Stack overflow - Lein test with Selectors - how to specify a test for multiple conditions
"},{"location":"testing/unit-testing/writing-unit-tests/","title":"Writing Unit Tests with clojure.test","text":"Unit tests are centered on assertions, testing if something returns a true or false value.
is
function is the simplest assertion and the most common. It checks to see if an expression given is true and if so then the assertion passes. If the value is false then that assertion fails.
as
provides a way to run the same assertion with different values, testing the same function with a collection of arguments. This provides a clean way to test a function without lots of repetition.
testing
is a macro to group multiple assertions together, providing a string in which to describe the context the assertions are testing. The well worded context string is invaluable for narrowing down on which assertions are failing.
deftest
is a collection of assertions, with or without testing
expressions. The name of the deftest should be the name of the function it is testing with -test
as a postfix. For example, the function practicalli.playground/calculator
would have a deftest
called practicalli.playground-test/calculator-test
"},{"location":"testing/unit-testing/writing-unit-tests/#requiring-namespaces","title":"Requiring Namespaces","text":"A test namespace has a singular purpose to test a matching src namespace.
The idiomatic approach is to :refer
specific functions from clojure.test
as those functions are used.
The namespace to be tested is referred using a meaningful alias. The alias highlight the exact functions being tested in the body of the code. This provides a visual way to separate functions under test with other test functions, especially if there are helper functions or vars used for test data.
REPLproject (require '[clojure.test :refer [are deftest is testing]])\n
The namespace under test should be referred using the alias so they are readily identified within the test code. (require '[practicalli.gameboard.spec :as gameboard-spec])\n
Add clojure.test
to the namespace definition along with the namespace under test.
(ns practicalli.app-namespace-test\n (:require '[clojure.test :refer [are deftest is testing]]\n [practicalli.gameboard.spec :as gameboard-spec]))\n
"},{"location":"testing/unit-testing/writing-unit-tests/#simple-example","title":"Simple Example","text":"(deftest public-function-in-namespace-test\n (testing \"A description of the test\"\n (is (= 1 (public-function arg)))\n (is (predicate-function? arg))))\n
"},{"location":"testing/unit-testing/writing-unit-tests/#assertion-data-set","title":"Assertion data set","text":"The are
macro can also be used to define assertions, especially when there would otherwise be multiple assertions that only differ by their test data.
An are
assertion defines the arguments to the test, the logic of the test and a series of test data.
(are [x y] (= x y)\n 2 (+ 1 1)\n 4 (* 2 2))\n
This is equivalent to writing
(do (is (= 2 (+ 1 1)))\n (is (= 4 (* 2 2))))\n
Refactor test assertion to use data set
Assertions in the test take the same shape of values, so are candidates to refactor to the are
macro.
(deftest encoder-test\n (testing \"Tens to number words\"\n (is (= '(\"zero\" \"ten\")\n (character-sequence->word-sequence dictionary/digit->word '(\\0 \\1 \\0))))\n (is (= '(\"zero\" \"eleven\")\n (character-sequence->word-sequence dictionary/digit->word '(\\0 \\1 \\1))))\n (is (= '(\"zero\" \"twenty\" \"zero\")\n (character-sequence->word-sequence dictionary/digit->word '(\\0 \\2 \\0))))\n (is (= '(\"zero\" \"twenty\"\"one\")\n (character-sequence->word-sequence dictionary/digit->word '(\\0 \\2 \\1))))\n (is (= '(\"zero\" \"forty\" \"two\")\n (character-sequence->word-sequence dictionary/digit->word '(\\0 \\4 \\2))))))\n
Refactor the assertions using are simplifies the code, making it simpler to change further and extend with more data. (deftest encoder-test\n (testing \"Tens to number words\"\n (are [words numbers]\n (= words (character-sequence->word-sequence dictionary/digit->word numbers))\n '(\"zero\" \"ten\") '(\\0 \\1 \\0)\n '(\"zero\" \"eleven\") '(\\0 \\1 \\1)\n '(\"zero\" \"twenty\" \"zero\") '(\\0 \\2 \\0)\n '(\"zero\" \"twenty\"\"one\") '(\\0 \\2 \\1)\n '(\"zero\" \"forty\" \"two\") '(\\0 \\4 \\2)))\n
Generative Testing provides a wide range of values
Generating test data from Clojure Specs provides an extensive set of values that provide an effective way to test functions.
"},{"location":"testing/unit-testing/writing-unit-tests/#reference","title":"Reference","text":"clojure.test API
"},{"location":"testing/unit-testing/writing-unit-tests/#code-challenges-with-tests","title":"Code challenges with tests","text":"TDD Kata: Recent Song-list TDD Kata: Numbers in words Codewars: Rock Paper Scissors (lizard spock) solution practicalli/codewars-guides practicalli/exercism-clojure-guides
"},{"location":"thinking-functionally/","title":"Thinking Functionally","text":"In this section I cover some simple examples of Clojure code to help you think about the concepts involved in functional programming.
An overview of thinking functionally is also covered in the presentation entitled Getting into Functional Programming with Clojure on slideshare and its accompanying youtube video
Get into Functional Programming with Clojure from John Stevenson Get a free Clojurians slack community account
"},{"location":"thinking-functionally/arity/","title":"Arity","text":"Fixme work in progress
"},{"location":"thinking-functionally/example-hitchhikers-guide/","title":"Example: Hitchhikers Guide","text":"This is an example of using the threading macros and a REPL to give fast feedback as you are developing code.
Suggest you use the assumed perfectly legal copy of the Hitch-hickers book text using the slurp
function
Approximate algorithm
- Use a regular expression to create a collection of individual words - eg. #\"[a-zA-Z0-9|']+\"
- Convert all the words to lower case so they match with common words source -
clojure.string/lower-case
Remove
the common English words used in the book, leaving more context specific words - Calculate the
frequencies
of the remaining words, returning a map of word & word count pairs Sort-by
word count values in the map Reverse
the collection so the most commonly used word is the first element in the map
(def book (slurp \"http://clearwhitelight.org/hitch/hhgttg.txt\"))\n\n(def common-english-words\n (-> (slurp \"https://www.textfixer.com/tutorials/common-english-words.txt\")\n (clojure.string/split #\",\")\n set))\n\n;; using a function to pull in any book\n(defn get-book [book-url]\n (slurp book-url))\n\n\n(defn -main [book-url]\n (->> (get-book book-url)\n (re-seq #\"[a-zA-Z0-9|']+\")\n (map #(clojure.string/lower-case %))\n (remove common-english-words)\n frequencies\n (sort-by val)\n reverse))\n\n;; Call the program\n\n(-main \"http://clearwhitelight.org/hitch/hhgttg.txt\")\n
"},{"location":"thinking-functionally/example-hitchhikers-guide/#note","title":"NOTE","text":"Write functions that will give a list of the most used words used in a book, excluding the common English words like \"the, and, it, I\". Join those functions with a threading macro.
"},{"location":"thinking-functionally/example-hitchhikers-guide/#deconstructing-the-code-in-the-repl","title":"Deconstructing the code in the repl","text":"To understand what each of the functions do in the -main
function then you can simply comment out one or more expressions using in front of the expression #_
(defn -main [book-url]\n (->> (get-book book-url)\n #_(re-seq #\"[a-zA-Z0-9|']+\")\n #_(map #(clojure.string/lower-case %))\n #_(remove common-english-words)\n #_frequencies\n #_(sort-by val)\n #_reverse))\n
Now the -main
function will only return the result of the (get-book book-url)
function. To see what each of the other lines do, simply remove the #_ character from the front of an expression and re-evaluate the -main
function in the repl
Hint In Spacemacs / Emacs, the keybinding C-c C-p show the output in a separate buffer. Very useful when the function returns a large results set.
"},{"location":"thinking-functionally/example-hitchhikers-guide/#off-line-sources-of-hitch-hickers-book-and-common-english-words","title":"Off-line sources of Hitch-hickers book and common English words","text":"(def book (slurp \"./hhgttg.txt\"))\n\n(def common-english-words\n (-> (slurp \"common-english-words.txt\")\n (clojure.string/split #\",\")\n set))\n
Original concept from Misophistful: Understanding thread macros in clojure
Hint The slurp
function holds the contents of the whole file in memory, so it may not be appropriate for very large files. If you are dealing with a large file, consider wrapping slurp in a lazy evaluation or use Java IO (eg. java.io.BufferedReader
, java.io.FileReader.
). See the Clojure I/O cookbook and The Ins & Outs of Clojure for examples.
"},{"location":"thinking-functionally/first-class-functions/","title":"First Class functions","text":"Idempotent - given the same input you get the same output
(+ 1 2 3 4 5 6 7 8 9 10)\n
The range
function generates a sequence of numbers and when given arguments it does so from a specific range. The second number is exclusive, so for 1 to 10 the second argument should be 11.
(range 1 11)\n
Unfortunately we cant just add the result of a range, because it returns a lazy sequence So (range)
by itself will create an error
(+ 1 (range 1 11))\n
Using a function called reduce
we can calculate a single total value from all the numbers in the collection.
The reduce function take 2 arguments, the first is the function to apply to a data structure, the second is the data structure.
(reduce + (range 1 11))\n\n(reduce + (1 2 3 4 5 6 7 8 9 10))\n
"},{"location":"thinking-functionally/first-class-functions/#note","title":"Note::","text":"Write an expression to add up the numbers from 1 to 10 and return the overall total.
"},{"location":"thinking-functionally/first-class-functions/#note_1","title":"Note::","text":"Create an expression to do the same calculation, but without having to write all the numbers. Hint: consider the functions called range and reduce.
"},{"location":"thinking-functionally/function-composition/","title":"Function Composition","text":"We have discussed how functional programs are essentially a number of functions that work together, this is called composition (functional composition).
(let [calculated-value (* 10 (reduce + (map inc (range 5))))]\n calculated-value)\n
This expression is common in the Lisp & Clojure languages. Occasionally the created expressions can becomes challenging to read. To overcome this parsing complexity, developers often break down a more complex expression into its parts, extracting code into its own function.
Note Brake down the above example into each expression that gives a value
(range 5)\n\n(map inc (range 5))\n\n(reduce + (map inc (range 5)))\n\n(* 10 (reduce + (map inc (range 5))))\n\n\n;; Additional examples\n\n;; Use a let expression for code that is used more than once in a function\n\n(let [calculated-value (* 10 (reduce + (map inc (range 5))))]\n calculated-value)\n\n;; Use defn to define a function for code that multiple functions will call\n;; and generalise the function with arguments\n\n(defn common-data-calculation\n [certainty-factor scope]\n (* certainty-factor (reduce + (map inc (range scope)))))\n
"},{"location":"thinking-functionally/functors/","title":"Functors","text":"Fixme work in progress
Put simply, a function that takes a value and a function as its arguments, eg map
. The argument pass as a value is most commonly a collection type (vector, map, string, list).
From Wikipedia
In mathematics, a functor is a type of mapping between categories which is applied in category theory. Functors can be thought of as homomorphisms between categories. In the category of small categories, functors can be thought of more generally as morphisms.
A functor applies the given function to each element in the the collection by unpacking and each element from the collection and passing it to the function as an argument. The result from each application of the function from the element of the collection is put into a new collection. This new collection is returned once all elements of the original collection have been processed.
The function, eg. + is applied in turn to each value and returns a structured value as a result, eg. a list or vector
(map inc [1 2 3 4 5])\n\n(inc 1 )\n
"},{"location":"thinking-functionally/higher-order-functions/","title":"Higher Order functions","text":"Functions can be used as an arguments to other functions as we have seen in function composition. This is possible because a function always evaluates to a value. This is the basis of function composition.
Higher Order functions can also return a function definition, as when that function definition is evaluated it to will return a value.
You could have a function that returns a function definition which in turn returns a function definition, but at some point this will get very confusing for the developers (yes, that means you).
(filter\n even?\n (range 1 10))\n
(defn twice [f]\n ,,,)\n
;; Our higher order function\n\n(defn twice [function x]\n (function (function x)))\n\n(twice\n (fn [arg]\n (* 3.14 arg))\n 21)\n;; => 207.0516\n\n;; using the short syntax for a function definition\n\n(twice #(+ 3.14 %) 21)\n;; => 207.0516\n
(defn calculation [f]\n ,,,)\n
(defn calculation [f]\n (fn [& args]\n (reduce f args)))\n\n((calculation +) 1 1 2 3 5 8 13)\n\n;; The result of `(calculation +)` is also in a list,\n;; so it will be called as a function, with the arguments 1 1 2 3 5 8 13\n
"},{"location":"thinking-functionally/higher-order-functions/#notereturn-the-even-numbers-from-1-to-10","title":"Note::Return the even numbers from 1 to 10","text":"Generate a range of numbers from 1 to 10
Use a function that checks if a number is even and filter the range of numbers to return only the numbers that match
"},{"location":"thinking-functionally/higher-order-functions/#notecreate-a-named-function-as-a-higher-order-function-called-twice","title":"Note::Create a named function as a higher order function called twice
","text":"The function twice which takes a function and value as arguments.
The twice function should call the function passed as an argument on the value passed as an argument.
The result should be then used as an argument to calling the function passed as an argument again.
Call the twice function with an inline function which takes a number as an argument and adds it to Pi, 3.14
.
"},{"location":"thinking-functionally/higher-order-functions/#notedefine-a-function-that-returns-a-function","title":"Note::Define a function that returns a function","text":"The function should take a clojure.core function for a mathematical calculation, i.e. +
, -
, *
, /
The returning function should take one or more arguments [& args]
and use the function originally passed as an argument to reduce
the data to a single value.
"},{"location":"thinking-functionally/higher-order-functions/#references","title":"References","text":" - Writing Elegant Clojure code using Higher Order functions
"},{"location":"thinking-functionally/homoiconicity/","title":"Homoiconicity","text":"Clojure is a homoiconic language, which is a term describing the fact that Clojure programs are represented by Clojure data structures.
In Clojure you write your business logic as functions. A function is defined using a list structure. A function is called using a list structure, as the first element of a list is evaluated as a function call.
Hint Everything in Clojure is a List (or vector, map, set).
This is a very important difference between Clojure (and Common Lisp) and most other programming languages - Clojure is defined in terms of the evaluation of data structures and not in terms of the syntax of character streams/files.
It is quite easy for Clojure programs to manipulate, transform and produce other Clojure programs. This is essentially what macros do in Clojure, they re-write Clojure for you.
Hint If you were going to create Skynet, it would be so much easier to do in Clojure
"},{"location":"thinking-functionally/homoiconicity/#an-example","title":"An example","text":"Consider the following expression:
(let [x 1] \n (inc x))\n
Evaluating the above code in the REPL returns 2
because the repl compiles and executes any code entered into it. But [x 1]
is also a literal vector data structure when it appears in a different context.
All Clojure code can be interpreted as data in this way. In fact, Clojure is a superset of EDN \u2013 Extensible Data Notation, a data transfer format similar to JSON. EDN supports numbers, strings, lists (1 2 3), vectors [1 2 3], maps {\"key\" \"value\"}.
If this sounds and looks a lot like Clojure syntax, it\u2019s because it is. The relationship between Clojure and EDN is similar to that of Javascript and JSON, but much more powerful.
In Clojure, unlike JavaScript, all code is written in this data format. We can look at our let statement not as Clojure code, but an EDN data structure. Let\u2019s take a closer look:
(let [x 1] \n (inc x))\n
In this data structure, there are four different types of data.
- 1 is a literal integer.
- let, x, and inc are symbols. A symbol is an object representing a name \u2013 think a string, but as an atomic object and not a sequence of characters.
- [x 1] is a vector containing two elements: symbol, x, and an integer, 1. Square brackets always signify vectors when talking about EDN data structures.
- (inc x) is a list (a linked list data structure) containing two symbols, inc and x.
When thinking about a piece of Clojure code as a data structure, we say we are talking about the form. Clojure programmers don\u2019t normally talk about EDN, there are just two ways to think about any bit of Clojure: 1) as code that will execute or 2) as a form, a data structure composed of numbers, symbols, keywords, strings, vectors, lists, maps, etc.
Symbols are particularly important. They are first class names. In Clojure, we distinguish between a variable and the name of that variable. When our code is executing, x refers to the variable established by our let binding. But when we deal with that code as a form, x is just a piece of data, it\u2019s a name, which in Clojure is called a symbol.
This is why Clojure is homoiconic. Code forms are data structures and data structures can be thought of as forms and executed as code. This transformation is quite literal, and two core operations, quote and eval are key ingredients to this potion.
"},{"location":"thinking-functionally/homoiconicity/#references","title":"References","text":" - The Reader - Clojure. org
- Homoiconicity - Wikipedia
- Is Clojure Homoiconic - muhuk.com
- Understanding Homoiconicity in Clojure - Drew Colthorp
"},{"location":"thinking-functionally/immutability/","title":"Immutability","text":"There is a strong emphasis on immutability in Clojure. Rather than create variables that change, Clojure uses values that do not change.
Values in Clojure include numbers, characters, strings.
When functions act on values, a new value is created and returned, rather than modifying the existing value.
TODO include a diagram to visualise this...
"},{"location":"thinking-functionally/immutability/#immutabile-data-structures","title":"Immutabile data structures","text":"List, Map, Vector and Set are all immutable data structures in Clojure.
So when you use these data structures with a function, a new data structure is returned.
Hint When a new data structure is created from an existing data structure, then under the covers the two data structures actually share memory use for any elements that are common. This keeps copies very cheap to create in terms of memory used.
See the section on data structures for more details.
"},{"location":"thinking-functionally/immutable-collections/","title":"Immutable collections","text":"As we have discussed, immutable data structures cannot be changed. So when you run a function over a collection a copy of that collection is returned. Lets see this by running some code in the REPL.
Note Define a data structure called numbers
using a vector. Then write a function that uses the map
and inc
function to increment all the numbers in a vector.
Then check the current value of the numbers
data structure by evaluating its name.
;; define the data structure \n(defn numbers [1 2 3 4 5])\n\n;; increment the numbers\n(map inc numbers)\n\n;; see the current value of numbers\nnumbers\n
Note Use the conj
function to first add the number 5
to the numbers
vector from the previous exercise and check the value of numbers
. Then add the number 6
to the numbers
vector and check the value of numbers
.
Finally, use the conj
function to add both 5
and 6
to the numbers
vector and check the value of numbers
(def numbers [1 2 3 4])\n\n;; add 5 to the numbers vector\n(conj numbers 5)\n\n;; check the value of numbers\nnumbers\n;; => [1 2 3 4]\n\n;; add 6 to the numbers vector\n(conj numbers 6)\n\n;; check the value of numbers\nnumbers\n;; => [1 2 3 4]\n\n;; add 5 and 6 to the numbers vector\n(conj numbers 5 6)\n\n;; Alternatively, you can use the threading macro to chain two conj function calls\n(-> numbers\n (conj 5)\n (conj 6))\n\n;; check the value of numbers\nnumbers\n;; => [1 2 3 4]\n
So even though we have applied several functions on the numbers
data structure it still has the same value.
"},{"location":"thinking-functionally/immutable-local-bindings/","title":"Immutable Local Bindings","text":"Names can be bound to values & and data structures with either the def
or let
function. The def
binding is global to the namespace, however the let
function is local to its use.
Hint The let
function is typically used to define names within a function definition, or in snippets of code created during repl driven development.
(let [five 5]\n (str \"Within the let expression the value is \" five))\n;; => Within the let expression the value is 5\n\n;; evaluating the name five outside the let expression returns an error\nfive\n;; => Unable to resolve symbol: five in this context\n
Note Create a local binding called number that represents the value 5 using the let
function. Increment the number, then print out the value of number.
(let [number 5]\n (inc number)\n (str \"The number is still \" number))\n
So the value that any local binding points to is immutable too.
"},{"location":"thinking-functionally/immutable-values/","title":"Immutable values","text":"Fixme work in progress
Values in Clojure include numbers, characters and strings. When you use functions on these values they do not change, instead a new value is returned.
Lets look at a simple example with a number:
(def two-little-ducks 22)\n\n(inc two-little-ducks)\n;; => 23\n\ntwo-little-ducks\n;; => 22\n
Another example with a string:
(def message \"Strings are immutable\")\n\n(str message \",\" \" \" \"you cant change them\")\n;; => \"Strings are immutable, you cant change them\"\n\nmessage\n;; => \"Strings are immutable\"\n
Fixme Add an exercise
"},{"location":"thinking-functionally/impure-functions/","title":"Impure functions","text":"We have seen some simple examples of pure functions, so lets see impure functions as a comparison.
(def global-value '(5 4 3 2 1))\n\n(defn impure-increment-numbers [number-collection]\n (map inc global-value))\n\n(impure-increment-numbers '(1 2 3 4 5))\n
The above function is using a global value rather than the argument passed makes this function deterministic
"},{"location":"thinking-functionally/impure-functions/#side-effect-printing-to-the-console-log","title":"Side Effect: Printing to the console log","text":"Although the following example is probably quite harmless, it is a simple example of a function effecting the state of something outside. These side effects should be avoided where possible to keep your code simpler to reason about.
(defn print-to-console [value-to-print]\n (println \"The value is:\" value-to-print))\n\n(print-to-console \"a side effect\")\n
"},{"location":"thinking-functionally/impure-functions/#side-causes-calling-libraries","title":"Side Causes: Calling libraries","text":"To demonstrate a side causes form of impure functions, lets create a task-comple function that marks a current task complete using the current timestamp.
(defn task-complete [task-name]\n (str \"Setting task \" task-name \" completed on \" (js/Date)))\n\n(task-complete \"hack clojure\")\n
(:import java.util.Date)\n\n(defn task-complete [task-name]\n (str \"Setting task \" task-name \" completed on \" (java.util.Date.)))\n\n(task-complete \"hack clojure\")\n
In this example we have called to the outside world to generate a value for us. The above example is fairly simple, however by calling the outside world rather than passing in a date it makes the functions purpose far less clear.
"},{"location":"thinking-functionally/impure-functions/#hint-the-function-javautildate-is-actually-a-call-to-instantiate-a-javautildate-object-the-full-stop-character-at-the-end-of-the-name-makes-it-a-function-call-and-is-the-short-form-of-new-javautildate","title":"Hint:: The function (java.util.Date.)
is actually a call to instantiate a java.util.Date object. The full-stop character at the end of the name makes it a function call and is the short form of (new java.util.Date)
","text":""},{"location":"thinking-functionally/impure-functions/#re-write-as-a-pure-function","title":"Re-write as a pure function","text":"Change the task-complete function definition and function call to take both the task-name and completed-date as arguments.
(defn task-complete [task-name completed-date]\n (str \"Setting task \" task-name \" completed on \" completed-date))\n\n(task-complete \"hack clojure\" (js/Date))\n
Required values should be generated outside a function where possible. In this case in the (js/Date)
function is first evaluated and replaced by its value, then that date value is passed to the function as an argument, keeping the function pure.
The pure version of the function in Clojure, using the java.util.Date function.
(:import java.util.Date)\n\n(defn task-complete [task-name completed-date]\n (str \"Setting task \" task-name \" completed on \" completed-date))\n\n(task-complete \"hack clojure\" (java.util.Date.))\n
"},{"location":"thinking-functionally/iterate-over-values/","title":"iterate Over Values","text":"This
- loop recur
- reducing functions
- map apply reduce
- partition group-by sort-by
"},{"location":"thinking-functionally/iterate-over-values/#hintwork-in-progress","title":"Hint::Work in progress","text":""},{"location":"thinking-functionally/iterate-over-values/#loop-recur","title":"loop recur","text":"loop recur is a very detailed way of defining a way to iterate over values. map, reduce and apply are commonly used abstractions for iterating over values. They simplify the code (once you are comfortable with them)
Functions that iterate over values usually treat a string as a sequence of characters.
"},{"location":"thinking-functionally/lazy-evaluation/","title":"Lazy Evaluation","text":"Fixme work in progress
In the most basic way possible, laziness is the ability to evaluate an expression only when it's actually needed. Taken further, laziness is also evaluating an expression only to the extent required.
"},{"location":"thinking-functionally/lazy-evaluation/#laziness-in-definition","title":"Laziness in definition","text":""},{"location":"thinking-functionally/lazy-evaluation/#laziness-in-evaluation","title":"Laziness in evaluation","text":""},{"location":"thinking-functionally/lazy-evaluation/#laziness-in-partial-evaluation","title":"Laziness in partial evaluation","text":"Clojure is not entirely lazy, only the majority of sequence operations like map, reduce, filter or repeatedly are lazy evaluated.
The most common use of laziness are infinite lists or streams. For example, we could define a list of all prime numbers. In case you didn't know, that's a lot of prime numbers (infinitely many).
If we would define such list in a language like C++ or Python then the language would try to calculate all prime numbers immediately, which would run literally forever.
If we define such list in Haskell or Clojure, then nothing is calculated just yet. As a matter of fact we could happily print out the first 1000 prime numbers from that list without running into a problem. Again, because lazy evaluation only calculates what is really needed, and nothing more.
"},{"location":"thinking-functionally/lazy-evaluation/#laziness-in-number-calculation-ratio-type","title":"Laziness in number calculation - Ratio type","text":"Dividing an integer value by another results in a Ratio type if the result would otherwise result in a decimal number. Clojure only partially evaluates this expression.
(/ 22 7)\n
We can also just express a value as a ratio. This works because of the prefix notation of Clojure
22/7\n
The laziness can be overridden by specifying a precision, eg coercing the result into a specific type
(/ 22 7.0)\n(double (/ 22 7))\n(float (/ 22 7))\n
"},{"location":"thinking-functionally/lazy-evaluation/#making-something-lazy","title":"Making something lazy","text":"The range
function returns a sequence of numbers limited by any arguments given when calling the range function.
Calling the range function without arguments will force an infinite sequence of numbers to be generated, quickly resulting in an out of memory error in the heap.
Instead, we can either pass arguments to the range function that limit the sequence size or wrap the range function in another function
(take 7 (range))\n
The take
function defines how much of a sequence that range
should generate. So we can call range without arguments and it will generate only those numbers in the sequence as specified by take
.
"},{"location":"thinking-functionally/lazy-evaluation/#references","title":"References","text":" - Being lazy in Clojure - lazily generating monsters
"},{"location":"thinking-functionally/list-comprehension/","title":"List Comprehension","text":"In general terms, list comprehensions should:
- be distinct from (nested) for loops and the use of map & filter functions within the syntax of the language.
- return either a list or an iterator (an iterating being something that returns successive members of a collection, in order),
In Clojure, list comprehension is via the for
function. This is different to the for in other languages as you will see.
(for [number [1 2 3]] (* number 2))\n
The for
function should be read as follows:
\"for each number in the collection [1 2 3], apply the function (* number 2)\"
Couldn't we just do this with map? Yes, we could.
(map #(* % 2) [1 2 3])\n
So why do we need for
function? It really shows its value when you are working with multiple collections
(for [number [1 2 3]\n letter [:a :b :c]]\n (str number letter))\n
Again we could use map
function for this as follows
(mapcat (fn [number] (map (fn [letter] (str number letter)))))\n
So with the for
function we can do the same calculation with much easier code to reason about.
"},{"location":"thinking-functionally/list-comprehension/#filtering-results-with-predicates","title":"Filtering results with predicates","text":"With the for
function we can add a filter on the results by using a predicate, to test if a condition is true or false. Any values that meet the condition as true are returned, values that are false are omitted.
(for [x (range 10) :when (odd? x)] x)\n\n(for [x (range 10) :while (even? x)] x)\n
To do this kind of filtering with maps would be possible, however the code would be harder for humans to parse and understand.
Note Create a 3-tumbler combination padlock, with each tumbler having a range of 0 to 9. Count the number of possible combinations. Then add a predicate that filters out some of the combinations
Lets just model all the possible combinations
(for [tumbler-1 (range 10)\n tumbler-2 (range 10)\n tumbler-3 (range 10)]\n [tumbler-1 tumbler-2 tumbler-3])\n
Now lets count the combinations
(count (for [tumbler-1 (range 10)\n tumbler-2 (range 10)\n tumbler-3 (range 10)]\n [tumbler-1 tumbler-2 tumbler-3]))\n
Now add a predicate using :when
to filter out the combinations that do not match.
(count (for [tumbler-1 (range 10)\n tumbler-2 (range 10)\n tumbler-3 (range 10)\n :when (or (= tumbler-1 tumbler-2)\n (= tumbler-2 tumbler-3)\n (= tumbler-3 tumbler-1))]\n [tumbler-1 tumbler-2 tumbler-3]))\n
Note Create a 2 character prefix for tickets, using capital letters from the English alphabet. However, exclude I and O as they can be mistaken for numbers
Lets just model all the possible combinations
(for [letter-1 capital-letters\n letter-2 capital-letters\n :when (and (not (blacklisted letter-1))\n (not (blacklisted letter-2)))]\n (str letter-1 letter-2))\n
"},{"location":"thinking-functionally/managing-state-changes/","title":"Managing state changes","text":""},{"location":"thinking-functionally/map-with-partial/","title":"map with partial","text":"Lets look at different ways we can map functions over collections with partial
We can map over a collection of words and increment them by writing an anonymous function.
(map (fn [animal] (str animal \"s\")) [\"pig\" \"cow\" \"goat\" \"cat\" \"dog\" \"rabbit\"])\n
The anonymous function has a terse form, that removes the boiler plate function definition (fn [])
, allowing definition of only the body of a function.
%
represents a single argument passed to the function. The %
syntax also supports numbers where there are multiple arguments, e.g. %1
, %2
for the first and second arguments. %&
represents all other arguments and is the same as (fn [& args])
or (fn [arg1 & args])
.
The #
character tells the Clojure reader that this is the macro form of a function definition and expands the code to the full form before executing.
(map #(str % \"s\") [\"pig\" \"cow\" \"goat\" \"cat\" \"dog\" \"rabbit\"])\n
"},{"location":"thinking-functionally/map-with-partial/#hintwhen-to-use-the-terse-form-of-anonymous-function","title":"Hint::When to use the terse form of anonymous function","text":"The terse form is often used with higher order functions, as an argument to a function. If the body of the function is simple to comprehend, then the terse form of anonymous function definition is appropriate. When the body of a function is more complex, then consider using a separate defn
function definition.
"},{"location":"thinking-functionally/map-with-partial/#returning-a-vector-instead-of-a-sequence","title":"Returning a Vector instead of a sequence","text":"The map
function returns a lazy sequence. This is very useful for large data sets.
mapv
is an eager version of map that returns the result as a vector. This is useful when you require random access lookup in real time. mapv
can also be used to return an eager result if laziness is not required.
(mapv #(str % \"s\") [\"pig\" \"cow\" \"goat\" \"cat\" \"dog\" \"rabbit\"])\n
"},{"location":"thinking-functionally/map-with-partial/#hintlists-and-vectors-does-it-matter","title":"Hint::Lists and vectors - does it matter?","text":"Some functions in clojure.core
will return a sequence using the list syntax, even if the arguments given are vectors. Most of the time this is not important, as Clojure considers values rather than constructs for most of its functions. For example, (= (\"pig\" \"cow\" \"goat\" \"cat\" \"dog\" \"rabbit\") [\"pig\" \"cow\" \"goat\" \"cat\" \"dog\" \"rabbit\"])
is true as the values are compared rather than the type of container (list, vector)
"},{"location":"thinking-functionally/map-with-partial/#using-conditionals","title":"Using conditionals","text":"Adding sheep as an element raises a problem, as the plural of sheep is sheep.
Using a conditional, a test can be added to determine if a name should be made plural
First lets abstract out the anonymous function to a shared function using defn
(defn pluralise\n \"Pluralise a given string value\"\n [animal]\n (str string \"s\"))\n
def
will bind a name to our collection of animals
(def animals [\"pig\" \"cow\" \"goat\" \"cat\" \"dog\" \"rabbit\"])\n\n(map pluralise animals)\n
The if
function included a conditional test. If that test is true the next expression is evaluated. If the test is false, the second expression is evaluated.
(defn pluralise\n \"Pluralise a given string value\"\n [animal]\n (if (= animal \"sheep\")\n animal\n (str animal \"s\")))\n\n(map pluralise animals)\n
There are several animals that do not have a plural form. Rather than make a complicated test, a collection of animals that are not plural can be defined.
(def non-plural-words [\"deer\" \"sheep\" \"shrimp\" ])\n\n(defn pluralise\n \"Pluralise a given string value\"\n [animal]\n (if (some #{animal} non-plural-words)\n animal\n (str animal \"s\")))\n\n(def animals [\"pig\" \"cow\" \"goat\" \"cat\" \"dog\" \"rabbit\" \"sheep\" \"shrimp\" \"deer\"])\n\n(map pluralise animals)\n
To keep the function pure, we should pass the non-plural-words as an argument
(defn pluralise\n \"Pluralise a given string value\"\n [animal non-plural-words]\n (if (some #{animal} non-plural-words)\n animal\n (str animal \"s\")))\n
Using the terse form of the anonymous function, #()
, call the pluralise function with two arguments. map
will replace the %
character with an element from the animals collection for each element in the collection.
(map #(pluralise % non-plural-words) animals)\n
The partial
function can be used instead of creating an anonymous function, removing the need for more custom code. The order of the arguments must be swapped for partial
to call pluralise
correctly
(defn pluralise\n \"Pluralise a given string value\"\n [non-plural-words animal]\n (if (some #{animal} non-plural-words)\n animal\n (str animal \"s\")))\n
Now we can call pluralise by wrapping it as a partial function.
The argument that is the non-plural-words is constant, its the individual elements of animals I want to get out via map. So when map runs it gets an element from the animals collection and adds it to the call to pluralise, along with non-plural-words
(map (partial pluralise non-plural-words) animals)\n
Using partial here is like calling (pluralise non-plural-words ,,,)
but each time including an element from animals where the ,,,
is.
"},{"location":"thinking-functionally/map-with-partial/#learning-at-the-repl","title":"Learning at the REPL","text":"At first I was getting incorrect output, [\"deer\" \"sheep\" \"shrimp\"]
, then I realised that it was returning the non-plural-words instead of pluralised animals. The arguments from the partial function were being sent in the wrong order. So I simply changed the order in the pluralise function and it worked.
I checked this by adding some old-fashioned print statement.
(defn pluralise-wrong-argument-order\n \"Pluralise a given string value\"\n [animal non-plural-words ]\n (if (some #{animal} non-plural-words)\n (do\n (println (str animal \" its true\"))\n animal)\n (do\n (println (str animal \" its false\"))\n (str animal \"s\"))))\n
"},{"location":"thinking-functionally/partial-functions/","title":"Currying & Partial Functions","text":"Clojure does not support automatic currying, (+3) would result in applying + to 3, resulting with number 3 instead of a function that adds 3 as in Haskell. Therefore, in Clojure we use partial that enables the equivalent behavior.
(defn sum\n \"Sum two numbers together\"\n [number1 number2]\n (+ number1 number2))\n\n(sum 1 2)\n;; => 3\n
If you try and evaluate sum
with a single value then you get an arity exception
(sum 1)\n;; => clojure.lang.ArityException\n;; => Wrong number of args (1) passed to: functional-concepts/sum\n
If we did need to call sum with fewer than the required arguments, for example if we are mapping sum over a vector, then we can use partial to help us call the sum function with the right number of arguments.
Lets add the value 2 to each element in our collection
(map (partial sum 2) [1 3 5 7 9])\n
"},{"location":"thinking-functionally/partial-functions/#using-functions-on-more-arguments-than-they-can-normally-take","title":"Using functions on more arguments than they can normally take","text":"The reduce
function can only work on a single collection as an argument (or a value and a collection), so an error occurs if you wish to reduce over multiple collections.
(reduce + [1 2 3 4])\n;; => 10\n\n(reduce + [1 2 3 4] [5 6 7 8])\n;; returns an error due to invalid arguments\n
However, by using partial we can take one collection at once and return the result of reduce on each of those collections.
(map (partial reduce +) [[1 2 3 4] [5 6 7 8]])\n
In the above example we map the partial reduce function over each element of the vector, each element being a collection.
"},{"location":"thinking-functionally/partial-functions/#using-partial-to-set-a-default-value","title":"Using partial to set a default value","text":"We can use the partial function to create a default message that can be just given just the custom part. For example, if we want to have a default welcome message but include a custom part to the message at the end.
First we would define a function that combines parts of the message together.
(defn join-strings\n \"join one or more strings\"\n [& args]\n (apply str args))\n
The [& args] argument string says take all the arguments passed and refer to them by the name args. Its the & character that has the semantic meaning, so any name after the & can be used, although args is common if there is no domain specific context involved.
We can simply call this function with all the words of the message.
(join-strings \"Hello\" \" \" \"Clojure\" \" \" \"world\")\n;; \u21d2 \"Hello Clojure world\"\n
Now we define a name called wrap-message
that can be used to wrap the start of our message. This name binds to a partial function call to join-strings
which send that function the default message and any custom message you add when evaluate wrap-message
(def wrap-message (partial join-strings \"Hello Clojurians in \"))\n\n(wrap-message)\n;; \u21d2 \"Hello Clojurians in \"\n\n(wrap-message \"London\")\n ;; => \"Hello Clojurians in London\"\n
"},{"location":"thinking-functionally/partial-functions/#currying-in-clojure","title":"Currying in clojure","text":"Currying is the process of taking some function that accepts multiple arguments, and turning it into a sequence of functions, each accepting a single argument. Or put another way, to transform a function with multiple arguments into a chain of single-argument functions.
Currying relies on having fixed argument sizes, whereas Clojure gets a lot of flexibility from variable argument lengths (variable arity).
Clojure therefore has the partial function gives results similar to currying, however the partial
function also works with variable functions.
partial
refers to supplying some number of arguments to a function, and getting back a new function that takes the rest of the arguments and returns the final result
One advantage of partial
is to avoid having to write your own anonymous functions
"},{"location":"thinking-functionally/partial-functions/#useful-references","title":"Useful references","text":" - Partial function applications for humans
"},{"location":"thinking-functionally/pattern-matching/","title":"Pattern matching","text":"Fixme work in progress
"},{"location":"thinking-functionally/pattern-matching/#regular-expression","title":"Regular Expression","text":""},{"location":"thinking-functionally/pattern-matching/#destructuring","title":"Destructuring","text":""},{"location":"thinking-functionally/persistent-data-structures/","title":"Persistent data structures","text":""},{"location":"thinking-functionally/polymorphism/","title":"Polymorphic function definitions","text":"Polymorphic means many forms.
The simplest example of polymorphism in Clojure is a function definition that acts differently based on the number of arguments passed.
Usually you define a function with one set of arguments, either none []
, one [one]
or many [any number of args]
, using the basic syntax
(defn name\n\"I am the doc string to describe the function\"\n [args]\n (str \"define your behaviour here\"))\n
Instead of writing multiple functions with the same name that each take different numbers of arguments, you can use the following polymorphic syntax in Clojure
(defn name\n \"I am the doc string to describe the function\"\n ([]\n (str \"behaviour with no args\"))\n ([one]\n (str \"behaviour with one arg\"))\n ([one two & args]\n (str \"behaviour with multiple args\")))\n
Note Write a simple function called i-am-polly
that returns a default message when given no arguments and a custom message when given a custom string as an argument
(defn i-am-polly\n ([] (i-am-polly \"My name is polly\"))\n ([message] (str message)))\n\n(i-am-polly)\n(i-am-polly \"I call different behaviour depending on arguments sent\")\n
"},{"location":"thinking-functionally/pure-functions/","title":"Pure functions","text":"A function is considered pure if does not side effects or is affected by side causes. A pure function does not change any other part of the system and is not affected by any other part of the system.
When you pass arguments to a function and that function returns a value without interacting with any other part of the system, then that function is considered pure.
Should something from outside a function be allowed to affect the result of evaluating a function, or if that function be allowed to affect the outside world, then its an impure function.
So lets look at a simple code example
(defn add-numbers [number1 number2]\n (+ number1 number2))\n\n(add-numbers 1 2)\n
Lets look at each line of this suggested answer
;; function takes 2 arguments\n;; function uses both arguments for result\n(defn add-numbers [number1 number2]\n (+ number1 number2))\n\n;; specific values are passed as arguments\n(add-numbers 1 2)\n
"},{"location":"thinking-functionally/pure-functions/#notewrite-a-pure-function-that-adds-two-numbers-together","title":"Note::Write a pure function that adds two numbers together ?","text":""},{"location":"thinking-functionally/pure-functions/#an-example-with-map","title":"An example with map","text":"Note Define a collection called numbers and write a named function that increments each number of the numbers collection. Is your function pure or impure ?
(def numbers '(5 4 3 2 1))\n\n(defn increment-numbers []\n (map inc numbers))\n\n(increment-numbers)\n
The function takes no arguments and is pulling in a value from outside the function. This is a trivial example, but if all your code is like this it would be more complex. If the value pointed to by numbers
is mutable and changes before the increment-numbers
function is called then you will get different results.
Here is a Pure function example
(def numbers '(5 4 3 2 1))\n\n(defn increment-numbers [number-collection]\n (map inc number-collection))\n\n(increment-numbers numbers)\n
In this example we are explicitly passing the numbers
collection to the function. The function works on passed value and returns a predictable result.
"},{"location":"thinking-functionally/recursion-polymorphism/","title":"Recursion & Polymorphism","text":"Fixme work in progress
The following sum
function will calculate the value of adding all the elements in a collection. You can alter the results by adding a starting value to the calculation as a second argument when calling sum
(defn sum\n ([vals] (sum vals 0))\n ([vals accumulating-total]\n (if (empty? vals)\n accumulating-total\n (sum (rest vals) (+ (first vals) accumulating-total)))))\n\n(sum [2 7 9 11 13])\n(sum [1])\n(sum [2 7 9 11 13] 9)\n
Rather than duplicate the calculation, the behaviour of calling sum
with just a collection simply calls sum
again, this time passing a starting value of zero.
"},{"location":"thinking-functionally/recursion/","title":"Recursion","text":"Fixme work in progress
Recursion is used greatly in Clojure to iterate through data and as anything can be treated as data in Clojure you can understand why.
The constructs available in Clojure for recursion include
loop
and recur
- Named function that calls itself
map
, reduce
, filter
, remove
, etc. for
"},{"location":"thinking-functionally/recursion/#recursively-calling-the-same-function","title":"Recursively calling the same function","text":"Lets iterate though a collection using recursion by writing a function that calls itself
(defn recursively-use-a-collection [collection]\n (println (first collection))\n (if (empty? collection)\n (print-str \"no more values to process\")\n (recursively-use-a-collection (rest collection))))\n\n(recursively-use-a-collection [1 2 3])\n
Lets take this recursive approach to create a function that can tell us the length of a collection (list or vector)
We define a function that takes a collection of an argument. The collection is tested to see if it is empty and if so a zero value is returned. If the collection is not empty, then we
(defn length [collection]\n (if (empty? collection)\n 0\n (+ 1 (length (rest collection)))))\n;; => #'clojure-through-code.01-basics/length\n
If we call the length
function with an empty collection, then the empty?
condition will return true and the if
expression will evaluate the first expression, 0, returning 0.
(length [])\n;; => 0\n
If we call the length
function with a collection containing 3 values, then the empty?
function will return false
and the if
function will evaluate the second expression.
The second expression starts with a simple counter, using the +
function and the value one
(length [0 1 2])\n;; => 3\n
(+ 1 (length [1 2]))\n(+ 1 (+ 1 (length [2])))\n(+ 1 (+ 1 (+ 1 (length []))))\n(+ 1 (+ 1 (+ 1 0)))\n\n(length (range 24))\n;; => 24\n
(defn length [collection] (kk))
"},{"location":"thinking-functionally/recursion/#further-recursion-examples","title":"Further recursion examples","text":"Other functions to consider
- every
- accumulating / accumulative
- keep
"},{"location":"thinking-functionally/sequence-abstractions/","title":"Sequence abstraction","text":"Fixme work in progress
(first '(1 2 3 4 5))\n(rest '(1 2 3 4 5))\n(last '(1 2 3 4 5))\n
(defn nth [items n]\n (if (= n 0)\n (first items)\n (recur (rest items) (- n 1))))\n\n(define squares '(0 1 4 9 16 25))\n\n(nth squares 3)\n
"},{"location":"thinking-functionally/sequences/","title":"Sequences","text":"Fixme work in progress
Data structures can be built by combining functions
(cons 1 (cons 2 (cons 3 (cons 4 nil))))\n
(->>\n nil\n (cons 4)\n (cons 3)\n (cons 2)\n (cons 1))\n
"},{"location":"thinking-functionally/side-effects/","title":"Side effects","text":"A side effect is something that creates a change outside of the current code scope, or something external that affects the behaviour or result of executing the code in the current scope.
"},{"location":"thinking-functionally/side-effects/#nondeterministic-the-complexity-iceberg","title":"Nondeterministic - the complexity iceberg","text":"When you have side effects, you cannot reason accurately about a piece of the code.
In order to understand a piece of code you must look at all possible side effects created in all lines of code to ensure you fully understand the result of executing your code.
With side effects in your system, complexity is hidden, causing a far greater risk of a dangerous situation.
"},{"location":"thinking-functionally/side-effects/#side-causes-side-effects","title":"Side causes - side effects","text":"You can think about these effects is in two specific areas, Side Causes and Side Effects
"},{"location":"thinking-functionally/side-effects/#hintside-causes-term","title":"Hint::Side Causes term","text":"The term of side causes was coined by Kris Jenkins in the superb article What is Functional Programming?
"},{"location":"thinking-functionally/tail-recursion/","title":"Tail recursion","text":"Fixme work in progress
If we generate a very large collection we run the risk of blowing our heap space. For example we could use range to generate a very large collection, say a vector containing 10 billion values
Don't try this example below
(vec (range 0 9999999999))\n;; this will crash after a short while as it will use up all your heap space\n
Using tail call optimisation (tail recursion) allows us to reuse a memory location when we call a function recursively. This tail recursion is not part of the underlying Java Virtual Machine (JVM), so instead Clojure has a specific function called recur
The recur
function allows the processing of a very large data set without blowing the heap space because the memory space will be reused.
The recur
function must be the last expression in order to work.
(defn sum\n ([vals] (sum vals 0))\n ([vals accumulating-total]\n (if (empty? vals)\n accumulating-total\n (recur (rest vals) (+ (first vals) accumulating-total)))))\n\n(sum (vec (range 0 9999999)))\n
"},{"location":"thinking-functionally/threading-macros/","title":"Threading macros","text":"The thread-first ->
and thread-last ->>
macros allow Clojure code to be written in a more sequential style and with a more terse syntax. This can sometimes make code easier to understand by humans.
Using the thread-first macro, ->
, the result of the first evaluation is passed as the first argument to the next function and so on.
```clojure (-> (clojure.string/lower-case \"HELLO\") (str \", Clojure world\"))
The value hello is converted to lower case and that result is passed as the first argument to the next function. The string function is then evaluated with this new argument and the final \"hello, Clojure world\" string is returned as the result.\n\n The thread-last macro `->>` passes the result of the first evaluation as the **last argument** to the next expression.\n\n```clojure\n(->> \" this\"\n (str \" is\")\n (str \" backwards\"))\n
"},{"location":"thinking-functionally/threading-macros/#hintparens-optional","title":"Hint::Parens optional","text":"function calls that only take one argument, the one passed by earlier expressions, can be included in the threading macro code without the surrounding ()
parens.
"},{"location":"thinking-functionally/threading-macros/#reading-clojure-code","title":"Reading Clojure code","text":"To read Clojure it is common to start from the inside out, as this is how the Clojure reader also works. This style is inherited from Lisp of which Clojure is an implementation.
The following code is written in classic Lisp style.
(reverse\n (sort-by val (frequencies\n (remove common-english-words\n (map #(clojure.string/lower-case %)\n (re-seq #\"[a-zA-Z0-9|']+\"\n (slurp book.txt)))))))\n
Reading inside out:
- slurp in the contents of the book.txt file, converting it to a string.
- use a regular expression to create a new sequence where the book is a sequence of individual strings for each word.
- convert each string in the sequence by mapping the lower-case function over each element of the sequence.
- remove common english words such as the and and from the sequence.
- count how many times each word occurs and pair the string with its frequency in the book.
- reverse the order of the sequence by the value of frequency, so the most used word is at the start of the sequence.
This function uses the var common-english-words
which is defined as:
(def (set\n (clojure.string/split (slurp \"common-english-words.txt\") #\",\" )))\n
This takes a comma separated file of words and splits them. The words are put into a set so only one instance of each word is included.
"},{"location":"thinking-functionally/threading-macros/#rewriting-the-code-with-threading-macros","title":"Rewriting the code with threading macros","text":"(->> (slurp book.txt)\n (re-seq #\"[a-zA-Z0-9|']+\" ,,,)\n (map #(clojure.string/lower-case %))\n (remove common-english-words)\n frequencies\n (sort-by val)\n reverse)\n
frequencies
and reverse
only take one argument, so they do not require surrounding ()
inside the threading macro.
The common-english-words var is fairly easy to read, so probably doesn't need to be written using a threading macro, however, for completeness here is a thread-first example.
(def common-english-words\n (-> (slurp \"common-english-words.txt\")\n (clojure.string/split #\",\")\n set))\n
"},{"location":"thinking-functionally/threading-macros/#hintmacroexpand","title":"Hint::Macroexpand","text":"use macroexpand-1
around the threading macro code to show resulting lisp style Clojure code. Clojure editors also provide evaluation functions that will macroexpand.
"},{"location":"thinking-functionally/transducers/","title":"Transducers","text":"Transducers provide an efficient way to transform values from a collection or stream of data, eg. core.async channel, java.jdbc database query (0.7.0 upward) or a network stream etc.
Transformations are applied directly to a stream of data without first creating a collection.
Multiple transducers are composed into a single transforming function, walking the data elements only once and without the need for intermediate collections.
One Transducer benefit is to allow the design to switch from a seq-based implementation to a core.async implementation using channels
Transducers: Clojure.org Transducer use cases
Basics Transducer walkthrough
Simple examples of Clojure Transducers
"},{"location":"thinking-functionally/transducers/#evolving-design","title":"Evolving design","text":"Each element of the data collection is acted upon by a composition of each transducer in an expression, making them more efficient than a applying one expression after another (e.g. as with a thread macro or composed list expressions).
Transducers provide benefits like lazy evaluation and alternative performance trade-offs.
Nested calls
(reduce + (filter odd? (map #(+ 2 %) (range 0 10))))\n
Functional composition
(def xform\n (comp\n (partial filter odd?)\n (partial map #(+ 2 %))))\n (reduce + (xform (range 0 10)))\n
Thread macro
(defn xform [xs]\n (->> xs\n (map #(+ 2 %))\n (filter odd?)))\n (reduce + (xform (range 0 10)))\n
Transducer
(def xform\n (comp\n (map #(+ 2 %))\n (filter odd?)))\n(transduce xform + (range 0 10))\n
The order of combinators in a transducer is the same as a thread macro (natural order).
"},{"location":"thinking-functionally/transducers/#coreasync","title":"core.async","text":"Transducers were introduced into the Clojure language to provide a common abstraction to avoid re-implmentation of clojure.core transformation functions specific to core.async.
in a large part to support processing data to and from a core.async channel.
The Clojure maintainers discovered they were re-implementing filter, map, partition, etc. on core.async and realized it would be better to have an abstraction for the transforms independent of the source and destination of data.
The xform can be used with an core.async channel
Transducer with core.async
(chan 1 xform)\n
an optional arg when creating a channel, causing a transform to be applied to all data that goes through the channel
"},{"location":"thinking-functionally/transducers/#database-queries","title":"Database queries","text":"Typical database access involves passing in functions to transform rows and process the transformed result set
java.jdbc
0.7.0-beta1 onwards can also apply transducers to \u201creducible queries\u201d (and reducible result sets).
Create a reducible query which is passed to transforming function, e.g. reducers, transducers, plain ol\u2019 reduce
and into
etc
"},{"location":"thinking-functionally/transducers/#simplified-description","title":"Simplified description","text":"Transducers are recipes what to do with a sequence of data without knowledge what the underlying sequence is (how to do it). It can be any seq, async channel or maybe observable.
They are composable and polymorphic.
The benefit is, you don't have to implement all standard combinators every time new data source is added. Again and again. As resulting effect you as user are able to reuse those recipes on different data sources.
https://stackoverflow.com/questions/26317325/can-someone-explain-clojure-transducers-to-me-in-simple-terms
Work In Progress, sorry A very messy brain dump of ideas to tidy up. Pull requests are welcome
Clojure.core functions such as map and filter are lazy, however they also generate containers for lazy values when chained together.
Without holding onto the head of the collection, large lazy sequences aren't created but intermediate abstractions are still created for each lazy element.
"},{"location":"thinking-functionally/transducers/#reducing-functions","title":"Reducing functions","text":"Reducing functions are those that take two arguments:
- a result so far
- an input.
The reducing function returns a new result (so far).
For example +: With two arguments, you can think of the first as the result so far and the second as the input.
A transducer could now take the + function and make it a twice-plus function (doubles every input before adding it). This is how that transducer would look like (in most basic terms):
Reducing function
(defn double\n [rfn]\n (fn [r i]\n (rfn r (* 2 i))))\n
For illustration substitute rfn with + to see how + is transformed into twice-plus:
Reducing function - twice plus
(def twice-plus ;; result of (double +)\n (fn [r i]\n (+ r (* 2 i))))\n\n(twice-plus 1 2) ;-> 5\n(= (twice-plus 1 2) ((double +) 1 2)) ;-> true\n
Calling the reducing function
(reduce (double +) 0 [1 2 3])\n\n;; => 12\n
Reducing functions returned by transducers are independent of how the result is accumulated because they accumulate with the reducing function passed to them.
conj
takes a collection and a value and returns a new collection with that value appended.
(reduce (double conj) [] [1 2 3])\n;; => [2 4 6]\n
Transducers are independent of the kind of data is passed.
optimisation goes beyond eliminating intermediate streams; it is possible to perform operations in parallel.
"},{"location":"thinking-functionally/transducers/#pmap","title":"pmap","text":"Use pmap
when mapping an expensive function over a sequence, making the operation parallel. No other changes to the code are required.
Transducers will still be faster if the pmap function creates intermedicate data structures.
A transducer clear definition is here:
Transducers are a powerful and composable way to build algorithmic transformations that you can reuse in many contexts.
"},{"location":"thinking-functionally/transducers/#reducing-functions_1","title":"Reducing functions","text":"Population a local Village
The
(def village\n [{:home :north :family \"smith\" :name \"sue\" :age 37 :sex :f :role :parent}\n {:home :north :family \"smith\" :name \"stan\" :age 35 :sex :m :role :parent}\n {:home :north :family \"smith\" :name \"simon\" :age 7 :sex :m :role :child}\n {:home :north :family \"smith\" :name \"sadie\" :age 5 :sex :f :role :child}\n\n {:home :south :family \"jones\" :name \"jill\" :age 45 :sex :f :role :parent}\n {:home :south :family \"jones\" :name \"jeff\" :age 45 :sex :m :role :parent}\n {:home :south :family \"jones\" :name \"jackie\" :age 19 :sex :f :role :child}\n {:home :south :family \"jones\" :name \"jason\" :age 16 :sex :f :role :child}\n {:home :south :family \"jones\" :name \"june\" :age 14 :sex :f :role :child}\n\n {:home :west :family \"brown\" :name \"billie\" :age 55 :sex :f :role :parent}\n {:home :west :family \"brown\" :name \"brian\" :age 23 :sex :m :role :child}\n {:home :west :family \"brown\" :name \"bettie\" :age 29 :sex :f :role :child}\n\n {:home :east :family \"williams\" :name \"walter\" :age 23 :sex :m :role :parent}\n {:home :east :family \"williams\" :name \"wanda\" :age 3 :sex :f :role :child}])\n
Define a reducing expression to return the number of children in the village.
(def children \n (r/map #(if (= :child (:role %)) 1 0)))\n
Call the expression
(r/reduce + 0 (children village))\n;;=> 8\n
Using a transducer to add up all the mapped values
create the transducers using the new arity for map that takes the function, no collection
(def child-numbers (map #(if (= :child (:role %)) 1 0)))\n
Use transduce (c.f r/reduce) with the transducer to get the answer
(transduce child-numbers + 0 village)\n;;=> 8\n
It is really powerful when taking subgroups in account, e.g to know how many children in the Brown Family
Reducer to count children in Brown family
Create the reducer to select members of the Brown family
(def brown-family-children\n (r/filter #(= \"brown\" (string/lower-case (:family %)))))\n
compose a composite function to select the Brown family and map children to 1
(def count-brown-family-children \n (comp ex1a-map-children-to-value-1 brown-family-children))\n
reduce to add up all the Brown children
(r/reduce + 0 (ex2a-count-brown-family-children village))\n;;=> 2\n
"},{"location":"thinking-functionally/transducers/#references","title":"References","text":"Transducers - Clojure next big idea
"},{"location":"using-data-structures/","title":"Using data structures","text":"Data structures in Clojure are used to model information and data, within a particular namespace. Functions are used to run behaviour over the data structures.
Lets look at some of the common functions that are used in Clojure with data structures
fixme the below content is work in progress, sorry.
"},{"location":"using-data-structures/#managing-return-values","title":"Managing Return values","text":"If you run a function over a data structure, you may not always get back the type of value you want. It easy to wrap a function around to give you the desired value type.
Note Use the str
function to get a string from person, rather than a set of characters
(first person)\n(rest person)\n\n(str (first person))\n\n;; How do we return the rest of the string as a string ?\n(str (rest person))\n(map str (rest person))\n(str (map str (rest person)))\n(apply str (rest person))\n
You can get the value of this map
(def luke {:name \"Luke Skywalker\" :skill \"Targeting Swamp Rats\"})\n(def darth {:name \"Darth Vader\" :skill \"Crank phone calls\"})\n(def jarjar {:name \"JarJar Binks\" :skill \"Upsetting a generation of fans\"})\n\n(get luke :skill)\n
"},{"location":"using-data-structures/#immutability","title":"Immutability","text":"When you use functions on data structures, although they can return a new value they do not change the original data structure.
Lets define a name for a data structure
(def name1 [1 2 3 4])\n
when we evaluate that name we get the original data we set
name1\n
Now we use a function called conj to adds (conjoin) another number to our data structure
(conj name1 5)\n
This returns a new value without changing the original data structure
name1\n
We cant change the original data structure, it is immutable. Once it is set it cant be changed. However, if we give a name to the result of changing the original data structure, we can refer to that new data structure
(def name2(conj name1 5))\n
Now name2 is the new data structure, but name1 remains unchanged
name2\nname1\n
So we cannot change the data structure, however we can achieve something that looks like we have changed it. We can re-assign the original name to the result of changing the original data structure
(def name2(conj name1 5))\n
Now name1 and name2 are the same result
name2\nname1\n
An analogy
You have the number 2. If you add 1 to 2, what value is the number 2?
The number 2 is still 2 no mater that you add 1 to it, however, you get the value 3 in return
"},{"location":"using-data-structures/#creating-new-data-structures","title":"Creating new data structures","text":"Use concat to add lists or vectors together
(concat [1 2] '(3 4)) ; => (1 2 3 4)\n
Use filter, map to interact with collections
(map inc [1 2 3]) ; => (2 3 4)\n(filter even? [1 2 3]) ; => (2)\n
Use reduce to reduce them
(reduce + [1 2 3 4])\n; = (+ (+ (+ 1 2) 3) 4)\n; => 10\n
Reduce can take an initial-value argument too
(reduce conj [] '(3 2 1))\n; = (conj (conj (conj [] 3) 2) 1)\n; => [3 2 1]\n
Use cons to add an item to the beginning of a list or vector
(cons 4 [1 2 3]) ; => (4 1 2 3)\n(cons 4 '(1 2 3)) ; => (4 1 2 3)\n
Use conj to add an item to the beginning of a list, or the end of a vector
(conj [1 2 3] 4) ; => [1 2 3 4]\n(conj '(1 2 3) 4) ; => (4 1 2 3)\n
"},{"location":"using-data-structures/applying-functions/","title":"Applying functions to data structures","text":"Applying a functions behaviour to the elements of a data structure
"},{"location":"using-data-structures/destructuring/","title":"Destructuring","text":"Destructuring is a form of pattern matching that is common in Clojure. Destructuring allow you to pull out the specific elements from a collection.
Destructuring is commonly used with the let
method for creating local bindings (locally scoped names).
(let [[a b c & d :as e] [1 2 3 4 5 6 7]]\n [a b c d e])\n\n(let [[[x1 y1][x2 y2]] [[1 2] [3 4]]]\n [x1 y1 x2 y2])\n\n;; with strings\n(let [[a b & c :as str] \"asdjhhfdas\"]\n [a b c str])\n\n;; with maps\n(let [{a :a, b :b, c :c, :as m :or {a 2 b 3}} {:a 5 :c 6}]\n [a b c m])\n
It is often the case that you will want to bind same-named symbols to the map keys. The :keys directive allows you to avoid the redundancy:
(let [{fred :fred ethel :ethel lucy :lucy} m] )\n
This can be written in a shorter form as follows:
(let [{:keys [fred ethel lucy]} m] )\n
As of Clojure 1.6, you can also use prefixed map keys in the map destructuring form:
(let [m {:x/a 1, :y/b 2}\n {:keys [x/a y/b]} m]\n (+ a b))\n
As shown above, in the case of using prefixed keys, the bound symbol name will be the same as the right-hand side of the prefixed key. You can also use auto-resolved keyword forms in the :keys directive:
(let [m {::x 42}\n {:keys [::x]} m]\n x)\n
"},{"location":"using-data-structures/lazy-sequences/","title":"Lazy Sequences","text":"Sequences are an interface for logical lists, which can be lazy. \"Lazy\" means that a sequence can define an infinite series, like so:
(range 4)\n
If you evaluate (range)
just by itself it will return an infinite number of integers, well at least until your computers memory space fills up.
So we dont blow up our memory and just get the values we want we can use range
in conjunction with other functions that define how many numbers we actually want.
For example, if we just wanted the first four numbers from the infinite sequence of range
we could specify that with the take
function
(take 4 (range)) ; (0 1 2 3)\n
Here the range function is being lazy, because it will only generate the first 4 numbers in its sequence.
Clojure (and Lisps in general) often evaluate at the last possible moment, usually when they have been given more specific content.
"},{"location":"using-data-structures/mapping-data-structures/","title":"Mapping functions over data structures","text":"Map allows you to work over one or more data sets, applying the function to each element of each of the data structures.
When the data structures are of equal size, then the same sized data structure is returned.
(map + [1 2 3] [1 2 3])\n;; => (2 4 6)\n
If one data structure is smaller, then the function is only applied up to the last element of the smallest data structure.
(map + [1 2 3] [1 2])\n;; => (2 4)\n\n(map + [1 2 3] [1])\n;; => (2)\n\n(map + [1 2 3] [])\n;; => ()\n\n(map + [1 2 3])\n;; => (1 2 3)\n
Lets look at another example. Here we have a pre-defined Fibonacci sequence up to the first 12 values.
(def fibonacci-sequence [1 2 3 5 8 13 21 34 55 89 144 278])\n
If we just want the first 10 values of the sequence, we can use the take
function.
(take 10 fibonacci-sequence)\n;; => (1 2 3 5 8 13 21 34 55 89)\n
If we want a calculation using the values of the fibonacci-sequence then we can use map
with a function. In this case we are going to generate a range of Integer numbers from 0-9 using the function range
. That range of numbers is then multiplied element by element with the corresponding element in the fibonacci-sequence.
(map * (range 10) fibonacci-sequence)\n;; => (0 2 6 15 32 65 126 238 440 801)\n
So,
- 0 times 1 is 0,
- 1, times 2 is 2,
- 2 times 3 is 6, etc.
If we evaluate the previous expression part by part, its easier to see what is going on. First lets evaluate the fibonacci-sequence
(map * (range 10) [1 2 3 5 8 13 21 34 55 89 144 278])\n;; => (0 2 6 15 32 65 126 238 440 801)\n
Now lets evaluate the (range 10)
function call
(map * (0 1 2 3 4 5 6 7 8 9) [1 2 3 5 8 13 21 34 55 89 144 278])\n;; => (0 2 6 15 32 65 126 238 440 801)\n
We can see the answer is the same, however by evaluating each part of the expression we get an exact view of what is going to happen with the map
function.
"},{"location":"using-data-structures/sequences/","title":"Sequence abstractions","text":"There are functions that work on all the built in data-structures in Clojure.
first
second
rest
cons
"},{"location":"using-data-structures/sequences/#practising-with-lists","title":"Practising with lists","text":"Create a simple collection of developer events. First use a list of strings, then try a map with keywords. For each data structure, pull out some of the event details
(def developer-events-strings '(\"Devoxx UK\" \"Devoxx France\" \"Devoxx\" \"Hack the Tower\"))\n\n(def developer-events-strings2 (list \"Devoxx UK\" \"Devoxx France\" \"Devoxx\" \"Hack the Tower\"))\n\ndeveloper-events-strings\n\n(first developer-events-strings)\n\n(def developer-events-vector\n [:devoxxuk :devoxxfr :devoxx :hackthetower] )\n
Using a Clojure Vector data structure is a more Clojure approach, especially when the vector contains keywords. Think of a Vector as an Array, although in Clojure it is again immutable in the same way a list is.
Create a slightly more involved data structure, holding more data around each developer events. Suggest using a map, with each key being the unique name of the developer event.
The details of each event (the value to go with the event name key) is itself a map as there are several pieces of data associated with each event name.
(def dev-event-details\n {:devoxxuk {:URL \"http://jaxlondon.co.uk\"\n :event-type \"Conference\"\n :number-of-attendees 700\n :call-for-papers true}\n :hackthetower {:URL \"http://hackthetower.co.uk\"\n :event-type \"hackday\"\n :number-of-attendees 60\n :call-for-papers false}})\n
Lets call the data structure and see what it evaluates too, it should not be a surprise
dev-event-details\n
We can ask for the value of a specific key, and just that value is returned
(dev-event-details :devoxxuk)\n
In our example, the value returned from the :devoxxuk key is also a map, so we can ask for a specific part of that map value by again using its key
(:URL (dev-event-details :devoxxuk))\n
Define a simple data structure for stocks data using a vector of maps, as there will be one or more company stocks to track.
Each map represents the stock information for a company.
Get the value of the whole data structure by referring to it by name, ask for a specific element by its position in the array using the nth
function.
Then try some of the common functions that work on collections.
(def portfolio [ { :ticker \"CRM\" :lastTrade 233.12 :open 230.66}\n { :ticker \"AAPL\" :lastTrade 203.25 :open 204.50}\n { :ticker \"MSFT\" :lastTrade 29.12 :open 29.08 }\n { :ticker \"ORCL\" :lastTrade 21.90 :open 21.83 }])\n\nportfolio\n\n(nth portfolio 0)\n\n(nth portfolio 3)\n\n(first portfolio)\n(rest portfolio)\n(last portfolio)\n
First and next are termed as sequence functions in Clojure, unlike other lisps, you can use first and next on other data structures too
"}]}
\ No newline at end of file
diff --git a/sitemap.xml.gz b/sitemap.xml.gz
index 3a939f03e..b077cb494 100644
Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ