Search

Dark theme | Light theme

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

March 5, 2021

Gradle Goodness: Enabling Preview Features For Java

Java introduced preview features in the language since Java 12. This features can be tried out by developers, but are still subject to change and can even be removed in a next release. By default the preview features are not enabled when we want to compile and run our Java code. We must explicitly specify that we want to use the preview feature to the Java compiler and Java runtime using the command-line argument --enable-preview. In Gradle we can customize our build file to enable preview features. We must customize tasks of type JavaCompile and pass --enable-preview to the compiler arguments. Also tasks of type Test and JavaExec must be customized where we need to add the JVM argument --enable-preview.

In the following Gradle build script written in Kotlin we have a Java project written with Java 15 where we reconfigure the tasks to enable preview features:

plugins {
    java
    application
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.1")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.1")
}

application {
    mainClass.set("mrhaki.Patterns")
}

tasks {
    val ENABLE_PREVIEW = "--enable-preview"

    // In our project we have the tasks compileJava and
    // compileTestJava that need to have the
    // --enable-preview compiler arguments.
    withType<JavaCompile>() {
        options.compilerArgs.add(ENABLE_PREVIEW)

        // Optionally we can show which preview feature we use.
        options.compilerArgs.add("-Xlint:preview")

        // Explicitly setting compiler option --release
        // is needed when we wouldn't set the
        // sourceCompatiblity and targetCompatibility
        // properties of the Java plugin extension.
        options.release.set(15)
    }

    // Test tasks need to have the JVM argument --enable-preview.
    withType<Test>() {
        useJUnitPlatform()
        jvmArgs.add(ENABLE_PREVIEW)
    }

    // JavaExec tasks need to have the JVM argument --enable-preview.
    withType<JavaExec>() {
        jvmArgs.add(ENABLE_PREVIEW)
    }
}

Written with Gradle 6.8.3

March 3, 2021

Java Joy: Apply Function To String With Transform

In Java 12 the transform method was add to the String class. This method accepts a Function as argument. The function must have a single parameter of type String and can return any other type. The nice thing is that it works on a String instance, so we can directly use the transform method when we have a String value. We don't have to pass the String object to another method to tranform it, but we can define the tranformation function close to the String value.

In the following example we take a String value and apply some functions with the transform method:

package mrhaki.lang;

import java.util.stream.Collectors;

public class StringTransform {
    public static void main(String[] args) {
        String alphabet = "abcdefghijklmnopqrstuvwxyz";
        
        // Find all letters that have an even
        // int representation and join the results
        // back into a string.
        String letters = alphabet
                .transform(s -> s.chars()
                                 .filter(n -> n % 2 == 0)
                                 .mapToObj(n -> String.valueOf((char) n))
                                 .collect(Collectors.joining()));

        assert "bdfhjlnprtvxz".equals(letters);

        // Transform the string to a User object.
        User user = "mrhaki,Hubert Klein Ikkink"
                .transform(name -> new User(name.split(",")[0], name.split(",")[1]));

        assert "mrhaki".equals(user.alias);
        assert "Hubert Klein Ikkink".equals(user.fullName);
    }

    /**
     * Simple class to store alias and full name.
     */
    private static class User {
        private final String alias, fullName;

        private User(final String alias, final String fullName) {
            this.alias = alias;
            this.fullName = fullName;
        }
    }
}

Written with Java 15.

March 1, 2021

Java Joy: Format Numbers In Compact Form

Since Java 12 we can format numbers in a compact style with the CompactNumberFormat class in the java.text package. A number like 23000 is formatted as 23K for the English locale. Instead of the short representation of K for 1000 we can also use a longer style where K is transformed as thousand for the English locale. We can use the same class to parse a String value that is in the compact style into a number.

In the following example we use several options of the CompactNumberFormat class:

package mrhaki.text;

import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;

public class CompactNumberFormat {

    public static void main(String[] args) throws ParseException {
        // When we would use NumberFormat.getCompactNumberInstance() the default system Locale
        // is used with the short style.
        // Create formatter for UK Locale and short style. 
        NumberFormat nf = NumberFormat.getCompactNumberInstance(Locale.UK, NumberFormat.Style.SHORT);
        assert "23K".equals(nf.format(23_000));
        assert "23K".equals(nf.format(23_491));
        assert "24K".equals(nf.format(23_791));
        assert "4M".equals(nf.format(4_250_392));

        nf.setMinimumFractionDigits(1);
        assert "4.3M".equals(nf.format(4_250_392));

        nf.setMaximumFractionDigits(3);
        assert "4.25M".equals(nf.format(4_250_392));

        // We can also parse the String value back to a number.
        assert Long.valueOf(23491).equals(nf.parse("23.491K"));

        // Instead of a short style we can also use a long style.
        nf = NumberFormat.getCompactNumberInstance(Locale.UK, NumberFormat.Style.LONG);
        assert "23 thousand".equals(nf.format(23_000));
        assert "4 million".equals(nf.format(4_250_392));

        // We can specify the minimum fraction digits to be used in the formatted number.
        nf.setMinimumFractionDigits(1);
        assert "4.3 million".equals(nf.format(4_250_392));

        // And the maximum fraction digits to change the ouput.
        nf.setMaximumFractionDigits(3);
        assert "4.25 million".equals(nf.format(4_250_392));
        
        // If we would use another locale we can see the output will be changed
        // if applicable.
        Locale dutch = new Locale("nl", "NL");
        nf = NumberFormat.getCompactNumberInstance(dutch, NumberFormat.Style.SHORT);
        assert "23K".equals(nf.format(23_230));

        nf = NumberFormat.getCompactNumberInstance(dutch, NumberFormat.Style.LONG);
        assert "23 duizend".equals(nf.format(23_230));
    }
}

Written with Java 15.

February 25, 2021

Java Joy: Merge Maps Using Stream API

In Java we can merge a key/value pair into a Map with the merge method. The first parameter is the key, the second the value and the third parameter of the merge method is a remapping function that is applied when the key is already present in the Map instance. The remapping function has the value of the key in the original Map and the new value. We can define in the function what the resulting value should be. If we return null the key is ignored.

If we want to merge multiple Map instances we can use the Stream API. We want to convert the Map instances to a stream of Map.Entry instances which we then turn into a new Map instance with the toMap method from the class Collectors. The toMap method also takes a remapping function when there is a duplicate key. The function defines what the new value is based on the two values of the duplicate key that was encountered. We can choose to simply ignore one of the values and return the other value. But we can also do some computations in this function, for example creating a new value using both values.

In the following example we use the Stream API to merge multiple Map instances into a new Map using a remapping function for duplicate keys:

package com.mrhaki.sample;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class MapMerge {
    public static void main(String[] args) {
        Map<Character, Integer> first = Map.of('a', 2, 'b', 3, 'c', 4);
        Map<Character, Integer> second = Map.of('a', 10, 'c', 11);
        Map<Character, Integer> third = Map.of('a', 3, 'd', 100);

        // First we turn multiple maps into a stream of entries and
        // in the collect method we create a new map and define
        // a function to multiply the entry value when there is a 
        // duplicate entry key.
        Map<Character, Integer> result =
                Stream.of(first, second, third)
                      .flatMap(m -> m.entrySet().stream())
                      .collect(
                              Collectors.toMap(
                                      Map.Entry::getKey,
                                      Map.Entry::getValue,
                                      (value1, value2) -> value1 * value2));

        // The values for duplicate keys are multiplied in the resulting map.
        assert Map.of('a', 60, 'b', 3, 'c', 44, 'd', 100).equals(result);


        // In this sample the value is a Java class Characteristic.
        // The function to apply when a key is duplicate will create
        // a new Characteristic instance contains all values.
        // The resulting map will contain all concatenated characteristic values
        // for each key.
        var langauges =
                Stream.of(Map.of("Java", new Characteristic("jvm")),
                          Map.of("Clojure", new Characteristic("dynamic", "functional")),
                          Map.of("Groovy", new Characteristic("jvm", "dynamic")),
                          Map.of("Clojure", new Characteristic("jvm")),
                          Map.of("Groovy", new Characteristic("dynamic")),
                          Map.of("Java", new Characteristic("static")))
                      .flatMap(m -> m.entrySet().stream())
                      .collect(
                              Collectors.toMap(
                                      Map.Entry::getKey,
                                      Map.Entry::getValue,
                                      (c1, c2) -> c1.addCharateristics(c2.getValues())));

        assert new Characteristic("static", "jvm").equals(langauges.get("Java"));
        assert new Characteristic("dynamic", "functional", "jvm").equals(langauges.get("Clojure"));
        assert new Characteristic("dynamic", "jvm").equals(langauges.get("Groovy"));
    }

    /**
     * Supporting class to store language characteristics.
     */
    static class Characteristic {
        // Store unique characteristic value.
        private Set<String> values = new HashSet<>();

        Characteristic(String characteristic) {
            values.add(characteristic);
        }

        Characteristic(String... characteristics) {
            values.addAll(Arrays.asList(characteristics));
        }

        Characteristic addCharateristics(Set<String> characteristics) {
            values.addAll(characteristics);
            return this;
        }

        Set<String> getValues() {
            return values;
        }

        @Override
        public boolean equals(final Object o) {
            if (this == o) { return true; }
            if (o == null || getClass() != o.getClass()) { return false; }
            final Characteristic that = (Characteristic) o;
            return Objects.equals(values, that.values);
        }

        @Override
        public int hashCode() {
            return Objects.hash(values);
        }
    }
}

Written with Java 15.

February 23, 2021

Clojure Goodness: Merge Maps With Function To Set Value Duplicate Keys

In Clojure we can use the merge function to merge multiple maps into a single map. If a key is in multiple maps the value of the key merged last will be used in the resulting map. If we want to influence how the value of a duplicate key is set we can use merge-with. We specify as first argument the function that will be used when the same key is available in multiple maps. The function must accept two arguments, where the the first argument is the value of the key in the first map and the second argument the value of the same key in the following map. The result is assigned to the key in the resulting map. If we pass more than two maps to the merge-with the function will be called multiple times for a key if it is part of more than two maps.

In the following example we use Clojure core functions and a custom function to merge multiples maps, so we can alter the value for duplicate keys:

(ns mrhaki.core.merge-with
  (:require [clojure.test :refer [is]]))

;; Merge maps and use the function specified as first argument
;; to calculate the value for keys that are present
;; in multiple maps.
(is (= {:a 60 :b 3 :c 44 :d 100}
       (merge-with * {:a 2 :b 3 :c 4} {:a 10 :c 11} {:a 3 :d 100})))

;; Works for all maps and independent of type that is used for keys.
;; We can use any function for merge-with.
(def languages (merge-with (comp vec flatten conj) {"Clojure" [:dynamic :functional]}
                           {"Java" [:jvm]}
                           {"Groovy" [:jvm]}
                           {"Clojure" [:jvm]}
                           {"Groovy" [:dynamic]}))

(is (= {"Clojure" [:dynamic :functional :jvm]
        "Java"    [:jvm]
        "Groovy"  [:jvm :dynamic]}
       languages))


;; Sample map with small inventory.
(def inventory {"pencil" {:count 10 :price 0.25}
                "pen"    {:count 23 :price 0.4}})
;; Sample basket with items.
(def basket {"pencil" {:count 5} "pen" {:count 2}})

;; Function to subtract the :count value for a basket item
;; from the :count value for the same inventory item.
(defn item-sold
  [inventory-item basket-item]
  (update-in inventory-item [:count] - (:count basket-item)))

(is (= {"pencil" {:count 5 :price 0.25}
        "pen"    {:count 21 :price 0.4}}
       (merge-with item-sold inventory basket)))

Written with Clojure 1.10.1.

February 19, 2021

Java Joy: Composing Functions

In Java we can write single argument functions that implement the java.util.function.Function interface. We can combine multiple functions into a new function using the andThen and compose methods from the Function interface. We need to give another function as argument to these methods. When we use the andThen method the output of the original function will be input of the function passed as argument. With the compose method our function will get as input the output of the function that is passed as argument. It is important to know the difference, because it can change the result of the function we are composing. The andThen and compose methods are also available on the IntUnaryOperator, LongUnaryOperator and DoubleUnaryOperator interface.

In the following example we use both andThen and compose to chain together some functions. We can see the result can be different when using andThen and compose with the same functions.

package com.mrhaki.sample;

import java.util.Map;
import java.util.function.Function;
import java.util.function.IntUnaryOperator;
import java.util.function.UnaryOperator;

public class Compose {
    public static void main(String[] args) {
        // Two simple functions that take a int argument
        // and do some calculations.
        IntUnaryOperator f = x -> 11 + (x - 90);
        IntUnaryOperator g = x -> x * 2;

        // Using andThen will first execute f and use
        // the result as input for g:
        // (11 + (100 - 90)) * 2
        assert 42 == f.andThen(g).applyAsInt(100);

        // Using compose will first execute g and use
        // the result as input for f:
        // 11 + ((100 * 2) - 90)
        assert 121 == f.compose(g).applyAsInt(100);


        // Map with some user data.
        var user =
                Map.of("name", "Hubert",
                       "alias", "MrHaki");

        // Function to duplicate a String.
        UnaryOperator<String> duplicate = s -> String.format("%1$s,%1$s", s);

        // Function to turn String into lowercase.
        UnaryOperator<String> lowerCase = String::toLowerCase;

        // Function with Map parameter to create a new function with
        // a String parameter that will get the
        // value for a given key from the map that is passed.
        Function<Map<String, String>, UnaryOperator<String>> getFromMap =
                map -> key -> map.get(key);

        // Chain using andThen.
        Function<String, String> andThenUserKey =
                getFromMap.apply(user)
                          .andThen(lowerCase)
                          .andThen(duplicate);

        assert "mrhaki,mrhaki".equals(andThenUserKey.apply("alias"));

        // Chain using compose.
        Function<String, String> composeUserKey =
                duplicate.compose(lowerCase)
                         .compose(getFromMap.apply(user));

        assert "mrhaki,mrhaki".equals(composeUserKey.apply("alias"));
    }
}

Written with Java 15.

February 17, 2021

Gradle Goodness: Setting Plugin Version From Property In Plugins Section

The plugins section in our Gradle build files can be used to define Gradle plugins we want to use. Gradle can optimize the build process if we use plugins {...} in our build scripts, so it is a good idea to use it. But there is a restriction if we want to define a version for a plugin inside the plugins section: the version is a fixed string value. We cannot use a property to set the version inside the plugins section. We can overcome this by using a pluginsManagement section in a settings file in the root of our project. Inside the pluginsManagement section we can use properties to set the version of a plugin we want to use. Once it is defined inside pluginsManagement we can use it in our project build script without having the specify the version. This allows us to have one place where all plugin versions are defined. We can even use a gradle.properties file in our project with all plugin versions and use that in pluginsManagement.

In the following settings file we use pluginsManagement to use a project property springBootPluginVersion to set the version to use for the Spring Boot Gradle plugin.

// File: settings.gradle.kts
pluginManagement {
    val springBootPluginVersion: String by settings // use project property with version
    plugins {
        id("org.springframework.boot") version "${springBootPluginVersion}"
    }
}

Next in our project build file we can simply reference the id of the Spring Boot Gradle plugin without the version. The version is already resolved in our settings file:

// File: build.gradle.kts
plugins {
    java
    application
    id("org.springframework.boot") // no version here: it is set in settings.gradle.kts
}

application {
    mainClass.set("com.mrhaki.sample.App")
}

Finally we can add a gradle.properties file with the project property (or specify it on the command line or environment variable):

# File: gradle.properties
springBootPluginVersion=2.4.2

Written with Gradle 6.8.2.

February 16, 2021

Gradle Goodness: Shared Configuration With Conventions Plugin

When we have a multi-module project in Gradle we sometimes want to have dependencies, task configuration and other settings shared between the multiple modules. We can use the subprojects or allprojects blocks, but the downside is that it is not clear from the build script of the subproject where the configuration comes from. We must remember it is set from another build script, but there is no reference in the subproject to that connection. It is better to use a plugin with shared configuration and use that plugin in the subprojects. We call this a conventions plugin. This way it is explicitly visible in a subproject that the shared settings come from a plugin. Also it allows Gradle to optimize the build configuration.

The easiest way to implement the shared configuration in a plugin is using a so-called precompiled script plugin. This type of plugin can be written as a build script using the Groovy or Kotlin DSL with a filename ending with .gradle or .gradle.kts. The name of the plugin is the first part of the filename before .gradle or .gradle.kts. In our subproject we can add the plugin to our build script to apply the shared configuration. For a multi-module project we can create such a plugin in the buildSrc directory. For a Groovy plugin we place the file in src/main/groovy, for a Kotlin plugin we place it in src/main/kotlin.

In the following example we write a script plugin using the Kotlin DSL to apply the java-library plugin to a project, set some common dependencies used by all projects, configure the Test tasks and set the Java toolchain. First we create a build.gradle.kts file in the buildSrc directory in the root of our multi-module project and apply the kotlin-dsl plugin:

// File: buildSrc/build.gradle.kts
plugins {
    `kotlin-dsl`
}

repositories.mavenCentral()

Next we create the conventions plugin with our shared configuration:

// File: buildSrc/src/main/kotlin/java-project-conventions.gradle.kts
plugins {
    `java-library`
}

group = "mrhaki.sample"
version = "1.0"

repositories {
    mavenCentral()
}

dependencies {
    val log4jVersion: String by extra("2.14.0")
    val junitVersion: String by extra("5.3.1")
    val assertjVersion: String by extra("3.19.0")
    
    // Logging
    implementation("org.apache.logging.log4j:log4j-api:${log4jVersion}")
    implementation("org.apache.logging.log4j:log4j-core:${log4jVersion}")

    // Testing
    testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}")
    testImplementation("org.assertj:assertj-core:${assertjVersion}")
}

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

tasks.withType<Test> {
    useJUnitPlatform()
}

The id of our new plugin is java-project-conventions and we can use it in our build script for a subproject as:

// File: rest-api/build.gradle.kts
plugins {
    id("java-project-conventions")  // apply shared config
    application  // apply the Gradle application plugin
}

dependencies {
    val vertxVersion: String by extra("4.0.2")

    implementation(project(":domain"))  // project dependency
    implementation("io.vertx:vertx-core:${vertxVersion}")
}

application {
    mainClass.set("com.mrhaki.web.Api")
}

The rest-api project will have all the configuration and tasks from java-library plugin as configured in the java-project-conventions plugin, so we can build it as a Java project.

Written with Gradle 6.8.2.

February 9, 2021

Clojure Goodness: Destructure Sequences

Clojure supports advanced destructure features. In a previous post we learned about destructuring maps, but we can also destructure vectors, list and sequences in Clojure using positional destructuring. We can define symbols for positions in the sequence to assign the value at a certain position to the symbol. The first symbol in the destructure vector gets the value of the first element in the sequence, the second symbol the value of the second element and so on. To get the remaining elements from the sequence without assigning them to specific symbols we can use & followed by a symbol. Then all remaining elements are assigned as sequence the symbol. Finally we can use :as to get the original vector, list or sequence.

The folowing examples show several destructure definitions for different type of collections and sequences:

(ns mrhaki.lang.destruct-seq
  (:require [clojure.test :refer [is]]))

(def items ["mrhaki" "Hubert Klein Ikkink" "Tilburg"])

;; Elements from the items vector are positionally
;; destructured to symbols.
(let [[alias name city] items]
  (is (= "mrhaki" alias))
  (is (= "Hubert Klein Ikkink" name))
  (is (= "Tilburg" city)))

;; When we define a symbol but there are no elements
;; to assign a value, the symbol will be nil.
(let [[alias name city country] items]
  (is (nil? country)))

;; When we don't need the destructured symbol we can
;; use the underscore to indicate this. But any name will do.
(let [[username _ city] items]
  (is (= "mrhaki lives in Tilburg" 
         (str username " lives in " city))))

;; We can destructure sequences just like vectors.
(def coords '(29.20090, 12.90391))

(let [[x y] coords]
  (is (= 29.20090 x))
  (is (= 12.90391 y)))

(let [[first-letter _ third-letter] "mrhaki"]
  (is (= \m first-letter))
  (is (= \h third-letter)))


;; We can nest our destructure definitions.
(def currencies [[42 "EUR"] [50 "USD"]])

;; We want the second value of the first element and
;; the first value of the second element.
(let [[[_ currency] [amount _]] currencies]
  (is (= "EUR" currency))
  (is (= 50 amount)))

;; Example sequence with fruit names.
(def basket '("Apple" "Pear" "Banana" "Grapes" "Lemon"))

;; We can use & to assign all remaining not-yet 
;; destructured element to a sequence.
(let [[first second & rest] basket]
  (is (= "Apple" first))
  (is (= "Pear" second))
  (is (= ["Banana" "Grapes" "Lemon"] rest)))

;; We can use :as to get the original sequence.
(let [[first _ third :as fruits] basket]
  (is (= "Apple" first))
  (is (= "Banana" third))
  (is (= "APBGL" (apply str (map #(.charAt % 0) fruits)))))


;; Use destructure in function parameter to 
;; destructure the argument value when invoked.
(defn summary
  [[first second :as all]]
  (str first ", " second " and " (- (count all) 2) " more fruit names."))

(is (= "Apple, Pear and 3 more fruit names."
       (summary basket)))

Written with Clojure 1.10.1.