Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/tailcallhq/zio-compose
A Scala DSL to write type-safe programs for distributed computing
https://github.com/tailcallhq/zio-compose
distributed-computing functional-programming scala
Last synced: about 1 month ago
JSON representation
A Scala DSL to write type-safe programs for distributed computing
- Host: GitHub
- URL: https://github.com/tailcallhq/zio-compose
- Owner: tailcallhq
- License: mit
- Created: 2022-07-17T10:56:26.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2024-04-16T04:35:10.000Z (9 months ago)
- Last Synced: 2024-04-20T09:07:53.187Z (8 months ago)
- Topics: distributed-computing, functional-programming, scala
- Language: Scala
- Homepage:
- Size: 837 KB
- Stars: 33
- Watchers: 2
- Forks: 3
- Open Issues: 24
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
![badge-workflow]
![badge-sonatype-releases]
![badge-sonatype-snapshots][badge-workflow]: https://github.com/tusharmath/graphql-compose/actions/workflows/ci.yml/badge.svg
[badge-sonatype-releases]: https://img.shields.io/nexus/r/https/oss.sonatype.org/com.tusharmath/zio-compose_2.13.svg "Sonatype Releases"
[badge-sonatype-snapshots]: https://img.shields.io/nexus/s/https/oss.sonatype.org/com.tusharmath/zio-compose_2.13.svg "Sonatype Snapshots"ZIO Compose is a library that helps you write programs that can be serialized and sent over the wire.
## Introduction
The basic idea behind having serializable programs is if code and data are on different machines, one of them **must be
moved** to the other before the code can be executed on the data.
Typically, in big-data applications it's much more efficient to move code than the other way around.There are other use-cases that don't involve big-data where you would want a serializable program. For eg: Building a
rule engine,
where the rules are implemented using a DSL and the DSL is serialized and sent to the server for execution.ZIO Compose intends to take care of such use cases. It intends to provide a complete DSL to write any kind of
distributed computation using Scala in a type-safe manner. It's built on top of [ZIO Schema].[zio schema]: https://github.com/zio/zio-schema/pulls
## Installation
Update your resolvers and add `zio-compose` as a dependency in your build.sbt.
```scala
libraryDependencies += "com.tusharmath" %% "zio-compose" % version
```# Getting started
- [Getting started](#getting-started)
- [Lambda](#lambda)
- [Serialization](#serialization)
- [Conditional](#conditional)
- [Piping](#piping)
- [Lenses](#lenses)
- [Transformations](#transformations)
- [Looping](#looping)
- [Scopes](#scopes)
- [Fibonacci](#fibonacci)1. Here is a simple program that adds two numbers -
```scala
import compose.Lambda._val program = constant(1) + constant(2)
```2. Programs can be executed using the default interpreter:
```scala
import zio._object ZIOCompose extends ZIOAppDefault {
val run = for {
res <- Interpreter.eval(program)
_ <- ZIO.succeed(println(s"Result: ${res}"))
} yield ()
}
```## Lambda
The core data type in ZIO Compose is `Lambda`. It is also type aliased by `~>` (tilde, greater than). A lambda `A ~> B`
represents a serializable unary function that takes in an input of type `A` and produces and output of type `B`. For eg:```scala
val c1: Any ~> Int = Lambda.constant(100)
```The above lambda `c1` is a function that takes in any input and produces an `Int` value.
Another example of lambda is `identity[A]` which like the scala's `identity` function, takes in a type `A` and returns
itself as output. The only difference is that zio-compose's `identity` is serializable.## Serialization
Any lambda from `A ~> B` can be serialized into JSON by performing a few steps.
```scala
// A program that adds two numbers
val program: Any ~> Int = constant(1) + constant(2)// Call the `compile` method to create an execution plan
val compilation: ExecutionPlan = program.compile// call `json` on the execution plan to encode it as JSON
val json: String = compilation.compile.json
```## Conditional
Conditional operations can be implemented on `Lambda`s that return a `Boolean` using the `diverge` operator.
The following program returns `"Yes"` if the condition is true and `"No"` if the condition is false.```scala
import Lambda._val program = (constant(1) > constant(2)).diverge(
isTrue = constant("Yes"),
isFalse = constant("No")
)
```Since `1 < 2` the condition is `false` and the output thus becomes `"no"`.
## Piping
Two lambdas can be composed using the `pipe` or `compose` operator. For eg: if there exists a lambda `l1: A ~> B` and a
lambda `l2: B ~> C` then they can be composed using the pipe operator as —```scala
val l1: A ~> B = ???
val l2: B ~> C = ???
val l12: A ~> C = A >>> B
```This is the semantic equivalent of `l2(l1(a))` , where `a` is of type `A`.
## Lenses
ZIO Compose has support for lenses which allows very precise control over getting and setting values over record types.
For eg: Let's say there is a type `User` and we want to get the `age` of that user. We could do something like this —```scala
import zio.schema._
import compose.macros.DeriveAccessorscase class User(firstName: String, lastName: String, age: Int)
object User {
// Derive the Schema
implicit val schema: Schema[User] = DeriveSchema.gen[User]// Derive accessors
val lens = DeriveAccessors.gen[User]
}
```The `schema` field inside of `User` provides access to the meta-data and structure of the type `User`.
Whereas `lens` internally uses `schema` to navigate through an instance to lookup or update it's fields in a type-safe
manner. Let's see that in action —```scala
val user: Any ~> User = constant(User("John", "Doe", 23))
val age: User ~> Int = User.lens.age.get
val program: Any ~> Int = user >>> age
```Here we create a user using `constant` and then using the derived lens we create a Lambda from `User ~> Int`.
We compose the two lambdas together using the `>>>` operator (alias to `pipe`).
The final program is a type-safe, serializable function that can take anything and produce an integer.Now let's look at an example where we are updating a field using lenses in the User type -
```scala
val user: Any ~> User = constant(User("John", "Doe", 23))
val program: Any ~> User = (user <*> constant(12)) >>> User.lens.age.set
```The `set` methods on lens is a binary function, so it needs two arguments - 1. The whole object which needs to be
updated and 2. the value it needs to set. In our case `age.set` would have a type like this - `(User, Int) ~> User`.
That's why we use the `<*>` operator (alias to `zip`) to combine the two inputs and send it to the `set` function.## Transformations
Transformations from one type to another are easily possible using the lens API, however it can become a bit verbose and
boilerplate sometimes.
ZIO Compose provides a DSL to simplify transformations. Here is an example of converting `User` to `Customer`, we start
by defining the types, schema and it's lens.```scala
case class Customer(name: String, age: Int, allowed: Boolean)object Customer {
implicit val schema = DeriveSchema.gen[Customer]
val lens = DeriveAccessors.gen[Customer]
}
```We then take each field of the user, perform some transformations on the field themselves and then set it in a customer.
A transformation can be defined using the `->>` operator.```scala
val t1: User ~> Customer = (User.lens.age.get + constant(10)) ->> Customer.lens.age.set
,
```A `Transformation`, is nothing but a pair of a getter and setter. We can combine multiple transformations using
the `transform` operator -```scala
import Lambda._val user2Customer: User ~> Customer = transform(
(User.lens.age.get + constant(10)) ->> Customer.lens.age.set,
(User.lens.firstName.get ++ constant(" ") ++ Person.lens.lastName.get) ->> Customer.lens.name.set,
(User.lens.age.get > constant(18)) ->> Customer.lens.isAllowed.set,
)
```The final output of the transformation is a function from `User ~> Customer`. We can then pipe in an actual user
instance to produce a customer as follows —```scala
val program: Any ~> Customer = constant(User("John", "Doe", 20)) >>> user2Customer
```## Looping
With ZIO Compose one can loop over a lambda in multiple ways. For eg:
Let's say I want to add all numbers between 0 to 10. We can do this by creating a type `Sum` which maintains
intermediary state of our program like this —```scala
import compose.macros.DeriveSchemacase class Sum(count: Int, result: Int)
object Sum {
implicit val schema = DeriveSchema.gen[Sum]
val lens = DeriveAccessor.gen[Sum]
}
```Then we make a lambda of type `Sum ~> Sum` to represent one iteration of our loop. In the iteration we perform two
operations -1. Increase the value of `count` by one.
2. Add the value of `count` to `result`.```scala
import Lambda._val iteration: Sum ~> Sum = transform(
Sum.lens.count.get.inc ->> Sum.lens.count.set,
Sum.lens.result.get + Sum.lens.count.get ->> Sum.lens.result.set
)
```We then use the `repeatWhile` operator to keep iterating while the condition is true.
```scala
val sum: Any ~> Sum = iteration.repeatWhile(Sum.lens.count.get < constant(10))
```To get the exact value of the sum we can again use the lens API as follows —
```scala
val program: Any ~> Int = sum >>> Sum.lens.result.get
```## Scopes
Scopes allows us to define and update variables within a given context.
They turn out to be pretty handy when we want to share some data across different part of our program without having to
pass it using `pipe`.
Below we take an arbitrary example where have two numbers and we want to check if their sum is greater than their
product.```scala
import Lambda._val program = scope { implicit ctx =>
val a = Ref.make(key = "a", value = 10)
val b = Ref.make(key = "b", value = 5)
val result = Ref.make(key = "result", value = false)(a.get + b.get) > (a.get * b.get) >>> result.set
}
```A `Ref` is like a `ZRef` with `get` and `set` methods on it.
It needs a unique key within the scope of it's usage and a default value at the time of initialization.
However, it can only be initialized inside a `scope { }` block. The `{implicit ctx =>` provides context in which the
scoped variable is available.## Fibonacci
Here is an advanced example of a program that calculates fibonacci numbers and is completely serializable.
```scala
import compose._
import zio.schema._case class Fib(a: Int, b: Int, i: Int)
object Fib {
implicit val schema: Schema[Fib] = DeriveSchema.gen[Fib]
val lens = DeriveAccessor.gen[Fib]
}def fib = constant(Fib(0, 1, 0)) >>>
transform(
Fib.lens.b.get ->> Fib.lens.a.set,
Fib.lens.a.get + Fib.lens.b.get ->> Fib.lens.b.set,
Fib.lens.i.get.inc ->> Fib.lens.i.set,
).repeatWhile(Fib.lens.i.get =!= constant(20)) >>> Fib.lens.b.get
```**PS:** If you like what you see, give the repo a ⭐️ 🙏