Saturday, September 18, 2021

Java 17: Pattern Matching for Switch

In Java 17 (released only a few days ago), Pattern Matching for switch has been introduced as a preview language feature, which allows case labels with patterns rather than just constants. Here is an example showing how you can match on type patterns:

public static String typedPatternMatching(Object o) {
  return switch(o) {
    case null      -> "I am null";
    case String s  -> "I am a String. My value is " + s;
    case Integer i -> "I am an int. My value is " + i;
    default        -> "I am of an unknown type. My value is " + o.toString();
  };
}

// Output:
> typedPatternMatching("HELLO")
"I am a String. My value is HELLO"

> typedPatternMatching(123)
"I am an int. My value is 123"

> typedPatternMatching(null)
"I am null"

> typedPatternMatching(0.5)
"I am of an unknown type. My value is 0.5"

You can also use a guarded pattern in order to refine a pattern so that it is only matched on certain conditions, for example:

public static String guardedPattern(Collection<String> coll) {
  return switch(coll) {
    case List list && (list.size() > 10) -> 
        "I am a big List. My size is " + list.size();
    case List list -> 
        "I am a small List. My size is " + list.size();
    default -> 
        "Unsupported collection: " + coll.getClass();
  };
}

If you have a Sealed Class (made a permanent language feature in Java 17), the compiler can verify if the switch statement is complete so no default label is needed. For example:

sealed interface Vehicle permits Car, Truck, Motorcycle {}
final class Car implements Vehicle {}
final class Truck implements Vehicle {}
final class Motorcycle implements Vehicle {}

public static String sealedClass(Vehicle v) {
  return switch(v) {
    case Car c -> "I am a car";
    case Truck t -> "I am a truck";
    case Motorcycle m -> "I am a motorcycle";
  };
}

Saturday, August 14, 2021

Java 16: Stream.mapMulti

Java 16 introduces a new Stream.mapMulti method which allows you to replace elements in a stream with multiple elements.

The example below shows how you can use mapMulti to replace each string in a stream with its uppercased and lowercased versions:

Stream.of("Twix", "Snickers", "Mars")
  .mapMulti((s, c) -> {
    c.accept(s.toUpperCase());
    c.accept(s.toLowerCase());
  })
  .forEach(System.out::println);

Output:
TWIX
twix
SNICKERS
snickers
MARS
mars

The same thing can also be achieved using flatMap like this:

Stream.of("Twix", "Snickers", "Mars")
  .flatMap(s -> Stream.of(s.toUpperCase(), s.toLowerCase()))
  .forEach(System.out::println);

So what is the difference between mapMulti and flatMap? According to the javadocs:

This method is preferable to flatMap in the following circumstances:

  • When replacing each stream element with a small (possibly zero) number of elements. Using this method avoids the overhead of creating a new Stream instance for every group of result elements, as required by flatMap.
  • When it is easier to use an imperative approach for generating result elements than it is to return them in the form of a Stream.

Inspecting the code for multiMap, we can see that it delegates to flatMap, however, it makes use of a SpinedBuffer to hold the elements before creating the stream, thus avoiding the overhead of creating new streams per group of result elements.

default <R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper) {
  Objects.requireNonNull(mapper);
  return flatMap(e -> {
    SpinedBuffer<R> buffer = new SpinedBuffer<>();
    mapper.accept(e, buffer);
    return StreamSupport.stream(buffer.spliterator(), false);
  });
}

Saturday, April 10, 2021

Java 16: Stream.toList()

Java 16 introduces a handy new Stream.toList() method which makes it easier to convert a stream into a list. The returned list is unmodifiable and calls to any mutator method will throw an UnsupportedOperationException.

Here is some sample code:

import java.util.stream.Stream;
import static java.util.stream.Collectors.*;

// Java 16
stream.toList(); // returns an unmodifiable list

// Other ways to create Lists from Streams:

stream.collect(toList());

stream.collect(toCollection(LinkedList::new)); // if you need a specific type of list

stream.collect(toUnmodifiableList());  // introduced in Java 10

stream.collect(
    collectingAndThen(toList(), Collections::unmodifiableList)); // pre-Java 10

Related post: Java 10: Collecting a Stream into an Unmodifiable Collection

Monday, April 05, 2021

Java 16: Records and Pattern Matching for instanceof

In Java 16, Records and Pattern Matching have been made a final and permanent feature of the Java language!

I blogged about them when they were first released as preview language features back in Java 14 here:

Friday, April 02, 2021

kdb+/q - Display a Table as a Tree

This post shows how you can convert a keyed table to a hierarchical tree format in kdb+/q. This could be useful if you want to display data as a tree widget in a front-end.

Consider the following keyed table of world populations:

continent     country        city            | population
---------------------------------------------| ----------
North America United States  New York City   | 8550405
North America United States  Los Angeles     | 3971883
North America Mexico         Mexico City     | 8918653
Europe        United Kingdom London          | 9126366
Europe        Russia         Moscow          | 12195221
Europe        Russia         Saint Petersburg| 5383890
Africa        Nigeria        Lagos           | 14862000
Africa        Egypt          Cairo           | 9908788
Africa        Egypt          Giza            | 8800000
Asia          China          Shanghai        | 22315474
Asia          India          Mumbai          | 12691836
Asia          China          Beijing         | 11716620

We would like to display it as a tree of continent > country > city, as shown below (similar to a pivot table in Excel):

node                        | population
----------------------------| ----------
Total                       | 128441136
    Asia                    | 46723930
        China               | 34032094
            Shanghai        | 22315474
            Beijing         | 11716620
        India               | 12691836
            Mumbai          | 12691836
    Africa                  | 33570788
        Egypt               | 18708788
            Cairo           | 9908788
            Giza            | 8800000
        Nigeria             | 14862000
            Lagos           | 14862000
    Europe                  | 26705477
        Russia              | 17579111
            Moscow          | 12195221
            Saint Petersburg| 5383890
        United Kingdom      | 9126366
            London          | 9126366
    North America           | 21440941
        United States       | 12522288
            New York City   | 8550405
            Los Angeles     | 3971883
        Mexico              | 8918653
            Mexico City     | 8918653

In order to achieve this, we need to aggregate the data with different groupings, then combine the resultant tables and format it into a tree.

1. Grouping the data

First, we will add a dummy Total column to the table and then aggregate the table with the following groupings:

  • Total
  • Total, continent
  • Total, continent, country
  • Total, continent, country, city

The code for this is shown below:

// add Total column to the table. (td is a keyed table)
td:(`Total,keys[td]) xkey update Total:`Total from td;
keyCols:keys td;

// create a list of groupings
groupings:(1+til count keyCols) sublist\: keyCols;

// aggregate the table with each grouping
// this gives us a list of keyed tables (one per grouping)
tds:?[td;();;c!(sum;)each c:cols value td] each {x!x} each groupings;

// this step is optional but it's nice to sort each table on population
tds:`population xdesc'tds;

2. Joining the data

Next, we need to join the tables that were obtained as a result of the groupings. We do this by unkeying the tables and then using uj:

td:keyCols xkey (uj/) 0!'tds;

3. Formatting the data

Now let's add a Path column by concatenating the key columns:

td:![td;();0b;enlist[`Path]!enlist(`$sv';">";(string;(each;{x except `};(flip;enlist,keyCols))))];
td:(`Path,keyCols) xkey td;

This is what our tree looks like so far:

Path                                            Total continent     country        city            | population
---------------------------------------------------------------------------------------------------| ----------
Total                                           Total                                              | 128441136
Total>Asia                                      Total Asia                                         | 46723930
Total>Africa                                    Total Africa                                       | 33570788
Total>Europe                                    Total Europe                                       | 26705477
Total>North America                             Total North America                                | 21440941
Total>Asia>China                                Total Asia          China                          | 34032094
Total>Africa>Egypt                              Total Africa        Egypt                          | 18708788
Total>Europe>Russia                             Total Europe        Russia                         | 17579111
Total>Africa>Nigeria                            Total Africa        Nigeria                        | 14862000
Total>Asia>India                                Total Asia          India                          | 12691836
Total>North America>United States               Total North America United States                  | 12522288
Total>Europe>United Kingdom                     Total Europe        United Kingdom                 | 9126366
Total>North America>Mexico                      Total North America Mexico                         | 8918653
Total>Asia>China>Shanghai                       Total Asia          China          Shanghai        | 22315474
Total>Africa>Nigeria>Lagos                      Total Africa        Nigeria        Lagos           | 14862000
Total>Asia>India>Mumbai                         Total Asia          India          Mumbai          | 12691836
Total>Europe>Russia>Moscow                      Total Europe        Russia         Moscow          | 12195221
Total>Asia>China>Beijing                        Total Asia          China          Beijing         | 11716620
Total>Africa>Egypt>Cairo                        Total Africa        Egypt          Cairo           | 9908788
Total>Europe>United Kingdom>London              Total Europe        United Kingdom London          | 9126366
Total>North America>Mexico>Mexico City          Total North America Mexico         Mexico City     | 8918653
Total>Africa>Egypt>Giza                         Total Africa        Egypt          Giza            | 8800000
Total>North America>United States>New York City Total North America United States  New York City   | 8550405
Total>Europe>Russia>Saint Petersburg            Total Europe        Russia         Saint Petersburg| 5383890
Total>North America>United States>Los Angeles   Total North America United States  Los Angeles     | 3971883

4. Reordering the rows

The tree looks okay so far and you can stop there if you want but it would look better if child nodes were directly under their parents e.g. Shanghai should appear under China. In order to do this, we cannot simply use uj to combine our tables but we need to use the Over (/) accumulator to build the tree instead.

In order to get the row ordering correct, we add an id to each row, which will be a combination of the parent id and the row id. These id's look like this: 0, 0.0, 0.1, 0.1.1 etc. and will be used to sort the tree so that children appear under their parents.

Here is the final version of the code:

// Converts a table into a tree.
// @param td - a keyed table
// @param sortCol - the column to sort on
// @returns a table with a tree column
table2tree:{[td;sortCol]
    // add Total column to the table
    td:(`Total,keys[td]) xkey update Total:`Total from td;
    keyCols:keys td;

    // create a list of groupings
    groupings:(1+til count keyCols) sublist\: keyCols;

    // aggregate the table with each grouping
    // this gives us a list of keyed tables (one per grouping)
    tds:?[td;();;c!(sum;)each c:cols value td] each {x!x} each groupings;

    // sort the tables
    if[not null sortCol;tds:sortCol xdesc'tds];

    // initial tree only has the Total row
    tree:update id:"0",node:enlist "Total" from 0!first tds;

    // build the tree using the over accumulator
    tree:{[tree;td]
        keyCols:keys td;

        // join the parent id to the current table
        td:td lj k xkey ?[tree;();0b;{x!x}(k:-1_keyCols),`id];

        // update the id by concatenating the parent id to the row id
        // we need to left-pad the row id so that sorting works correctly
        // e.g. 1.3 should come before 1.10
        td:update id:`$"."sv'flip(string id;(-1*count string count td)$string i) from td;

        // add a node column which corresponds to the value of the last key column
        td:![td;();0b;enlist[`node]!enlist last keyCols];

        // add indentation to the node based on the depth (i.e. number of key columns)
        indentation:(4*-1+count keyCols)#" ";
        td:update node:(indentation,/:string node) from td;

        // now add the table to tree
        tree uj 0!td
	}/[tree;1_tds];

    // sort the tree on id
    (`node,keyCols) xkey `id xasc tree}

This is what our final tree looks like:

q) data:3!("SSSI";enlist",") 0: `$"population.csv";
q) select node,population from table2tree[data;`population]

node                         population
-----------------------------------------
Total                        128441136
    Asia                     46723930
        China                34032094
            Shanghai         22315474
            Beijing          11716620
        India                12691836
            Mumbai           12691836
    Africa                   33570788
        Egypt                18708788
            Cairo            9908788
            Giza             8800000
        Nigeria              14862000
            Lagos            14862000
    Europe                   26705477
        Russia               17579111
            Moscow           12195221
            Saint Petersburg 5383890
        United Kingdom       9126366
            London           9126366
    North America            21440941
        United States        12522288
            New York City    8550405
            Los Angeles      3971883
        Mexico               8918653
            Mexico City      8918653

I also played around with adding lines to connect nodes of the tree but it got complicated very fast!

Can you think of a better way to do this? Let me know in the comments below!