The first thing you need to do is to add two dependencies into your project: parapet-core and interop-{effect_library} for a specific effect library. You can find the latest version in maven central.

libraryDependencies += "io.parapet" %% "core" % version

For Cats Effect add libraryDependencies += "io.parapet" %% "interop-cats" % version

For Monix add libraryDependencies += "io.parapet" %% "interop-monix" % version"

For Scalaz ZIO add libraryDependencies += "io.parapet" %% "interop-scalaz-zio" % version

Once you added the library, you can start writing your first program. However, it's worth taking a few minutes and getting familiar with two main approaches to write processes: Generic and Effect Specific . I'll describe both in a minute. For those who aren't familiar with effect systems like Cats Effect , I'd recommend to read the Wiki page about Effect system or the Cats Effect official documentation. Fortunately, you don't need to be an expert in Cats Effect or any other effect system library to use Parapet.

The first approach we'll consider is Generic . It's recommended to stick to this style when writing processes. Let's develop a simple Printer process that will print users requests to the system output.

import io.parapet.core.{Event, Process} class Printer[F[_]] extends Process[F] { import Printer._ // import Printer API import dsl._ // import DSL operations override def handle: Receive = { case Print(data) => eval(println(data)) } } object Printer { case class Print(data: Any) extends Event }

Process

F[_]

Printer

F[_]

IO

IO[Unit]

generic

IO

Printer

Let's walk through this code. You start writing your processes by extendingtrait and parameterizing it with an effect type. In this example, we left so-called holein ourtype which can be any type constructor with a single argument, e.g.is a generic type constructor, cats effectis a specific type constructor andis a concrete type. Starting from this moment, it should become clear what it means for a process to be. Simply speaking, it means that a process doesn't depend on any specific effect type e.g.. Thus we can claim that ourprocess is surely generic.

The next step is to define a process API or contract that defines a set of events that it can send and receive. Process contract is an important part of any process specification that should be taken seriously. API defines a protocol that other processes will use to communicate with your process. Please remember that it's a very important aspect of any process definition and take it seriously.

Then we need to import DSL smart constructors. Parapet DSL is a small set of operations that we will consider in detail in the next chapters. In this example, we need only eval operator that suspends a side effect in F , in our Printer process we suspend println effectful computation.

Finally, every process should override handle function defined in Process trait. handle function is a partial function that matches input events and produces an executable flows . If you ever tried Akka framework you may find this approach familiar (for the curious, Receive is simply a type alias for PartialFunction[Event, DslF[F, Unit]] ). In our Printer process, we match on Print event using a well known pattern-matching feature in Scala language. If you are new in functional programming, I'd recommend to read about pattern-matching - it's a very powerful instrument.

That's it. We have considered every important aspect of our Printer process.

Let's move forward and write a simple client process that will talk to our Printer .

import io.parapet.core.Event.Start import io.parapet.core.{Process, ProcessRef} import io.parapet.examples.Printer._ // import Printer API class PrinterClient[F[_]](printer: ProcessRef) extends Process[F] { override def handle: Receive = { // Start is a lifecycle event that gets delivered when a process started case Start => Print("hello world") ~> printer } }

As you already might have noticed, we are repeating the same steps we made when were writing our Printer process:

Create a new Process with a hole F[_] in its type definition

in its type definition Extend io.parapet.core.Process trait and parametrizing it with generic effect type F

trait and parametrizing it with generic effect type Implement handle partial function

Let's consider some new types and operators we have used to write our client: ProcessRef , Start lifecycle event and ~> (send) infix operator. Let's start from ProcessRef . ProcessRef is a unique process identifier (UUID by default). It represents a process address in Parapet system and must be unique - it's recommended to use ProcessRef instead of a Process object directly unless you are sure you want otherwise. It's not prohibited to use Process object directly, however using a process reference may be useful in some scenarios. Let's consider one such case. Imagine we want to dynamically change the current Printer process in our client so that it will store data in a file on disk instead of printing it to the console. We can add a new event ChangePrinter :

case class ChangePrinter(printer: ProcessRef) extends Event

Then our client will look like this:

class PrinterClient[F[_]](private var printer: ProcessRef) extends Process[F] { import PrinterClient._ import dsl._ override def handle: Receive = { case Start => Print("hello world") ~> printer case ChangePrinter(newPrinter) => eval(printer = newPrinter) } } object PrinterClient { case class ChangePrinter(printer: ProcessRef) extends Event }

This design cannot be achieved when using direct processes b/c it's not possible to send `Process` objects, processes are NOT serializable in general. One more thing, you can override a Process#ref field, only make sure it's unique otherwise Parapet system will return an error during the startup.

Ok, we are almost done! There are a few more things left we need to cover: Start lifecycle event and ~> operator and there is nothing special about these two. Parapet has two lifecycle events:

Start event is sent to a process once it's created in Parapet system

event is sent to a process once it's created in Parapet system Stop event is sent to a process when an application is interrupted with Ctrl-C or when some other process sent Stop or Kill event to that process. The main difference between Stop and Kill is that in the former case a process can finish processing all pending events before it will receive Stop event, whereas Kill will interrupt a process and then deliver Stop event, all pending events will be discarded. If you familiar with Java ExecutorService then you can think of Stop as shutdown and Kill as shutdownNow .

Finally ~> is the most frequently used operator that is defined for any type that extends Event trait. ~> is just a symbolic name for send(event, processRef) operator.

By this moment we have two processes: Printer and PrinterClient , nice! But wait, we need to run them somehow, right? Fortunately, it's extremely easy to do so, all we need is to create PrinterApp object which represents our application and extend it from CatsApp abstract class. CatsApp extends ParApp by specifying concrete effect type IO :

abstract class CatsApp extends ParApp[IO]

CatsApp comes from interop-cats library.

import cats.effect.IO import io.parapet.CatsApp import io.parapet.core.Process object PrinterApp extends CatsApp { override def processes: IO[Seq[Process[IO]]] = IO { val printer = new Printer[IO] val printerClient = new PrinterClient[IO](printer.ref) Seq(printer, printerClient) } }

This is Cats Effect specific application, meaning it uses IO type under the hood. If you run your program you should see hello world printed to the console. Also notice that we are using concrete effect type IO to fill the hole in our Printer type, e.g.: new Printer[IO] in practice it can be any other effect type like Task .

In our example, we created PrinterClient which does nothing but sending Print event at the startup. In my opinion, it doesn't deserve to be a standalone process, would be better if we create a process in place:

object PrinterApp extends CatsApp { override def processes: IO[Seq[Process[IO]]] = IO { val printer = new Printer[IO] val start = Process[IO](_ => { case Start => Printer.Print("hello world") ~> printer.ref }) Seq(start, printer) } }

Although it's a matter of taste, there is no hard rule.