# Getting Started

# Design Concept

Fun I/O employs a few simple design principles:

  • The API is defined by abstract classes and interfaces in the module fun-io-api.
  • The module fun-io-scala-api adds operators and implicit conversions for an enhanced development experience in Scala.
  • Each implementation module provides a single facade class which consists of one or more static factory methods.
  • Each static factory method returns an instance of a class or interface defined by the API without revealing the actual implementation class.
  • Except for their expected side effect (e.g. reading or writing data), implementations are virtually stateless, and hence reusable and trivially thread-safe.

With this design, the canonical way of using Fun I/O is to import some static factory methods from one or more facade classes. It's perfectly fine to import all static factory methods using a wildcard like *. However, for the purpose of showing the originating facade class, the examples on this page do not use wildcard imports for static factory methods.

# Configuring The Classpath

Once you've decided on the set of features required by your application you need to add the respective modules to the class path - see Module Structure And Features. A Java application typically has a dependency on fun-io-bios. A Scala application typically has the same dependencies as a Java application plus an additional dependency on fun-io-scala-api to improve the development experience in Scala.

The examples on this page depend on fun-io-bios, fun-io-jackson, fun-io-scala-api and, transitively, fun-io-api. If your application is a Java project build with Maven or a Scala project build with SBT, then you need to add the following to its project configuration:

<!-- pom.xml -->
<project [...]>
    [...]
    <dependencies>
        [...]
        <dependency>
            <groupId>global.namespace.fun-io</groupId>
            <artifactId>fun-io-bios</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>global.namespace.fun-io</groupId>
            <artifactId>fun-io-jackson</artifactId>
            <version>2.3.0</version>
        </dependency>
    </dependencies>
</project>

# Encoding Objects

The following code encodes the string "Hello world!" to JSON and writes it to standard output - including the quotes:

import global.namespace.fun.io.api.Codec;       // from module `fun-io-api`
import global.namespace.fun.io.api.Encoder;
import global.namespace.fun.io.api.Sink;
import global.namespace.fun.io.bios.BIOS;       // from module `fun-io-bios`
import global.namespace.fun.io.jackson.Jackson; // from module `fun-io-jackson`

class Scratch {
    public static void main(String[] args) throws Exception {
        Codec codec = Jackson.json();
        Sink sink = BIOS.stdout();
        Encoder encoder = codec.encoder(sink);
        encoder.encode("Hello world!");
    }
}

In the preceding code, first an instance of the Codec interface is obtained from the Jackson facade class for encoding/decoding objects to/from JSON. Next, an instance of the interface Sink is obtained from the BIOS facade class for writing to standard output. The codec and the sink are then combined into an instance of the Encoder interface. Finally, the encoder is used to encode the string "Hello world!" and write it to standard output.

The preceding code can be simplified to the following one-liner:

import static global.namespace.fun.io.bios.BIOS.stdout;
import static global.namespace.fun.io.jackson.Jackson.json;

class Scratch {
    public static void main(String[] args) throws Exception {
        json().encoder(stdout()).encode("Hello world!");
    }
}

Note that importing static members of facade classes like BIOS and Jackson is the canonical way to use their API. As you can see, this leads to concise, easily comprehensible code.

There are many other Codecs and Sinks available - see Module Structure And Features. Also, the Sink interface is extended by the Store interface, of which you will find plenty implementations provided by facade classes like BIOS and others, as you will see in the next example.

# Applying Filters

# Encoding Objects To Files

The following example is only slightly more complex than the previous one. Again, the string "Hello world!" is encoded to JSON, but this time it also gets compressed using GZIP and the result saved to the file hello-world.gz:

import global.namespace.fun.io.api.Codec;
import global.namespace.fun.io.api.Filter;
import global.namespace.fun.io.api.Store;
import global.namespace.fun.io.bios.BIOS;
import global.namespace.fun.io.jackson.Jackson;

class Scratch {
    public static void main(String[] args) throws Exception {
        Filter gzip = BIOS.gzip();
        Filter buffer = BIOS.buffer();
        Codec codec = Jackson.json().map(gzip).map(buffer);
        Store store = BIOS.file("hello-world.gz");
        codec.encoder(store).encode("Hello world!");
    }
}

In the preceding code, first two instances of the Filter interface are obtained from the BIOS facade class which represent the GZIP compression and an heap buffer algorithm, respectively. Next, an instance of the Codec interface is obtained from the Jackson facade class for encoding/decoding objects to/from JSON, just like in the previous example. This time however, the map method is called to transform the codec into a new codec which applies the two filters. In Scala, this expression can be more concisely written as json << gzip << buffer, where the << operator is associative. Also, note that the buffer filter is applied last in order to minimize the number of subsequent write operations to the file. Next, an instance of the Store interface is obtained from the BIOS facade class which represents the file hello-world.gz. Finally, the codec is connected to the store and used to encode, compress and write the string "Hello world!" to the file hello-world.gz.

Again, using static imports from the facade classes, the preceding code can be simplified to the following:

import static global.namespace.fun.io.bios.BIOS.*;
import static global.namespace.fun.io.jackson.Jackson.json;

class Scratch {
    public static void main(String[] args) throws Exception {
        json()
                .map(gzip())
                .map(buffer())
                .encoder(file("hello-world.gz"))
                .encode("Hello world!");
    }
}

# Decoding Objects From Files

To read the object back from the file, you can use the following code:

import static global.namespace.fun.io.bios.BIOS.*;
import static global.namespace.fun.io.jackson.Jackson.json;

class Scratch {
    public static void main(String[] args) throws Exception {
        String clone = json()
                .map(gzip())
                .map(buffer())
                .decoder(file("hello-world.gz"))
                .decode(String.class);
        assert clone.equals("Hello world!");
    }
}

As you can see, it's analogous to the writing algorithm: You just need to replace the calls to the methods encoder and encode with decoder and decode, respectively. The composition of the JSON codec with the GZIP and buffer filters remains the same.

# Using Connected Codecs

Sometimes, an application needs to read and write some structured data to the same store again and again.
In this case, rather than repeatedly creating an instance of the Encoder interface for writing and its Decoder counterpart for reading, it's simpler to make a permanent connection of a Codec to a Store by using a ConnectedCodec:

import global.namespace.fun.io.api.ConnectedCodec;

import static global.namespace.fun.io.bios.BIOS.*;
import static global.namespace.fun.io.jackson.Jackson.json;

class Scratch {
    public static void main(String[] args) throws Exception {
        ConnectedCodec codec = json()
                .map(gzip())
                .map(buffer())
                .connect(file("hello-world.gz"));
        codec.encode("Hello world!");
        String clone = codec.decode(String.class);
        assert clone.equals("Hello world!");
    }
}

In the preceding code, the connect method connects the (transformed) Codec to a (file) Store into a ConnectedCodec. Subsequently, the connected codec is used to write and read back the string "Hello world!".

# Cloning Objects

A ConnectedCodec is an Encoder and a Decoder in one, so it can be used to create a deep clone of the original object, as seen in the previous example. To make this more useful, the gzip() and buffer() filters and the file(...) store can be removed and the encoded data get buffered on the heap instead:

import global.namespace.fun.io.api.ConnectedCodec;

import static global.namespace.fun.io.bios.BIOS.memory;
import static global.namespace.fun.io.jackson.Jackson.json;

class Scratch {
    public static void main(String[] args) throws Exception {
        ConnectedCodec codec = json().connect(memory());
        codec.encode("Hello world!");
        String clone = codec.decode(String.class);
        assert clone.equals("Hello world!");
    }
}

Note that the memory method returns just another instance of the Store interface which is backed by an array of bytes. Encoding and decoding can be done in a single step:

import global.namespace.fun.io.api.ConnectedCodec;

import static global.namespace.fun.io.bios.BIOS.memory;
import static global.namespace.fun.io.jackson.Jackson.json;

class Scratch {
    public static void main(String[] args) throws Exception {
        String clone = json().connect(memory()).clone("Hello world!");
        assert clone.equals("Hello world!");
    }
}

Because deep-cloning is a standard use case, there is a ready-made method in the BIOS facade for it:

import static global.namespace.fun.io.bios.BIOS.clone;

class Scratch {
    public static void main(String[] args) throws Exception {
        String c = clone("Hello world!");
        assert c.equals("Hello world!");
    }
}

In contrast to the previous examples, this method uses BIOS.serialization() instead of Jackson.json() as the Codec, so the object to clone must implement java.io.Serializable.

# Copying Data

The BIOS facade class provides some utility methods for standard use cases based on the abstractions provided by the API. One of these standard use cases is implemented by BIOS.copy in all its overloaded variants: Copying all data from a given source of some form to a given sink of some form. Other than the naive while-read-do-write loop, these copy methods employ a background thread and a ring buffer for reading the data and piping it to the current thread for writing the data. The result is a significant performance boost due to much better utilization of I/O channels:

import static global.namespace.fun.io.bios.BIOS.buffer;
import static global.namespace.fun.io.bios.BIOS.copy;
import static global.namespace.fun.io.bios.BIOS.file;
import static global.namespace.fun.io.bios.BIOS.gzip;

class Scratch {
    public static void main(String[] args) throws Exception {
        copy(file("file.gz").map(buffer()).map(gzip()), file("file"));
    }
}

The preceding code decompresses the data from the file file.gz and writes the decompressed data to the file file.