1. Introduction
‘Declarative’ is a word we use often and it makes a good sense in everyday conversation in software development. For instance, “…we like to write our programs in such a way that the code looks like a description of the solution;” [11]
. Declarative programming is often associated with functional and logic programming. Adding database query and configuration management languages to the list of declarative programming languages would sufficiently cover the meaning. However, if we try to define it precisely, the concept appears to be elusive.
The legendary SICP [1] mentions “…the distinction between declarative knowledge and imperative knowledge. In mathematics we are usually concerned with declarative (what is) descriptions, whereas in computer science we are usually concerned with imperative (how to) descriptions.” This way ‘declarativeness’ becomes the key difference between mathematics and programming. How is it possible then talk about programming becoming increasingly more declarative? Is programming becoming more mathematical?
Logic programming refines the what and how distinction. By Kowalski’s equation algorithm=logic+control, declarative programming should deal with the logic part only and leave the control flow part out. More precisely, a program is a theory, and computation is a deduction from that theory [22].
Other definitive sources on programming (e.g. [19, 14]) simply do not mention the world declarative, suggesting that it is not an essential concept, or it is merely a synonym for another important notion.
The vague definition of working on a higher level of abstraction hints that declarative is a relative notion, not an absolute one. Adding two numbers can be thought as a typical example of imperative style, but from the perspective of machine code it is very declarative since we do not specify how to add those numbers together. Thus, the level of declarativeness indicates where we are in the hierarchy of abstractions, and the history of programming can be viewed as moving upwards on this abstraction ladder.
It seems that language development has recently reached a sweet spot on the hierarchy of abstractions. A beginner programmer does not have to understand the inner workings of computers, and also does not need to master advanced abstractions. There are of course excellent books for these topics, including computer architecture [26], lambda calculus [16], category theory [21, 5, 29]
. However, these require a full course on their own. Luckily, nowadays a beginner can start programming on a natural level. What is natural for beginners? It is functional programming, since it is on the familiar level of mathematics that is taught in compulsory math classes. This is at odds with a clear separation of mathematics and computing. Indeed, here we argue that unifying computational and mathematical thinking is possible. Moreover, it may be the crucial next step in education.
Why is it important to discuss philosophical concepts for computer programming? There is a growing sense of the hidden philosophical assumptions that block or enable the learning of programming, or successfully adapt new technologies. Therefore, investigations in the history and philosophy of computing may be useful [30]. A particularly painful example is the continued usage of shared mutable sate in parallel concurrent programming.
1.1. Structure of the paper
In this paper we define declarativeness by making its implicit assumption explicit: there is always some automation involved. Section 2 defines this manual/automated separation pattern, which will be used throughout the paper. Section 3 briefly introduces the Clojure language [15, 7, 13, 10, 11]. The core language is small enough that readers with no prior knowledge of Clojure can follow the content easily. Section 4 will analyze examples of declarative programming accessible for beginners. Section 5 will go through more advanced examples.
2. Declarative = the work is done by something else
What is the essential idea of declarative programming? Ideas are often understood in terms of cognitive metaphors, even in software engineering and computer science [32]. The leading metaphor here will be automation. Declarativeness can be captured by separating the work that needs to be done and the work which is done by a general mechanism, the automated part. We will describe examples of declarative programming (and thinking in general) with tables separating the manual and the automated part in tasks.
Cognitive task  

manual  work we have to do 
automated  work done by a general mechanism 
2.1. Core idea of computation
If declarativeness is the position on a scale of abstractions, then it is the amount of detail we need to deal with. In the context of computation, these are subtasks, i.e. computational work. Therefore, in declarative programming the work is done by something else. Putting it this way, a lot becomes declarative. The whole idea of using computers can be understood as such. What we now call programming was described as automatic programming in the early days of software engineering [8].
People often say that a declarative piece of code looks like ‘magic’. The work done at a lower hierarchical level of computing stack makes it so. Repeating this relation several times is exactly the magic of digital computation. As we go down deeper in the computing stack, the work is always done on the level below, bottoming out in simple physical mechanisms that are only capable of processing bit sequences.
Digital Computation  

manual  write programs 
automated  a runtime executing our programs 
2.2. Mathematics as declarative knowledge
One thing is to know what is. It is the number that gives when multiplied by itself. It is a different thing to know how to obtain its actual value. Here’s a method: start with a guess, let’s say , divide by this number , and take their average . By repeating this process we can calculate arbitrary many digits of . The very act of denoting a number by the symbol is declarative, as it assumes that the number is available for us (which would of course require infinite amount of calculation). It is an infinitely efficient work saving device. The history is fitting since Turing’s original paper was concerned with computing real numbers [31, 27].
Working with real numbers  

manual  symbolic notation 
automated  algorithms for finding/approximating numerical values 
Also, an equation can be (and should be when learning math properly) conceptualized as description of its solution set. This becomes obvious in analytic geometry, where we the solution sets are visualized in space. We can talk about the equation of a circle, or a line as its declarative description.
In differential calculus, the derivation rules compress the work of calculating limits of functions into symbol manipulation formulas. Explicitly pointing this out to students helps them to appreciate more the mathematical results. The derivation rules are worksaving devices, rather than just a set of rules to be memorized.
Derivation of real functions  

manual  symbolic manipulation of algebraic formulas using derivation rules 
automated  limit calculation of real functions values 
2.3. Unification of mathematical and computational thinking
In programming it is the reliable code, the welltested library where we can delegate the work. In mathematics the previously proved theorems play the same role. From this general perspective computer programming and mathematical thinking are very similar. Their common core idea can be summarized easily.
We use a formal system to lighten the cognitive load or completely offload our thinking processes whenever possible.
Trying to save work is a natural engineering idea, which applies to the physical world as well. Here we talk about special cases where this is done by and/or through a formal system, a system of symbols and rules for manipulating them. In the context of formal systems, saving work can be advantageous in several ways.

efficiency, to save cognitive effort

promote understanding, increase readability

minimize moving parts to prevent errors
Efficiency is not just about execution speed. It can be decisive between possible and impossible. Without special training we cannot multiply large numbers in our head, but using an external representation (e.g. pen and paper) it is a routine exercise.
Our understanding is limited by our cognitive constraints. For instance the number of items we can think of at the same time. The less pieces we have, the easier to deal with.
Minimizing the number of parts also has an effect on the reliability of the software product. The less code we write the less chances we have to make a mistake.
3. The Clojure language
Clojure is a dynamic, generalpurpose functional programming language with a special focus on immutable data structures [15, 7, 13, 10, 11]. Its design makes a strong statement about concurrency and parallelism. The language is a suggested solution for the difficult problems in concurrency in Java[25], where the relatively low level features of the language make concurrency a formidable problem. By using persistent and immutable data structures shared memory concurrent processes are easier to handle.
Clojure belongs to the Lisp family of languages [24]. As such, the core syntax is very simple. Function calls are written as lists. For instance, the list (f x y) is evaluated by treating the first element of the list as a function and the remaining items as arguments of the function, corresponding to the mathematical notation . Functions can have different arities. Arithmetic operators are also in the form of a function call. cverb (+ 1 2 3) 6 (+ 1 7) 8 (+ 2) 2 (+) 0
cverb
The last constant function’s value comes from abstract algebra: 0 is the additive identity, the neutral element).
Some special forms (e.g. conditionals, symbol bindings) have different evaluation patterns, but they all have the same list form. Function definitions are also special forms. cverb (fn [x] (* x x))
cverb
This is a function literal for squaring. It is an anonymous (lambda) function. We can bind it to the name square at the same time as defining the function. cverb (defn square [x] (* x x))
cverb
As a Lisp language, the code is represented as lists. Thus the source code coincides with the abstract syntax tree, a property known as homoiconicity. Clojure style is closer to the Programs = Data Structures model, than to the imperative style of Algorithm + Data structures = Programs. For the same reasons, the metaprogramming facilities are powerful in Clojure, making it possible to use programming techniques from several different paradigms.
A distinctive feature of Clojure is the addition of extra fundamental data structures. Beyond the lists, it has vectors (indexable sequential collection, e.g. [1 2 3 4 5], also used for defining function arguments); hashmaps (anything to anything hashtables, taking over the roles of objects, e.g. {:name Arthur :age 42}); and hashsets (hashmaps mapping each key to itself, e.g. #{1 2}); all immutable with performance guarantees (persistence). These all behave like mappings, so indeed, they are also functions.
The simplicity of the core language makes it quite readable. Therefore, it is possible to illustrate the techniques with relevant source code examples. This eliminates the need for pseudocode level descriptions.
4. Declarative style in introductory programming
If being relatively more declarative is about going up and down the abstraction scale, then what is the most natural level of declarativeness? Of course, the amount of abstraction one can comfortably deal with depends on education and personal development. But a baseline is easy to establish. The compulsory part of our math education is centered around algebraic calculations and real valued functions. While many feel that there is no immediate applicability of this knowledge, it is a great entry point for programming. Functional programming is on the human scale, so it is the easiest to start with for beginner programmers.
In this section we have examples taken from a course designed Liberal Arts students, assuming no background knowledge in programming. More details about the curriculum can be found on the course’s website https://egrinagy.github.io/popbook/.
4.1. Functional collection processing
The core of an introductory functional programming course can be defined around the higher order functions map, filter and reduce. They automate collection processing: map applies the same function to all elements of a collection, filter selects elements from a collection satisfying a predicate, and reduce produces a single result (which might be another collection).
Functional collection processing (map, filter, reduce)  

manual  code for dealing with an element of a collection 
automated  the mechanism of processing a collection 
The Collatz conjecture is interesting open problem in number theory, which is easy to state but seems to be an immensely difficult statement to prove [20]. The conjecture states that the following simple function when iterated starting from a positive integer always end up in a cycle containing 1.
Due to the lack of regularity in the behaviour of the iterated function, it is a nice problem to explore with computational means.
What number between 1 and 1000 produces the longest sequence? The numerical function is just a translation of the mathematical definition. cverb (defn collatz [n] (if (even? n) (/ n 2) (inc (* n 3))))
cverb
Finding the length of the Collatz iteration from a particular number , i.e. the number of iterations needed to reach 1. With lazy evaluation and higher order functions this is simple task. cverb (defn clength [n] (count (takewhile (fn [x] (not= 1 x)) (iterate collatz n))))
cverb
We count the number of elements in the list built from the consecutive values of the iterated Collatz function. Once we can calculate Collatz length of a number, we can map this on the sequence of numbers, and find the maximum value. cverb (apply max (map clength (range 1 1001))) 178
cverb
The apply higher order function call is needed since max is a multiarity function that is called with a single collection argument. (apply f coll) is essentially the function f called with arguments coming from the sequential collection coll.
Knowing the maximum length, we can find the number that produces that. cverb (filter (fn [x] (= 178 (clength x))) (range 1 1001)) (871)
cverb
There happens to be only one such value. The solution is somewhat unsatisfactory as we need to go through the numbers twice. With reduce we can rectify this. cverb (reduce (fn [v n] (let [l (clength n)] (if (¿ l (first v)) [l n] v))) [0 0] (range 1 1001)) [178 871]
cverb
This code does more, but it is also a longer one. We can transform this solution by introducing maxkey, which automates the above steps: finds a number that produces the highest value for clength. cverb (apply maxkey clength (range 1 1001)) 871
cverb
This shows that a higher order function can move the code closer to the declarative ideal. It is also important for the students that they see the transformation of the code after a correct solution is obtained. This emphasizes the gain from automation.
4.2. Pointfree style, tacit programming
Combinatory logic can be used in place of lambda calculus [16] (for a popular science description of combinatory logic see [28]). Defining a function explicitly by describing how it acts on its arguments can be replaced by several different techniques.

composing functions by comp

preloading arguments by partial

grouping functions to work on same input by juxt

negating logical output value by complement
Here are two different functions for calculating the mean of a sequential collection of numbers (assumed to be nonempty).
cverb (defn mean [nums] (let [sum (apply + nums) n (count nums)] (/ sum n)))
cverb
This function computes the sum of the numbers, their count, and divides these two. cverb (def mean (comp (partial apply /) (juxt (partial apply +) count)))
cverb
The pointfree version defines the function by composing the partial application of / with a function that juxtaposes the sum and the count of its argument collection.
Pointfree style  

manual  specifying functions to compose 
automated  managing input parameters and return values 
It is debatable how much tacitness is good for readability, but there are examples where it led to great success (e.g. the UNIX pipelines [17]).
4.3. Destructuring
Destructuring is a convenient way to extract pieces of data from a composite data structure. For example, we can represent a line defined by two points by a vector of vectors and we would like to compute the slope of the line like (slope [[1 2] [2 4]]). So we define the function slope. cverb (defn slope [line] (/ ( (first (first line)) (first (second line))) ( (second (first line)) (second (second line)))))
cverb
Here the coordinate information is extracted on demand, making the actual calculation obscure, littered with the retrieval. We can separate these two. cverb (defn slope2 [line] (let [p1 (first line) p2 (second line) x1 (first p1) y1 (second p1) x2 (first p2) y2 (second p2)] (/ ( x1 x2) ( y1 y2))))
cverb
Now the computation is quite clear, it is basically the mathematical formula. However, we have a long list of bindings. Destructuring gets rid of this, by giving the ‘shape’ of the input data in the argument list. cverb (defn slope3 [[[x1 y1] [x2 y2]]] (/ ( x1 x2) ( y1 y2)))
cverb
This may look like magic first, but it is actually just a simple automation. It is easy to reveal how it is done. cverb (destructure ’[ [x y] [13 19]]) [vec__1246 [13 19] x (clojure.core/nth vec__1246 0 nil) y (clojure.core/nth vec__1246 1 nil)]
cverb
It does exactly the work we did not want to do manually. destructure produces a vector of bindings, that is given to let in a real destructuring situation. It is also very useful in figuring out what goes wrong in an unsuccessful and complex destructuring attempt.
Destructuring  

manual  describing the shape of a data structure 
automated  extracting data, local bindings 
5. Declarative style in advanced programming
Some declarative programming concepts cannot be fit into an introductory class. This might be due to time constraints, or due to the fact that the less declarative form is more general. For instance conditionals can be used for any decision making, but the more elegant pattern matching may have some limitations and not applicable in all cases.
5.1. Pattern matching
Destructuring is just a special case of pattern matching. We can also use it for making decisions based on the structure of the data. In Clojure there is a core library for pattern matching: core.match is based on techniques from OCaml [23]. cverb (require ’[clojure.core.match :refer [match]])
cverb
The archetypal example for pattern matching is the Fizzbuzz game. cverb (defn fizzbuzz [lim] (for [n (range 1 lim)] (match [(mod n 3) (mod n 5)] [0 0] ”FizzBuzz” [0 _] ”Fizz” [_0] ”Buzz” :else n)))
cverb
Matching replaces conditional statements. When nested, conditionals seem to be difficult to read and thus errorprone.
Pattern matching  

manual  specifying choices based on the structure of data 
automated  control flow by conditional statements 
Since the order of the patterns does matter, the programmer still has to think in terms of the underlying matching process.
5.2. Logic programming
Logic programming is considered to be a higher level of declarativeness on two accounts. It is a generalization of functional programming to relational programming, so in a sense functions can be run backwards. It is also a more general type of pattern matching. The underlying unification operation can be viewed as twoway pattern matching.
Most programming problems (without need for user interaction) can be rephrased as search problems. Moreover, they can be easily described by using first order logic. These two combined lead to the idea of logic programming. Its promise is that we only need to specify properties of the required solution.
Logic programming  

manual  specifying properties of a solution by clauses 
automated  finding models for the properties by a search algorithm 
On the other hand, the most important logic programming language, Prolog, kept tools for manipulating the search algorithm. For instance, cutting the search tree or changing the underlying database (see standard references of the language like [6, 3]). A more declarative version of logic programming is relational programming, which can be viewed as ‘pure’ logic programming free from lowlevel access, or as a generalization of functional programming where the directionality of computation from function arguments to its result is removed. This style fits Lisplike languages well [12, 4], but can be implemented in any language (see minikanren.org for a list of implementations).
5.2.1. Relation programming in Clojure: core.logic
The Lisp nature (especially its macro system) of Clojure makes it possible to accommodate different programming paradigms as an additional library. For instance, the Thinking programs chapter of [11] describes several solutions for a sudoku solver, starting from a brute force functional solution to one using unification. Here we describe a smaller such transition. The task is a simple combinatorial problem, producing all permutations of (assumed to be unique) elements of a collection. Here is a classic functional recursive implementation. cverb (defn permutations [coll] (if (empty? coll) [coll] (mapcat (fn [x] (map (partial cons x) (permutations (remove (partial = x) coll)))) coll)))
cverb
If a collection is empty, then we can immediately return all permutations of its elements, which is the empty collection itself. If not empty, then for each element we recursively create the collection of all permutations of the remaining elements and include the chosen element in front. These collections have to be flattened (by mapcat) for each depth on the recursion. While the recursive style with the higher order functions can be seen as elegant, it is very much a howto description. When reading the code snippet, one has to imagine the recursive process of building the permutations backwards.
The same problem can be solved with a logic programming approach, offering a more declarative solution. In Clojure we can switch paradigm just be including a library. cverb (require ’[clojure.core.logic :as l] ’[clojure.core.logic.fd :as fd])
cverb
This piece of code loads the logic engine and a library for dealing with finite domains. cverb (defn permutations [n] (let [p (vec (repeatedly n logic/lvar)) points (fd/interval 1 n)] (logic/run* [q] (logic/== q p) (logic/everyg (fn [x] fd/in x points) p) (fd/distinct p))))
cverb
The logic code is wrapped in a normal function definition. p is a vector of logic variables, and points is just a finite domain defining their possible values. run* calls the logic engine requesting all solutions in q, which is the query variable. q is associated with p by the unification operator ==. The last two lines are the description of a solution: everyg takes a goal (a predicate function) and checks whether it holds for all elements of a collection, codedistinct checks for any repeated element. Thus the code reads as ‘find all the tuples with integers from the interval such that the elements of the tuples are all distinct’.
As the above example illustrates, core.logic is not directly accessible for an absolute beginner. It already requires the understanding of higher order functions and some basic knowledge of Clojure.
5.3. SATsolvers
Another very interesting case of logic programming is the progress of efficient general purpose solvers for the satisfiability problem [18, 2]. While satisfiability is ‘the’ hard problem, the problem instances coming up in practical applications often have enough structure for finding solutions quickly. Thus, if a computational problem can be represented as a conjunctive normal form, then we can rely on the performant search techniques.
Search problems  

manual  describe search problem in CNF 
automated  SATsolver finds solution configurations 
Relying on the general solution algorithm (and all of its sealed off optimizations) has another benefit. For computational projects, where a bespoke search algorithm is needed (e.g. [9]), SATsolvers still can serve as validation methods.
6. Conclusion
Here we attempted to clarify the vague notion of declarative programming by interpreting each of its occurrences as a separation of manual and automated computation. We showed that abstraction could almost cover the meaning of declarative, but it does not necessarily involve the work saving part. Therefore, the usage of the term declarative programming is justified. While it cannot readily provide a quantitative measure, it would be beneficial if each use of the term came with a clear indication of what is automated and what needs to be done by the programmer.
This general definition also invites rethinking the similarity and differences of mathematical and computational thinking. For education though, it is decisive to emphasize the similarities. We use a formal systems for offloading our cognitive processes, so both mathematics and programming languages automate our thinking.
References
 [1] H. Abelson, G.J. Sussman, and J. Sussman. Structure and Interpretation of Computer Programs. Electrical engineering and computer science series. MIT Press, 1996. https://mitpress.mit.edu/sicp/, https://sicpebook.wordpress.com/, https://github.com/sarabander/sicppdf.

[2]
A. Biere.
Handbook of Satisfiability.
Frontiers in artificial intelligence and applications. IOS Press, 2009.
 [3] M.A. Bramer. Logic Programming with Prolog. Springer, 2005.
 [4] W.E. Byrd. Relational programming in miniKanren: Techniques, applications, and implementations. PhD thesis, Indiana University, 2009.
 [5] E. Cheng. Cakes, Custard and Category Theory: Easy Recipes for Understanding Complex Maths. Profile Books, 2015.
 [6] W.F. Clocksin and C.S. Mellish. Programming in Prolog: Using the ISO Standard. Springer, 2003.
 [7] Cognitect Inc., https://clojure.org. Clojure v1.8.0, 2016.
 [8] Edgar G. Daylight, Niklaus Wirth, Tony Hoare, Barbara Liskov, Peter Naur, and Kurt De Grave. The Dawn of Software Engineering: From Turing to Dijkstra. Lonely Scholar, Belgium, 2012.
 [9] James East, Attila EgriNagy, and James D. Mitchell. Enumerating transformation semigroups. Semigroup Forum, Apr 2017.
 [10] C. Emerick, B. Carper, and C. Grand. Clojure Programming. O’Reilly, 2012.
 [11] M. Fogus and C. Houser. The Joy of Clojure 2nd Edition. Manning Publications, 2014.
 [12] D.P. Friedman, W.E. Byrd, and O. Kiselyov. The Reasoned Schemer. The Reasoned Schemer. MIT Press, 2005.
 [13] S.D. Halloway and A. Bedra. Programming Clojure. The pragmatic programmers. Pragmatic Bookshelf, 2012.
 [14] R. Harper. Practical Foundations for Programming Languages. Cambridge University Press, 2016.
 [15] R. Hickey. The Clojure programming language. In Proceedings of the 2008 Symposium on Dynamic Languages, DLS ’08, page 1, New York, NY, USA, 2008. ACM.
 [16] J. Roger Hindley and Jonathan P. Seldin. LambdaCalculus and Combinators: An Introduction. Cambridge University Press, New York, NY, USA, 2 edition, 2008.
 [17] B. W. Kernighan and R. Pike. The UNIX Programming Environment. Prentice Hall, 1984.
 [18] D. E. Knuth. The Art of Computer Programming, Volume 4, Fascicle 6: Satisfiability. AddisonWesley, 2015.
 [19] D.E. Knuth. The Art of Computer Programming Vol. 14. AddisonWesley, 2011.
 [20] J.C. Lagarias. The Ultimate Challenge: The Problem. American Mathematical Society, 2010.
 [21] F.W. Lawvere and S.H. Schanuel. Conceptual Mathematics: A First Introduction to Categories. Cambridge University Press, 2009.
 [22] John W. Lloyd. Practical advtanages of declarative programming. In María Alpuente, Roberto Barbuti, and Isidro Ramos, editors, 1994 Joint Conference on Declarative Programming, GULPPRODE’94 Peñiscola, Spain, September 1922, 1994, Volume 1, pages 18–30, 1994.

[23]
L. Maranget.
Compiling pattern matching to good decision trees.
In Proceedings of the 2008 ACM SIGPLAN Workshop on ML, pages 35–46, New York, NY, USA, 2008. ACM.  [24] John McCarthy. Recursive functions of symbolic expressions and their computation by machine, part I. Communications of ACM, 3(4):184–195, 1960.
 [25] T. Peierls, B. Goetz, J. Bloch, J. Bowbeer, D. Lea, and D. Holmes. Java Concurrency in Practice. Pearson Education, 2006.
 [26] C. Petzold. Code: The Hidden Language of Computer Hardware and Software. Developer Best Practices. Pearson Education, 2000.

[27]
Charles Petzold.
The Annotated Turing: A Guided Tour Through Alan Turing’s Historic Paper on Computability and the Turing Machine
. Wiley Publishing, 2008.  [28] R.M. Smullyan. To Mock a Mockingbird: And Other Logic Puzzles Including an Amazing Adventure in Combinatory Logic. Oxford University Press, 1985.
 [29] D.I. Spivak. Category Theory for the Sciences. MIT Press, 2014.
 [30] M. Tedre. The Science of Computing: Shaping a Discipline. CRC Press, 2014.
 [31] Alan M. Turing. On computable numbers, with an application to the Entscheidungsproblem. Proceedings of the London Mathematical Society. Second Series, 42:230–265, 1936.
 [32] Alvaro Videla. Metaphors we compute by. ACM Queue, 15(3):40:52–40:62, 2017.