Clojure Basics - Syntax and Native Data Types
Let's get our hands dirty with native data types and common operations.
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
This lesson preview is part of the Tinycanva: Clojure for React Developers course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to Tinycanva: Clojure for React Developers, plus 70+ \newline books, guides and courses with the \newline Pro subscription.
Closure is considered to be an expressive language and one of the reasons behind this is its syntax and native data types. If you don't come from a list background, you might find it hard to read closure code at first. But we are in luck today because closure has just one syntactical rule. Elements inside parentheses are elements of a list. The first element of the list is the function or the operation that you want to execute and all other elements are the arguments to this operation. This operation can be a function, this operation can be a macro or this operation can be something else that we will see later. All you need to remember is that the first element of the list is the operation and all other elements are arguments to that operation. Let's start with the basic data types. I highly recommend that you start the repel and follow along. We have numbers, we have floats and we have ratios for non-terminating decimals . Strings are defined using double quotes and blue lanes can be true or false. The string function can be used to cast a variable to string and also to conc atenate two strings together. The type function can be used to check the type of any variable. You would observe that the type of one is Java long number and the type of a ratio is closure long ratio. This tells us that numbers in closure are just Java numbers whereas ratios are closure specific. Closure has no support for in-fix operations. If you try to use an operator between two operands like 1+2 and try to execute it in the repel, you will get an error. All algebraic operations are defined as functions. The plus function can be called with two or more arguments. Similarly, the product function can also be called using two or more arguments. Closure course ships with all the algebraic functions that you would expect like question, remainder and all others. Inc or increment can be used to add one to any number. Similarly, dick or decrement can be used to remove one from any number. Algebraic comparisons are also available as functions and all comparisons can take two or more arguments. In fact, the comparison operator and most algebraic operators can work with just one argument. Closure code is organized into namespaces. You can think of a namespace as an address for every file. It is similar to JavaScript module or PHP namespaces. The thing to note here is that when you start a repel, you are in the user namespace by default. We can change this namespace and we will get to this later. The death macro can be used to define variables in a namespace. Here we defined the age variable in the user namespace. The literal age is now a symbol. A symbol is a string that points to another value. Like age points to the value 20. A concept similar to symbols is keywords. Where symbols point to another value, keywords point to themselves. Keywords can be defined using a colon prefix. And you can define namespace qualified keywords using double colons. Keywords and closure are very similar to keywords in Ruby. And they are particularly suited to define keys for maps, enumerations, or variables that would never change like roles in a web based application. Collections and closure is an abstract type that implements the i sequence interface. All compound data structures like lists, vectors, maps, and sets are collections. Collections have a common set of functions that apply across data structures and some data structure specific functions. And all the operations that manipulate the collection in any way are immutable. Closure has no support for mutating a data structure after it is defined. List enclosure is an implementation of the linked list algorithm. Link lists are optimized for traversal. A list can be defined using the list operation with any number of arguments or using the literal form with a single code followed by parenthesis. We need the single code because if we try to define a list without it, closure will consider the list to be a function called where the first element is the operation and all other elements are the arguments to that operation. In our case, the first element is not a valid operation. If you try to execute this code, you will get an error saying that one does not implement the iFn interface. Which in plain words means that one is not a function and hence cannot be called. There are many functions that you can use to operate on a list. We will try a few of them in this screencast, but I highly recommend that you read the lesson and test all the functions listed. The list predicate, i.e. a list with the question mark, can be used to figure out if a variable is a list. Conventionally, functions ending with the question mark are considered to be predicates. The count function can be used to count the number of elements in a list. Now count is a sequence function, which means that it will work for lists for vectors, for maps and even for sets. The conge function can be used to append an element at the top of the list. Let's append 9 to the list 1, 3. The conge function is immutable, which means that the original list was never modified, but a new copy was created with the extra element 9. In terms of implementation, a view was created on the original list and the elements were not copied to a new physical location. But from the point of view of a developer, this operation was immutable. Vectors are conceptually similar to lists, but in terms of implementation, they are an array list and not a linked list. An array list is a list where each element has an index. This makes vectors efficient for lookups. All these sequence functions like count and conge will work with vectors because vectors also implement the ISC quince interface. To define a new vector, we can use the vector function or we can use the literal form and just use square brackets. And as you have guessed, there is also a predicate function to figure out if a variable is vector. Vectors implement the I-F-N interface as well, which means that vectors are callable as functions. They accept an argument, which is the index that you want to look up and return the value of the element at that index. If the index is out of bounds, an exception is raised. They should give you more insight into how closure is built up as a language. For something to be a sequence, it needs to implement the ISC qu interface. For something to be a function, it needs to implement the I-F-N interface. Maps are a collection of key value pairs and can be defined using the literal form of curly brackets. The keys in the map are generally a keyword. Maps also implement the I-S-E-Q interface, which means that all sequence functions like count and conge will work for maps. ASOC, or short for associate, is a map-specific function that adds one or more key value pairs to a map. Similarly, D-SOC removes one or more key value pairs from the map. The function keys and vowels can be used to get all the keys or all the values of a map as a list, and the function get can be used to look up certain keys in the map. By default, the maps are not sorted, but it is possible to create sort orders, depending on the keys or values or a combination. Sets are unordered collection of elements that can be defined using the literal form hash followed by curly brackets, or also using the operation set. There are many set specific functions, like Union Intersection Difference, which are defined in the closure set namespace. So, in order to access them, you need to require the closure set namespace. All other functions that we have used so far are a part of the closure core namespace, which is required by default. We can use the required macro to require the set specific functions. Don't worry if this syntax doesn't make sense. We'll study this in more detail later in this course. After you have required these functions, you can execute them just like other functions. Custom functions can be defined using the Defend Macro. The Defend Macro accepts a function name, a vector of arguments, and the function body. There is no need to specify return values explicitly, because the last element of the list is returned by default. Comments can be defined using a semicolon. The convention is to use two semicolons for a fullline command and one semicol on for an inline comment. We can also use the comment macro to define multiline comments that contain code. This is generally called a rich comment, and we'll study about this later in this course. Commas are read as white spaces. You don't really need to add them, but you can if you wish. This brings us to the concept of homo-econicity. In C-family languages, like JavaScript, the syntax used for defining data structures is remarkably different from the syntax used for defining functions. In closure, the same literal forms are used to define data as well as code. On the left you see a list of three numbers, a vector of two elements, and a function definition, which is a combination of list and vectors. And on the right hand side, you can see JavaScript code, where the syntax for defining data is different from the syntax for defining functions. In conclusion, closure affords us all the data types that we'd expect from a language. Some data types like numbers and strings are just host data types, whereas some others like ratios are closure-specific data types. We also learned that collections are abstract and are built around the i- sequence protocol. We then studied the homo-iconic nature of closure to conclude that code is data and data is code. Before you proceed to the next chapter, I recommend that you read the lesson text and practice all the functions mentioned in there. This chapter just scratches the surface of the extent of closure's API. Closure.org is a good community resource to learn more about the API and functions available in the closure core.