Search

Dark theme | Light theme

September 8, 2021

PlantUML Pleasantness: Change Look-and-feel With Themes

When we convert our PlantUML diagrams we get a default look-and-feel for our diagrams that is mostly red and yellow based. We can alter the style and color of individual elements using the skinparam command. But we can also apply so-called themes to our diagram that will alter the look-and-feel of our diagram with a simple theme reference.

We can define the name of the theme in our diagram markup using the !theme directive. Or we can use the command-line option -theme when we generate a diagram from our markup. We can even combine both methods where the end result is also a combination of both themes.

Let's start with simple PlantUML markup to test some themes:

@startuml

' Elements
actor "Application User" as User
[Mail server] as Mail <<Mail>>

package "Sample Application" {
    [Controller] <<Spring REST controllers>>
    [Service] <<Spring service>>
}

' Connections
User --> Controller
Controller --> Service
Service --> Mail

@enduml

Without themes we get the following diagram with the default look-and-feel:

First we use the !theme directive and apply the cerulean theme. Later we will how we can check which themes are part of your PlantUML version.

@startuml

' Set theme
!theme cerulean

' Elements
actor "Application User" as User
[Mail server] as Mail <<Mail>>

package "Sample Application" {
    [Controller] <<Spring REST controllers>>
    [Service] <<Spring service>>
}

' Connections
User --> Controller
Controller --> Service
Service --> Mail

@enduml

The resulting diagram now has a different look-and-feel:

Instead of adding the !theme directive we can also the command-line tool for PlantUML and specify a theme with the option -theme. For our example we could run $ plantuml diagram.puml -theme crt-green and we get the following result:

If the PlantUML markup contains the !theme directive and we use the command-line -theme option the end result will be a combination. For example if we set the cerulean with the !theme directive and specify sketchy-outline with the -theme option we get:

If we want to know which themes are included with our PlantUML version we can use the command help themes in our PlantUML markup and the resulting diagram shows all themes available.

@startuml
help themes
@enduml

For an overview of themes we can check out the theme gallery.
More information is also available in the PlantUML documentation.

Written with PlantUML 1.2021.10.

August 30, 2021

Turn Backspace Key Into Delete On MacOS

Simply hold the fn key while pressing the backspace key to turn the backspace into a Del key. Depending on the position of the cursor in a piece of text we can simply choose to either use backspace or Del.

March 24, 2021

Java Joy: Run Action When Optional Value Present Or Not

If we have an Optional instance we can consume the value if it is present using the ifPresent method. Since Java 9 the method ifPresentOrElse has been added to the Optional class. The first argument is of type Consumer and is invoked when there is an optional value. The second argument is of type Runnable and is executed when the the optional is empty. The method in the Consumer and Runnable implementations does not return a type but returns void. Therefore we should use ifPresentOrElse when we need a conditional side effect for an Optional instance.

In the following example we have a method handleName that will update a list if an optional value is present or increases a counter when the optional value is empty:

package mrhaki.optional;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;

public class IfPresentOrElse {

    // Store names.
    private static List<String> names = new ArrayList<>();
    
    // Counter for counting empty or null names.
    private static AtomicInteger emptyCounter = new AtomicInteger();

    public static void main(String[] args) {
        // Optional name property will have a value.
        handleName(new User("mrhaki"));

        assert "MRHAKI".equals(names.get(0));
        assert 0 == emptyCounter.get();
        
        // Reset properties
        names.clear();
        emptyCounter.set(0);
        
        // Optional name property is empty.
        handleName(new User());

        assert names.isEmpty();
        assert 1 == emptyCounter.get();
    }

    private static void handleName(User user) {
        user.getName().ifPresentOrElse(
                // If present add name in uppercase to names.
                s -> names.add(s.toUpperCase(Locale.ROOT)),
                // If not present increment the counter.
                emptyCounter::incrementAndGet);
    }

    // Simple class with an optional property name.
    private static class User {
        private final String name;

        private User() {
            this(null);
        }

        private User(final String name) {
            this.name = name;
        }

        Optional<String> getName() {
            return Optional.ofNullable(name);
        }
    }
}

Written with Java 16.

March 22, 2021

Clojure Goodness: Turning Map Values To Map Keys

In the clojure.set namespace we can find the function map-invert. This function returns a new map where the values are keys with the appropriates keys of the original map assigned as value. If the original map has duplicate values than the latest key for the duplicate value will be the value of the new key.

In the following example code we see the result of using map-invert:

(ns mrhaki.set.map-invert
  (:require [clojure.set :refer [map-invert]]
            [clojure.test :refer [is]]))

(is (= {"mrhaki" :alias "Clojure" :language}
       (map-invert {:alias "mrhaki" :language "Clojure"})))

;; With duplicate values only one will be key.
(is (= {1 :c 2 :b 3 :d}
       (map-invert {:a 1 :b 2 :c 1 :d 3})))

Written with Clojure 1.10.1.

March 19, 2021

Java Joy: Getting Multiple Results From One Stream With Teeing Collector

If we want to get two types of information from a Stream of objects we can consume the Stream twice and collect the results. But that is not very efficient, especially when the stream has a lot of objects. Since Java 12 we can use the teeing method of the java.util.stream.Collectors class to get multiple results while consuming the stream of objects only once. The teeing method takes two collectors as argument each returning a separate result for the stream items. As third argument we must pass a function that will merge the results of the two collectors into a new object.

In the following code we have two example use cases that use the teeing method to get multiple results while consuming a Stream of objects only one time:

package mrhaki.stream;

import java.util.List;
import java.util.Locale;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.stream.Collector;
import java.util.stream.Collectors;

public class Teeing {
    public static void main(String[] args) {
        // Simple list of language names.
        List<String> languageNames = List.of("Java", "Clojure", "Kotlin", "Groovy", "Scala");

        // Predicate for String value that has the letter j or J.
        Predicate<String> containsJ = s -> s.toLowerCase(Locale.ROOT).contains("j");

        // Collector to apply two filters for one stream process and combine the result.
        Collector<String, ?, List<List<String>>> splitOnLetterJ =
                Collectors.teeing(
                        // Filter on language name with a j or J.
                        Collectors.filtering(containsJ, Collectors.toList()),
                        // Filter on language name withtout a j or J.
                        Collectors.filtering(containsJ.negate(), Collectors.toList()),
                        // Combine both results into a  new List with two items.
                        (withJ, withoutJ) -> List.of(withJ, withoutJ));

        List<List<String>> split = languageNames.stream().collect(splitOnLetterJ);

        assert List.of("Java", "Clojure").equals(split.get(0));
        assert List.of("Kotlin", "Groovy", "Scala").equals(split.get(1));


        // List of language records with a name and 
        // boolean to indicate the language is dynamic or not. 
        List<Language> languages =
                List.of(new Language("Clojure", true),
                        new Language("Java", false),
                        new Language("Groovy", true),
                        new Language("Scala", false),
                        new Language("Kotlin", false));

        // Filter for dynamic languages and transform to list.
        Collector<Language, ?, List<Language>> filteringDynamicLanguages =
                Collectors.filtering(Language::dynamic, Collectors.toUnmodifiableList());
        
        // Make a list with the language names in upper case.
        Collector<Language, ?, List<String>> upperCaseLanguageNames =
                Collectors.mapping(language -> language.name.toUpperCase(Locale.ROOT),
                                   Collectors.toUnmodifiableList());

        // Function to merge both list into a list with first item the result
        // of filteringDynamicLanguages and the second item the resul
        // of upperCaseLanguageNames.
        final BiFunction<List<Language>, List<String>, List<List<?>>> mergeLists =
                (dynamicLanguages, upperCaseLanguages) -> List.of(dynamicLanguages, upperCaseLanguages);

        List<List<?>> result = languages
                .stream()
                .collect(
                        Collectors.teeing(
                                filteringDynamicLanguages,
                                upperCaseLanguageNames,
                                mergeLists));

        assert List.of(new Language("Clojure", true), new Language("Groovy", true)).equals(result.get(0));
        assert List.of("CLOJURE", "JAVA", "GROOVY", "SCALA", "KOTLIN").equals(result.get(1));
    }

    // Record to store language name and if the language is dynamic.
    record Language(String name, boolean dynamic) {}
}

Written with Java 16.

March 18, 2021

Clojure Goodness: Create New Instance Of Java Class

Working with Java classes from Clojure code is easy. If we want to invoke methods or access fields on instances of Java classes we must first create an instance by invoking the constructor. In Clojure we can do that using the special form new or using a dot (.) notation. The new special form has the class name of the Java class we want to create an instance of as argument followed by arguments for the Java class constructor. With the dot notation we place a . after the class name followed by arguments for the class constructor to create a new instance.

In the following example we see several ways to create an instance of a Java class.

(ns mrhaki.java.new-instance
  (:require [clojure.test :refer [is]])
  (:import (java.net URI)
           (java.util Map TreeMap)))

;; Using a dot after the class name to invoke the constructor.
(is (instance? String (String.)))

;; Or using the new special form to invoke the constructor.
(is (instance? String (new String)))

;; Constructor arguments can be used.
(is (instance? URI (URI. "https://www.mrhaki.com")))
(is (instance? URI (new URI "https" "www.mrhaki.com" "/" "")))

;; We can use Clojure data structures in constructors.
(is (instance? Map (TreeMap. {:language "Clojure"})))

Written with Clojure 1.10.1.

March 16, 2021

Gradle Goodness: Create Properties File With WriteProperties Task

If we need to create a Java properties file in our build we could create a custom task and use the Properties class to store a file with properties. Instead of writing our custom task we can use the task WriteProperties that is already part of Gradle. Using this task Gradle will not add a timestamp in the comment to the properties file. Also the properties are sorted by name, so we know the properties are always in the same order in the output file. Finally, a fixed line separator is used, which is \n by default, but can be set via the task property lineSeparator.

To define the properties that need to be in the output file we can use the property method for a single property, or we can use properties method with a Map argument to set multiple properties.

In the following build file we define a new task projectProps of task type WriteProperties and we use task output as dependency for the processResources task. This way our new task will be executed, if there are changes, when the processResources task is executed:

// File: build.gradle.kts
plugins {
    java
}

version = "1.0.0"

tasks {
    val projectProps by registering(WriteProperties::class) {
        description = "Write project properties in a file."

        // Set output file to build/project.properties
        outputFile = file("${buildDir}/project.properties")
        // Default encoding is ISO-8559-1, here we change it.
        encoding = "UTF-8"
        // Optionally we can specify the header comment.
        comment = "Version and name of project"

        // Define property.
        property("project.version", project.version)

        // Define properties using a Map.
        properties(mapOf("project.name" to project.name))
    }

    processResources {
        // Depend on output of the task to create properties,
        // so the properties file will be part of the Java resources.
        from(projectProps)
    }
}

When we invoke the classes task we can see that our new task is executed:

$ gradle --console plain classes
> Task :compileJava UP-TO-DATE
> Task :projectProps
> Task :processResources
> Task :classes

BUILD SUCCESSFUL in 1s
3 actionable tasks: 2 executed, 1 up-to-date

$ cat build/project.properties
#Version and name of project
project.name=write-properties-sample
project.version=1.0.2

Without any changes we see our task was up-to-date and doesn’t have to run:

$ gradle --console plain classes
> Task :compileJava UP-TO-DATE
> Task :projectProps UP-TO-DATE
> Task :processResources UP-TO-DATE
> Task :classes UP-TO-DATE

BUILD SUCCESSFUL in 596ms
3 actionable tasks: 3 up-to-date

Written with Gradle 6.8.3

March 11, 2021

Java Joy: Formatting A String Value With Formatted Method

Java 15 introduced the multi-line string value referred to as a text block. With this introduction also the formatted method was added to the String class. The method can be invoked on a String value directly and function exactly as the static String.format method. The nice thing is that now we directly can use a method on the value instead of having to use a static method where the value is passed as argument.

In the following example we use the formatted method for a normal String value and a text block:

package mrhaki.string;

public class Formatted {
    public static void main(String[] args) {
        String text = "Hi, %s, thank you for reading %d blogs".formatted("mrhaki", 2);
        assert "Hi, mrhaki, thank you for reading 2 blogs".equals(text);

        String email = """
        Hello %s,

        thank you for reading %d blogs.
        """.formatted("Hubert", 2);

        assert """
                Hello Hubert,

                thank you for reading 2 blogs.
                """.equals(email);
    }
}

Written with Java 15.

March 10, 2021

Gradle Goodness: Add Support For "Scratch" Files To Java Project

When working on a Java project, we might want to have a place where we can just play around with the code we write. We need a "scratch" file where we can access the Java classes we write in our main sourceset. The scratch file is actually a Java source file with a main method where we can create instances of the Java code we write and invoke methods on them. This gives back a fast feedback loop, and we can use it to play around with our Java classes without the need to write a test for it. It gives great flexiblity during development. We must make sure the scratch file will not be packed in the JAR file with our production code.

To support this in our Gradle build file we can add a new sourceset that can access all classes we write in the main sourceset. Also we want to have new configurations for this sourceset so we can add dependencies that are only used by our scratch file. And finally we want a new task to run our scratch file. By default our scratch file will not be part of the JAR file with the classes from the main sourceset.

In the following example build script we first define the common configuration for a Java project with a dependency on the Log4j2 library. Notice we use the toolchain feature of Gradle to use Java 15 to compile and run our Java code. Using the toolchain definition Gradle will look for a Java 15 JDK on our computer and if it cannot find one can even download it automatically.

Next we define a new sourceset dev so we can create a Scratch.java file in the directory src/dev/java and we define the compile and runtime classpath to be dependent on the main source set output. As a bonus we also can use the src/dev/resources directory for resource files we want to have in the classpath when we run our Scratch.java file.

If we want to define dependencies that are only used by our Scratch class file we must add extra configurations: devImplementation and devRuntimeOnly. These configurations extend from the implementation and runtimeOnly configurations added by the java-library plugin. So all dependencies needed by classes in the main sourceset will also be available in the configurations for the dev sourceset.

Finally, we add a new task runDev that executes the main method in the Scratch.java file in the src/dev/java directory.

// File: build.gradle.kts
plugins {
    `java-library`
}

repositories {
    mavenCentral()
}

dependencies {
    implementation(platform("org.apache.logging.log4j:log4j-bom:2.14.0"))
    implementation("org.apache.logging.log4j:log4j-api")
    implementation("org.apache.logging.log4j:log4j-core")
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(15))
    }
}

//------------------------------------------------------------------------------
// Configure "dev" sourceset for running Scratch class
//------------------------------------------------------------------------------

// Create new dev sourceset with a compile and runtime classpath dependency
// on the main sourceset. This allows us to use the classes we create in
// the main sourceset in our dev sourceset.
// The directories src/dev/java and src/dev/resources are recognized
// this sourceset.
val dev: SourceSet by sourceSets.creating {
    compileClasspath += sourceSets.main.get().output
    runtimeClasspath += sourceSets.main.get().output
}

// Create implementation and runtimeOnly configurations for the dev sourceset.
// These configurations can be used to define dependencies that only
// apply for the source files in the dev sourceset.
val devImplementation: Configuration by configurations.getting {
    extendsFrom(configurations.implementation.get())
}
val devRuntimeOnly: Configuration by configurations.getting {
    extendsFrom(configurations.runtimeOnly.get())
}

// Create a new task "runDev" that will run the compiled Scratch.java file
// in the root of src/dev/java. The classpath will contains all dependencies
// from the devImplementation and devRuntimeOnly configurations.
val runDev by tasks.registering(JavaExec::class) {
    description = "Run Scratch file."
    group = "dev"
    classpath = dev.runtimeClasspath
    mainClass.set("Scratch")
}

dependencies {
    // Here we add an extra dependency only for the dev sourceset.
    devImplementation("org.apache.commons:commons-lang3:3.12.0")
}

Now we have our build file with scratch file support so it is time to have some sample code.

First we create a simple Java file in our main sourceset together with a Log4j2 configuration properties file:

// File: src/main/java/mrhaki/Sample.java
package mrhaki;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Sample {
    private static Logger log = LogManager.getFormatterLogger(Sample.class);

    public String sayHello(String name) {
        log.info("sayHello(name=%s)", name);
        return "Hello %s".formatted(name);
    }
}
# File: src/main/resource/log4j2.properties
appender.console.type=Console
appender.console.name=STDOUT
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %m%n

rootLogger.level=ERROR
rootLogger.appenderRef.stdout.ref=STDOUT

To play around with our Sample class we add a scratch file and also an extra Log4j2 configuration properties file to change the configuration when we run our scratch file:

// File: src/dev/java/Scratch.java
import mrhaki.Sample;
import org.apache.commons.lang3.SystemUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Scratch {
    private static Logger log = LogManager.getFormatterLogger(Scratch.class);

    public static void main(String[] args) {
        log.info("Running dev with Java %s.", SystemUtils.JAVA_VERSION);
        Sample sample = new Sample();
        sample.sayHello("mrhaki");
    }
}
# File: src/dev/resources/log4j2.properties
rootLogger.level=DEBUG

To execute our scratch file we invoke the runDev task from the command-line:

$ gw runDev

> Task :runDev
Running dev with Java 15.0.2.
sayHello(name=mrhaki)

BUILD SUCCESSFUL in 1s
5 actionable tasks: 5 executed

Written with Gradle 6.8.3.

March 8, 2021

Clojure Goodness: Pure Function Sample Buying Coffee From FP Programming In Scala Written In Clojure

Some of our colleagues at JDriven work with Scala and we talked about the book Functional Programming in Scala written by Paul Chiusano and Runar Bjarnason. We looked at one of the examples in the first chapter in the book to show the importance of having pure functions without side effects. The example is about buying a cup of coffee and charging a credit card with the purchase. In three examples a function with side effects is refactored to a pure function without side effects. When looking at the example I was wondering how this would look like in Clojure using only functions and simple immutable data structures. We can look at the examples in Scala to see how it is explained and implemented in the book. There are also Kotlin samples available. In this post we see a possible implementation in Clojure to buy coffee and charge our credit card.

The first example is a buy-coffee function with a credit card type as parameter. When we invoke the function with a credit card argument the credit card gets charged as side effect and a coffee type is created and returned. The coffee type is simply a map with a price key.

(ns mrhaki.coffee.cafe1)

(defn charge
  "Charge credit card with given amount.
   This function has side effects with a println in our example,
   but in a real world implementation could also include
   REST API calls, database access or other side effects."
  [cc amount]
  (println (str "Charge " amount " to credit card " (:number cc))))

(defn buy-coffee
  "Sample function to buy a coffee and charge the given credit card
   with the price of the coffee.
   The function returns coffee."
  [cc]
  (let [cup {:price 2.99}]
    (charge cc (:price cup))
    cup))

Let’s write a simple test for the buy-coffee function:

(ns mrhaki.coffee.cafe1-test
  (:require [clojure.test :refer :all]
            [mrhaki.coffee.cafe1 :refer :all]))

(deftest buy-coffee-test
  (is (= {:price 2.99} (buy-coffee {:number "123-456"})))
  (is (= {:price 2.99} (buy-coffee {:number "456-789"}))))

The test runs and shows the function works as expected, but we cannot test the side effect. If the side effect would involve calls to other systems, those systems would be called when we run our tests. That is something we would try to avoid, because it makes testing in isolation more difficult.

In the Scala example the next step is to pass in a Payment type that will take care of charging the credit card. This would make testing the function easier, because we can pass a testable implementation of the Payment type (e.g. a mock). So at least in our tests we can have an implementation that doesn’t access external systems.

For our Clojure example we can pass a payment function as argument to the buy-coffee function. This function takes a credit card type and amount as arguments and will be invoked in the buy-coffee function.

(ns mrhaki.coffee.cafe2)

(defn buy-coffee
  "Sample function to buy coffee and charge the given credit card
   with the price of the coffee.
   The function to charge the credit card is given as second argument.
   The fn-payment function must support a credit card as first
   argument and an amount as second argument."
  [cc fn-payment]
  (let [cup {:price 2.99}]
    (fn-payment cc (:price cup))
    cup))

When we write a test we can now check how the payment function is invoked:

(ns mrhaki.coffee.cafe2-test
  (:require [clojure.test :refer :all]
            [mrhaki.coffee.cafe2 :refer :all]))

(deftest buy-coffee-test
  (let [payment (fn [cc amount]
                  (is (= "123-456" (:number cc)))
                  (is (= 2.99 amount)))]
    (is (= {:price 2.99}
           (buy-coffee {:number "123-456"} payment)))))

But our buy-coffee function is not pure. We have still the side effect, but we made the function better testable. It is also mentioned in Functional Programming in Scala that if we want to invoke our buy-coffee function multiple times, the credit card also gets charged multiple times and this would include extra processing fees as well.

The final implementation of the buy-coffee will no longer charge the credit card, but it will a charge type with all information needed for charging the credit card, together with returning the coffee type. In another function we can handle the actual charging of the credit card by invoking REST calls, database calls or what else is needed. But now it is no longer a concern of our buy-coffee function. Also we can now invoke the buy-coffee function multiple times and combine all returned charge types into a single charge, to save on processing fees.

In our Clojure example we change the buy-coffee function and return both a coffee type and a charge type. The charge type is a map with a key for the credit card and a key for the amount to be charged. Also we introduce the buy-coffees function to show how we can invoke buy-coffee multiple times and combine the charges into a single charge.

(ns mrhaki.coffee.cafe3)

(defn buy-coffee
  "Sample function to buy coffee.
   The function returns a vector with coffee as first element and
   a charge type as second element."
  [cc]
  (let [cup {:price 2.99}]
    [cup {:credit-card cc :amount (:price cup)}]))

(defn- combine
  "Return a new charge with the sum of the amount values when
   the credit card number keys are equal.
   Throws exception when credit card numbers are not equal."
  [charge1 charge2]
  (if (= (:number charge1) (:number charge2))
    (update-in charge1 [:amount] + (:amount charge2))
    (throw (Exception. "Can't combine charges to different cards."))))

(defn- unzip
  "Returns pair of collections where first collection is built from first
   values of each pair in the coll argument and the second collection
   is built from the second value of each pair."
  [coll]
  (reduce (fn [[coll-a coll-b] [a b]]
            [(conj coll-a a) (conj coll-b b)])
          [[] []]
          coll))

(defn buy-coffees
  "Buy multiple times a cofee and combine all charges for given credit card.
  The first parameter accepts a credit card type and
  the second parameter is the number of times a coffee is bought."
  [cc n]
  (let [[coffees charges] (unzip (repeatedly n #(buy-coffee cc)))]
    [coffees (reduce combine charges)]))

And testing the functions is now much easier:

(ns mrhaki.coffee.cafe3-test
  (:require [clojure.test :refer :all]
            [mrhaki.coffee.cafe3 :refer :all]))

(deftest buy-coffee-test
  (let [[coffee charge] (buy-coffee {:number "123-456"})]
    (is (= {:price 2.99} coffee))
    (is (= {:credit-card {:number "123-456"} :amount 2.99} charge))))

(deftest buy-coffees-test
  (let [[coffees charge] (buy-coffees {:number "123-456"} 2)]
    (is (= (repeat 2 {:price 2.99}) coffees))
    (is (= {:credit-card {:number "123-456"} :amount 5.98} charge))))

The charge type also makes it easier to work with the charges. For example we can add a coalesce function that takes a collection of charge types and returns all charges per credit card. We can use this information to minimize the processing fees with the credit card company.

(ns mrhaki.coffee.cafe3)

...

(defn coalesce
  "Coalesce same card charges."
  [charges]
  (->> charges
       (group-by :credit-card)
       (vals)
       (map #(reduce combine %))))

And we can write the following test:

(ns mrhaki.coffee.cafe3-test
  (:require [clojure.test :refer :all]
            [mrhaki.coffee.cafe3 :refer :all]))
...

(deftest coalesce-test
  (let [charge-1a {:credit-card {:number "123-456"} :amount 2.99}
        charge-2a {:credit-card {:number "456-789"} :amount 2.99}
        charge-1b {:credit-card {:number "123-456"} :amount 2.99}
        charges (coalesce [charge-1a charge-2a charge-1b])]
    (is (= {:credit-card {:number "123-456"} :amount 5.98} (first charges)))
    (is (= {:credit-card {:number "456-789"} :amount 2.99} (second charges)))))

In the examples we use a map as type for the coffee, credit card and charge types. We can add records for these types to our Clojure examples to have some more semantics in our code. The good thing is that a records still can be used queries as a map, so a keyword function like :price still works for a Coffee record.

In the next example we add records and use them in the buy-coffee function. Notice the other functions still work without changes.

(ns mrhaki.coffee.cafe4)

;; Define records.
(defrecord Coffee [price])
(defrecord CreditCard [number])
(defrecord Charge [credit-card amount])

(defn buy-coffee
  "Sample function to buy coffee.
   The function returns a vector with coffee as first element and
   a charge type as second element."
  [cc]
  (let [cup (->Coffee 2.99)
        charge (->Charge cc (:price cup))]
    [cup charge]))

(defn- combine
  "Return a new charge with the sum of the amount values when
   the credit card number keys are equal.
   Throws exception when credit card numbers are not equal."
  [charge1 charge2]
  (if (= (:number charge1) (:number charge2))
    (update-in charge1 [:amount] + (:amount charge2))
    (throw (Exception. "Can't combine charges to different cards."))))

(defn- unzip
  "Returns pair of collections where first collection is built from first
   values of each pair in the coll argument and the second collection
   is built from the second value of each pair."
  [coll]
  (reduce (fn [[coll-a coll-b] [a b]]
            [(conj coll-a a) (conj coll-b b)])
          [[] []]
          coll))

(defn buy-coffees
  "Buy multiple times a cofee and combine all charges for given credit card.
  The first parameter accepts a credit card type and
  the second parameter is the number of times a coffee is bought."
  [cc n]
  (let [[coffees charges] (unzip (repeatedly n #(buy-coffee cc)))]
    [coffees (reduce combine charges)]))

(defn coalesce
  "Coalesce same card charges."
  [charges]
  (->> charges
       (group-by :credit-card)
       (vals)
       (map #(reduce combine %))))

In our tests we can now also use the functions to create a Coffee, CreditCard and Charge records:

(ns mrhaki.coffee.cafe4-test
  (:require [clojure.test :refer :all]
            [mrhaki.coffee.cafe4 :refer :all]))

(deftest buy-coffee-test
  (let [[coffee charge] (buy-coffee (->CreditCard "123-456"))]
    (is (= (->Coffee 2.99) coffee))
    (is (= (->Charge (->CreditCard "123-456") 2.99)) charge)))

(deftest buy-coffees-test
  (let [[coffees charge] (buy-coffees (->CreditCard "123-456") 2)]
    (is (= (repeat 2 (->Coffee 2.99)) coffees))
    (is (= (->Charge (->CreditCard "123-456") 5.98)) charge)))

(deftest coalesce-test
  (let [cc1     (->CreditCard "123-456")
        cc2     (->CreditCard "456-789")
        charges (coalesce [(->Charge cc1 2.99)
                           (->Charge cc2 2.99)
                           (->Charge cc1 2.99)])]
    (is (= (->Charge (->CreditCard "123-456") 5.98)) (first charges))
    (is (= (->Charge (->CreditCard "456-789") 2.99)) (second charges))))

Written with Clojure 1.10.1