Random Clojure Functionλ︎
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
Live Coding Video walk-throughλ︎
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.
Create a projectλ︎
Use the :project/create
Practicalli Clojure CLI Config to create a new Clojure project.
This project has a deps.edn
file that includes the aliases
:test
- includes thetest/
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
REPL experimentsλ︎
Open the project in a Clojure-aware editor or start a Rebel terminal UI 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.
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
require
will make a namespace available from within the REPL
Change into the random-function
namespace to define functions
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.
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
The hash-map keys are function symbols and the values are the function vars
{+' #'clojure.core/+',
decimal? #'clojure.core/decimal?,
sort-by #'clojure.core/sort-by,
macroexpand #'clojure.core/macroexpand
,,,}
The meta
function will return a hash-map of details about a function, when given a function var.
The hash-map has several useful pieces of information for the application, including :name
, :doc
, and :arglists
;; => {:added "1.0",
;; :ns #namespace[clojure.core],
;; :name map,
;; :file "clojure/core.clj",
;; :static true,
;; :column 1,
;; :line 2727,
;; :arglists ([f] [f coll] [f c1 c2] [f c1 c2 c3] [f c1 c2 c3 & colls]),
;; :doc
;; "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."}
To use the meta
function, the values from the ns-publics
results should be used.
rand-nth
will return a random function var from the sequence of function vars
A single function var is returned, so then the specific meta data can be returned.
Define a name for all functionsλ︎
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
"Fully qualified function names from clojure.core"
(vals (ns-publics 'clojure.core)))
Write Unit Testsλ︎
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).
(ns practicalli.random-function-test
(:require [clojure.test :refer [deftest is testing]]
[practicalli.random-function :as random-fn]))
(deftest standard-library-test
(testing "Show random function from Clojure standard library"
(is (seq random-fn/standard-library-functions))
(is (string? (random-fn/-main)))))
Update the main functionλ︎
Edit the src/practicalli/random-function.clj
file. Change the -main
function to return a string of the function name and description.
(defn -main
"Return a function name from the Clojure Standard library"
[& args]
(let [function-details (meta (rand-nth standard-library-functions))]
(str (function-details :name) "\n " (function-details :doc)))
)
Run the tests with the Congnitect test runner via the test
function in the build.clj
file.
```shell
clojure -T:build test
```
Run the tests with the Kaocha test runner using the alias :test/run
from Practicalli Clojure CLI config
```shell
clojure -M:test/run
```
Running the applicationλ︎
Use the clojure command with the main namespace of the application. Clojure will look for the -main function and evaluate it.
This should return a random function name and its description. However, nothing is returned. Time to refactor the code.
Improving the codeλ︎
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
(testing "Show random function from Clojure standard library"
(is (seq SUT/standard-library-functions))
(is (string? (SUT/random-function SUT/standard-library-functions)))))
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
[function-list]
(let [function-details (meta (rand-nth function-list))]
(str (function-details :name) "\n " (function-details :doc) "\n ")))
Update the main function to call this new function.
(defn -main
"Return a function name from the Clojure Standard library"
[& args]
(println (random-function standard-library-functions)))
Run the tests again.
If the tests pass, then run the application again
A random function and its description are displayed.
Adding the function signatureλ︎
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
[function-list]
(let [function-details (meta (rand-nth function-list))]
(str (function-details :name)
"\n " (function-details :doc)
"\n " (function-details :arglists))))
Add more namespacesλ︎
All current namespaces on the classpath can be retrieved using the all-ns
function. This returns a lazy-seq, (type (all-ns))
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.
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.
Bind the results of this expression to the name all-public-functions
.
Control which namespaces are consultedλ︎
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.
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.
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
"Return a function name from the Clojure Standard library"
[& args]
(if (seq args)
(println (random-function (mapcat #(function-list (symbol %)) args)))
(println (random-function standard-library-functions))))
Use the fully qualified name for the namespaceλ︎
Now that functions can come from a range of namespaces, the fully qualified namespace should be used for the function, eg. domain/namespace
Update the random function to return the domain part of the namespace, separated by a /
(defn random-function
[function-list]
(let [function-details (meta (rand-nth function-list))]
(str (function-details :ns) "/" (function-details :name)
"\n " (function-details :arglists)
"\n " (function-details :doc))))
Use all available namespaces by defaultλ︎
Define a name to represent the collection of all available namespaces, in the context of the running REPL.
(def all-public-functions
"Fully qualified function names from available"
(mapcat #(vals (ns-publics %)) (all-ns)))
Update the -main
function to use all available namespaces if no arguments are passed to the main function.
(defn -main
"Return a random function and its details
from the available namespaces"
[& args]
(if (seq args)
(println (random-function (mapcat #(function-list (symbol %)) args)))
(println (random-function all-public-functions))))
Follow-on idea: Convert to a web serviceλ︎
Add http-kit server and send information back as a plain text, html, json and edn
Follow-on idea: Convert to a libraryλ︎
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