{"id":15984653,"url":"https://github.com/aappddeevv/zio-test","last_synced_at":"2025-04-04T20:46:04.930Z","repository":{"id":97610218,"uuid":"162065309","full_name":"aappddeevv/zio-test","owner":"aappddeevv","description":"Test ZIO abstractions. Does bifunctor IO work better in some cases?","archived":false,"fork":false,"pushed_at":"2018-12-23T23:21:28.000Z","size":27,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-10T05:25:59.285Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aappddeevv.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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":"2018-12-17T02:47:19.000Z","updated_at":"2018-12-23T23:21:29.000Z","dependencies_parsed_at":"2023-03-19T13:09:29.586Z","dependency_job_id":null,"html_url":"https://github.com/aappddeevv/zio-test","commit_stats":{"total_commits":11,"total_committers":1,"mean_commits":11.0,"dds":0.0,"last_synced_commit":"d9d80c42d4111b0cc3f18c8c3d02c2fd06a853ac"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aappddeevv%2Fzio-test","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aappddeevv%2Fzio-test/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aappddeevv%2Fzio-test/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aappddeevv%2Fzio-test/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aappddeevv","download_url":"https://codeload.github.com/aappddeevv/zio-test/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247249602,"owners_count":20908211,"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":[],"created_at":"2024-10-08T02:10:00.065Z","updated_at":"2025-04-04T20:46:04.912Z","avatar_url":"https://github.com/aappddeevv.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ZIO bifunctor testbed\n\nDoes ZIO's bifunctor design offer a better abstraction? And if so, when does\nit work best?\n\nThis repo is a simple, quick testbed to explore a few questions around zio.\n\nAll the code assumes the ZIO IO so there are no `F` parameters in the algebras,\nonly an `E` parameter.\n\nTests are written in scalatest so there really is no useful main.\n\n\n## Test1 - Multi-layer web client.\n\nCreate 3 layers of an HTTP client based on http4s--just the essentials. A client\nis really a Kleisli `HttRequest =\u003e IO[E, HttpResponse]`.\n\nWe want to have each layer declare its own error type and the idea is to blend\nlayers together and have precise error types.\n\n* Backend: A OS/runtime environment specific HTTP client. This is completed\n  faked in this test to return exactly what I want e.g. errors :-)\n* Client: Uses the lowest level layer to provide a friendly API.\n* OData: An OData 4.0 REST client, totally fake as well.\n\nFindings:\n* Without proper sum types, you cannot merge the error types from the different\n  layers upward to the topmost layer if you wanted to, so its a bit more\n  difficult to \"combine\" error types to handle at the top.\n* If you want to stop all exceptions from propagating upward, you must supply a\n  handler that handles a Throwable and converts it to an `E` in that layer. This\n  should be expected but is made explicit give the type signature of IO.\n* It is hard to break the habit instilled by cats that the error channel is\n  built into `IO` already.\n* Explicit `E` forces you to handle the error in the layer above or let it\n  bubble upwards into the next layer based on your explicit choice. It's alot\n  like checked exceptions, which I don't think are bad except that java did not\n  have the language support to make it a useful feature when they introduced it\n  into java. Checked exceptions have a bad reputation for that reason.\n* It's not strictly necessary to parameterize on `E` but if you parameterize on\n  `E`, you need to provide the ability to create an `E` (which is what the\n  `mkError` functions do in each layer).\n* Wow! It's complex to think through the outer layers of your API where you\n  interface into code that throws exceptions for errors. If you don't trap the\n  throws there, the upper layers require even more thought. This is a statement\n  that says reasoning about your code is easier if errors are explicit values.\n* If one layer of software returns Either, trying to be functional, but other\n  layers return Throwable, you can merge these types together through value\n  conversion. The opposite direction conversion is also possible. If you did not\n  have IO[E,A], you would only have the ability to convert to a Throwable sum\n  type.\n* The type signatures are much clearer once the errors have been handled with\n  ZIO. You you are more confident that the effect is not hiding an error\n  somewhere in the effect that is not represented explicitly.\n* Without an explicit E, it is possible to wrap all objects in some exception\n  derived classes although you would need to unwrap to access the\n  data. Essentially, explicit E removes one layer of indirection/wrapping that\n  exists when you have to use Throwable derived classes.\n* The point of bifunctor IO is not get rid of Throwable but to be explicit about\n  the state of the data.\n\n## Test 2\n\n...TBD...\n\n## Test Conclusions\n\nSome conclusions:\n\n* In highly failure prone environments where the IO is likely to fail more\nfrequently than not *and* you want to recover in architecture layers other than\nthe outermost layer, an explicit `E` seems to improve code reasoning.\n  * High failure environments include applications such as web UIs or user\n    oriented applications. Some operational, analytical data pipelines also fall\n    into thas category. Batch oriented data pipelines probably do not fall into\n    this category as often.\n* If you are working on batch oriented programs whose failure model is to\nrecover by restarting the program, the explicit `E` is less important.\n* Errors should be handled at units-of-work (roughly speaking) such as the\n\"record processing level\" or the program level. An explicit `E` helps you manage\nerrors across units-of-work if your program has multiple granularities of\nunits-of-work.\n* dotty's sum types will make ZIO more useful.\n* Given that `E` allows you to explicitly handle errors in a certain layer (and\n  not jump the stack), it forces your code to be referentially transparent when\n  that is important in your architecture. You cannot be referentially\n  transparent if you throw exceptions.\n* ZIO does not preclude you from jumping the stack if you want to still handle\n  exceptions the traditional way so don't lose this type of flexbility.\n* Bifunctor IO is not about getting rid of Throwable and replacing it with an\n  explicit E, although you can do that. Bifunctor IO is really about being\n  explicit about the possible \"error\" that an effect could have including the\n  ability to declare that an effect cannot contain an error.\n\nTo me, potentially the most important conclusion is that a type parameter `E` in\n`IO` gives you an option of handling errors differently. Generally, more choices\nare good assuming the increase in complexity is not large. The tests suggest\nthat the complexity is moderate and can become learned behavior fairly quickly.\n\nOn a separate note, integrating ZIO into a cats-core based application appears\nto be difficult.\n\n## Declaring Error Intent with Types\nIt's clear that an API can lie. You could declare a `IO[E,A]` with an\n`E=Nothing` but still have that IO throw something you were not expecting that\ncause the IO to fail in an \"unchecked way.\" Because the failure was unexpected\naccording to the type, its a defect that the programmer must resolve. It's not a\nrecoverable error.\n\nSuppose you declare your IO to be `IO[Nothing,A]`. If there is a throw that you\ndid not handle as a programmer then the run time system for ZIO will have an\nexit status of \"failed\" with a `Cause` of `Unchecked`. If we had handed the\nerror inside our code and translated that to a `A` our type is good and we can\nreturn that as an ExitStatus of `Succeeded`. If the IO fails with another error\n`E` (remember this E is not in our `IO[Nothing,A]`), we may *want* to \"fail\" the\nIO with a `Cause` of `Checked` `E`. But since the IO type just mentioned is\n`IO[Nothing,A]`, we can't return an `Checked[E]`.\n\nSo *if* your `E=Nothing` the only `ExitStatus[Nothing,A]` you can create is a\n`Unchecked` error, which has type `Cause[Nothing]`, or an `Interruption`\nconcept, which is also of type `Cause[Nothing]`.\n\nWhen we say \"return a value\" in the asynchronous case, we use the runtime system\nmethod `unsafeRunAsync` to run the IO:\n\n```scala\ntrait RTS { \n  def unsafeRunAsync[E, A](io: IO[E, A])(k: (ExitResult[E, A]) ⇒ Unit): Unit\n}\n```\n\nIf the IO we are running is `IO[Nothing,A]` then our \"callback\" can only be a\n`ExitResult[Nothing,A] =\u003e Unit`. The presence of `Nothing` therefore indicates\nthat the only way the IO can fail is by the IO being interrupted or an unchecked\nexception is thrown.\n\nIn contrast, if we just had a cats `IO[A]`. We know that this IO can have an\nexception in it. But its quite possible that the IO has be constructed so that\nno Throwable will occur or that any Throwable is translated into an `A`. Even\n*if* the error handling had provided via an `IO.recover` the signature would\nstill be `IO[A]`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faappddeevv%2Fzio-test","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faappddeevv%2Fzio-test","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faappddeevv%2Fzio-test/lists"}