{"id":20485218,"url":"https://github.com/tailcallhq/zio-compose","last_synced_at":"2025-04-13T14:53:02.705Z","repository":{"id":58962955,"uuid":"514817670","full_name":"tailcallhq/zio-compose","owner":"tailcallhq","description":"A Scala DSL to write type-safe programs for distributed computing","archived":false,"fork":false,"pushed_at":"2025-04-05T06:44:34.000Z","size":859,"stargazers_count":35,"open_issues_count":25,"forks_count":3,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-10T14:15:15.547Z","etag":null,"topics":["distributed-computing","functional-programming","scala"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tailcallhq.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-07-17T10:56:26.000Z","updated_at":"2025-01-30T18:18:03.000Z","dependencies_parsed_at":"2023-02-17T00:30:44.445Z","dependency_job_id":"de3ff476-17d7-4e8e-87ca-9a0f20cfef55","html_url":"https://github.com/tailcallhq/zio-compose","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tailcallhq%2Fzio-compose","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tailcallhq%2Fzio-compose/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tailcallhq%2Fzio-compose/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tailcallhq%2Fzio-compose/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tailcallhq","download_url":"https://codeload.github.com/tailcallhq/zio-compose/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248732509,"owners_count":21152851,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["distributed-computing","functional-programming","scala"],"created_at":"2024-11-15T16:28:31.309Z","updated_at":"2025-04-13T14:53:02.675Z","avatar_url":"https://github.com/tailcallhq.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"![badge-workflow]\n![badge-sonatype-releases]\n![badge-sonatype-snapshots]\n\n[badge-workflow]: https://github.com/tusharmath/graphql-compose/actions/workflows/ci.yml/badge.svg\n[badge-sonatype-releases]: https://img.shields.io/nexus/r/https/oss.sonatype.org/com.tusharmath/zio-compose_2.13.svg \"Sonatype Releases\"\n[badge-sonatype-snapshots]: https://img.shields.io/nexus/s/https/oss.sonatype.org/com.tusharmath/zio-compose_2.13.svg \"Sonatype Snapshots\"\n\nZIO Compose is a library that helps you write programs that can be serialized and sent over the wire.\n\n## Introduction\n\nThe basic idea behind having serializable programs is if code and data are on different machines, one of them **must be\nmoved** to the other before the code can be executed on the data.\nTypically, in big-data applications it's much more efficient to move code than the other way around.\n\nThere are other use-cases that don't involve big-data where you would want a serializable program. For eg: Building a\nrule engine,\nwhere the rules are implemented using a DSL and the DSL is serialized and sent to the server for execution.\n\nZIO Compose intends to take care of such use cases. It intends to provide a complete DSL to write any kind of\ndistributed computation using Scala in a type-safe manner. It's built on top of [ZIO Schema].\n\n[zio schema]: https://github.com/zio/zio-schema/pulls\n\n## Installation\n\nUpdate your resolvers and add `zio-compose` as a dependency in your build.sbt.\n\n```scala\nlibraryDependencies += \"com.tusharmath\" %% \"zio-compose\" % version\n```\n\n# Getting started\n\n- [Getting started](#getting-started)\n  - [Lambda](#lambda)\n  - [Serialization](#serialization)\n  - [Conditional](#conditional)\n  - [Piping](#piping)\n  - [Lenses](#lenses)\n  - [Transformations](#transformations)\n  - [Looping](#looping)\n  - [Scopes](#scopes)\n  - [Fibonacci](#fibonacci)\n\n1. Here is a simple program that adds two numbers -\n\n   ```scala\n   import compose.Lambda._\n\n   val program = constant(1) + constant(2)\n   ```\n\n2. Programs can be executed using the default interpreter:\n\n   ```scala\n   import zio._\n\n   object ZIOCompose extends ZIOAppDefault {\n     val run = for {\n       res \u003c- Interpreter.eval(program)\n       _   \u003c- ZIO.succeed(println(s\"Result: ${res}\"))\n     } yield ()\n   }\n   ```\n\n## Lambda\n\nThe core data type in ZIO Compose is `Lambda`. It is also type aliased by `~\u003e` (tilde, greater than). A lambda `A ~\u003e B`\nrepresents a serializable unary function that takes in an input of type `A` and produces and output of type `B`. For eg:\n\n```scala\nval c1: Any ~\u003e Int = Lambda.constant(100)\n```\n\nThe above lambda `c1` is a function that takes in any input and produces an `Int` value.\n\nAnother example of lambda is `identity[A]` which like the scala's `identity` function, takes in a type `A` and returns\nitself as output. The only difference is that zio-compose's `identity` is serializable.\n\n## Serialization\n\nAny lambda from `A ~\u003e B` can be serialized into JSON by performing a few steps.\n\n```scala\n// A program that adds two numbers\nval program: Any ~\u003e Int = constant(1) + constant(2)\n\n// Call the `compile` method to create an execution plan\nval compilation: ExecutionPlan = program.compile\n\n// call `json` on the execution plan to encode it as JSON\nval json: String = compilation.compile.json\n```\n\n## Conditional\n\nConditional operations can be implemented on `Lambda`s that return a `Boolean` using the `diverge` operator.\nThe following program returns `\"Yes\"` if the condition is true and `\"No\"` if the condition is false.\n\n```scala\nimport Lambda._\n\nval program = (constant(1) \u003e constant(2)).diverge(\n  isTrue = constant(\"Yes\"),\n  isFalse = constant(\"No\")\n)\n```\n\nSince `1 \u003c 2` the condition is `false` and the output thus becomes `\"no\"`.\n\n## Piping\n\nTwo lambdas can be composed using the `pipe` or `compose` operator. For eg: if there exists a lambda `l1: A ~\u003e B` and a\nlambda `l2: B ~\u003e C` then they can be composed using the pipe operator as —\n\n```scala\nval l1: A ~\u003e B = ???\nval l2: B ~\u003e C = ???\nval l12: A ~\u003e C = A \u003e\u003e\u003e B\n```\n\nThis is the semantic equivalent of `l2(l1(a))` , where `a` is of type `A`.\n\n## Lenses\n\nZIO Compose has support for lenses which allows very precise control over getting and setting values over record types.\nFor 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 —\n\n```scala\nimport zio.schema._\nimport compose.macros.DeriveAccessors\n\ncase class User(firstName: String, lastName: String, age: Int)\n\nobject User {\n\n  // Derive the Schema\n  implicit val schema: Schema[User] = DeriveSchema.gen[User]\n\n  // Derive accessors\n  val lens = DeriveAccessors.gen[User]\n}\n```\n\nThe `schema` field inside of `User` provides access to the meta-data and structure of the type `User`.\nWhereas `lens` internally uses `schema` to navigate through an instance to lookup or update it's fields in a type-safe\nmanner. Let's see that in action —\n\n```scala\nval user: Any ~\u003e User = constant(User(\"John\", \"Doe\", 23))\nval age: User ~\u003e Int = User.lens.age.get\nval program: Any ~\u003e Int = user \u003e\u003e\u003e age\n```\n\nHere we create a user using `constant` and then using the derived lens we create a Lambda from `User ~\u003e Int`.\nWe compose the two lambdas together using the `\u003e\u003e\u003e` operator (alias to `pipe`).\nThe final program is a type-safe, serializable function that can take anything and produce an integer.\n\nNow let's look at an example where we are updating a field using lenses in the User type -\n\n```scala\nval user: Any ~\u003e User = constant(User(\"John\", \"Doe\", 23))\nval program: Any ~\u003e User = (user \u003c*\u003e constant(12)) \u003e\u003e\u003e User.lens.age.set\n```\n\nThe `set` methods on lens is a binary function, so it needs two arguments - 1. The whole object which needs to be\nupdated and 2. the value it needs to set. In our case `age.set` would have a type like this - `(User, Int) ~\u003e User`.\nThat's why we use the `\u003c*\u003e` operator (alias to `zip`) to combine the two inputs and send it to the `set` function.\n\n## Transformations\n\nTransformations from one type to another are easily possible using the lens API, however it can become a bit verbose and\nboilerplate sometimes.\nZIO Compose provides a DSL to simplify transformations. Here is an example of converting `User` to `Customer`, we start\nby defining the types, schema and it's lens.\n\n```scala\ncase class Customer(name: String, age: Int, allowed: Boolean)\n\nobject Customer {\n  implicit val schema = DeriveSchema.gen[Customer]\n  val lens = DeriveAccessors.gen[Customer]\n}\n```\n\nWe then take each field of the user, perform some transformations on the field themselves and then set it in a customer.\nA transformation can be defined using the `-\u003e\u003e` operator.\n\n```scala\nval t1: User ~\u003e Customer = (User.lens.age.get + constant(10)) -\u003e\u003e Customer.lens.age.set\n,\n```\n\nA `Transformation`, is nothing but a pair of a getter and setter. We can combine multiple transformations using\nthe `transform` operator -\n\n```scala\nimport Lambda._\n\nval user2Customer: User ~\u003e Customer = transform(\n  (User.lens.age.get + constant(10)) -\u003e\u003e Customer.lens.age.set,\n  (User.lens.firstName.get ++ constant(\" \") ++ Person.lens.lastName.get) -\u003e\u003e Customer.lens.name.set,\n  (User.lens.age.get \u003e constant(18)) -\u003e\u003e Customer.lens.isAllowed.set,\n)\n```\n\nThe final output of the transformation is a function from `User ~\u003e Customer`. We can then pipe in an actual user\ninstance to produce a customer as follows —\n\n```scala\nval program: Any ~\u003e Customer = constant(User(\"John\", \"Doe\", 20)) \u003e\u003e\u003e user2Customer\n```\n\n## Looping\n\nWith ZIO Compose one can loop over a lambda in multiple ways. For eg:\nLet's say I want to add all numbers between 0 to 10. We can do this by creating a type `Sum` which maintains\nintermediary state of our program like this —\n\n```scala\nimport compose.macros.DeriveSchema\n\ncase class Sum(count: Int, result: Int)\n\nobject Sum {\n  implicit val schema = DeriveSchema.gen[Sum]\n  val lens = DeriveAccessor.gen[Sum]\n}\n```\n\nThen we make a lambda of type `Sum ~\u003e Sum` to represent one iteration of our loop. In the iteration we perform two\noperations -\n\n1. Increase the value of `count` by one.\n2. Add the value of `count` to `result`.\n\n```scala\nimport Lambda._\n\nval iteration: Sum ~\u003e Sum = transform(\n  Sum.lens.count.get.inc -\u003e\u003e Sum.lens.count.set,\n  Sum.lens.result.get + Sum.lens.count.get -\u003e\u003e Sum.lens.result.set\n)\n```\n\nWe then use the `repeatWhile` operator to keep iterating while the condition is true.\n\n```scala\nval sum: Any ~\u003e Sum = iteration.repeatWhile(Sum.lens.count.get \u003c constant(10))\n```\n\nTo get the exact value of the sum we can again use the lens API as follows —\n\n```scala\nval program: Any ~\u003e Int = sum \u003e\u003e\u003e Sum.lens.result.get\n```\n\n## Scopes\n\nScopes allows us to define and update variables within a given context.\nThey turn out to be pretty handy when we want to share some data across different part of our program without having to\npass it using `pipe`.\nBelow we take an arbitrary example where have two numbers and we want to check if their sum is greater than their\nproduct.\n\n```scala\nimport Lambda._\n\nval program = scope { implicit ctx =\u003e\n  val a = Ref.make(key = \"a\", value = 10)\n  val b = Ref.make(key = \"b\", value = 5)\n  val result = Ref.make(key = \"result\", value = false)\n\n  (a.get + b.get) \u003e (a.get * b.get) \u003e\u003e\u003e result.set\n}\n```\n\nA `Ref` is like a `ZRef` with `get` and `set` methods on it.\nIt needs a unique key within the scope of it's usage and a default value at the time of initialization.\nHowever, it can only be initialized inside a `scope { }` block. The `{implicit ctx =\u003e` provides context in which the\nscoped variable is available.\n\n## Fibonacci\n\nHere is an advanced example of a program that calculates fibonacci numbers and is completely serializable.\n\n```scala\nimport compose._\nimport zio.schema._\n\ncase class Fib(a: Int, b: Int, i: Int)\n\nobject Fib {\n  implicit val schema: Schema[Fib] = DeriveSchema.gen[Fib]\n  val lens = DeriveAccessor.gen[Fib]\n}\n\ndef fib = constant(Fib(0, 1, 0)) \u003e\u003e\u003e\n  transform(\n    Fib.lens.b.get -\u003e\u003e Fib.lens.a.set,\n    Fib.lens.a.get + Fib.lens.b.get -\u003e\u003e Fib.lens.b.set,\n    Fib.lens.i.get.inc -\u003e\u003e Fib.lens.i.set,\n  ).repeatWhile(Fib.lens.i.get =!= constant(20)) \u003e\u003e\u003e Fib.lens.b.get\n```\n\n**PS:** If you like what you see, give the repo a ⭐️ 🙏\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftailcallhq%2Fzio-compose","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftailcallhq%2Fzio-compose","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftailcallhq%2Fzio-compose/lists"}