From 6f51ce3fce68892e424604e9333fa08fe9cc5896 Mon Sep 17 00:00:00 2001 From: Flavio Brasil Date: Wed, 13 Sep 2023 23:46:15 -0700 Subject: [PATCH] wip --- README.md | 256 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 255 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2692100a2..b73e17edb 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,261 @@ ![Chat](https://img.shields.io/discord/1087005439859904574) ![Version](https://img.shields.io/maven-central/v/io.getkyo/kyo-core_3) -Sorry, documentation coming soon + +Kyo is a complete toolkit for Scala application development, spanning browser-based apps in ScalaJS to high-performance backends on the JVM. It leverages an algebraic effect system to deliver streamlined APIs that within the pure functional programming paradigm. Unlike similar solutions, Kyo achieves this without inundating developers with esoteric concepts like category theory -- making for a development experience that's as intuitive as it is robust. + +Drawing inspiration from ZIO's effect rotation, Kyo takes a more generalized approach. While ZIO restricts effects to two channels -- dependency injection and short-circuiting -- Kyo allows for an arbitrary number of effectful channels. This enhancement affords developers greater flexibility in effect management, while also simplifying Kyo's internal codebase through more principled design patterns. + +## The `>` type + +In Kyo, computations use the infix type `>`, which takes two parameters: + +1. The first parameter specifies the type of the expected output. +2. The second parameter lists the pending effects that must be addressed, represented as an **unordered** type-level set via a type interssection. + +```scala +// Expect an Int after handling the 'Options' effect +Int > Options + +// Expect a String after handling both 'Options' and 'IOs' effects +String > (Options with IOs) +``` + +> Note: effect types follow a naming convention, which is the plural form of the functionalities they manage. + +Kyo is designed so that any type `T` is automatically a `T > Any`, where `Any` signifies an empty set of pending effects. This design makes it straightforward to handle computations as if they are pure types, when appropriate. + +```scala +// An 'Int' is also an 'Int > Any' +val a: Int > Any = 1 +``` + +It's possible to directly extract the pure value from a computation marked as `T > Any`. The given example essentially signifies a computation that yields an `Int` without any pending effects. Therefore, it's possible to safely extract the value: + +```scala +// Since there are no pending effects, the computation can produce a pure value +val b: Int = a.toPure +``` + +This property eliminates the need to distinguish between `map` and `flatMap`, as values are automatically lifted to a Kyo computation with no pending effects. + +```scala +// Kyo still supports both `map` and `flatMap` +def example1(a: Int > Options, b: Int > Tries): Int > (Options with Tries) = + a.flatMap(v => b.map(_ + v)) + +// But `map` alone suffices due to Kyo's design +def example1(a: Int > Options, b: Int > Tries): Int > (Options with Tries) = + a.map(v => b.map(_ + v)) +``` + +The map method in Kyo has the ability to automatically update the set of pending effects. When you apply map to computations that have different sets of pending effects, Kyo reconciles these into a new computation type that combines all the unique pending effects from both operands. + +## Effect widening + +Kyo's set of pending effects is a contravariant type parameter. This encoding permits computations to be widened to encompass a larger set of effects. The concept of "widening" refers to the capability to expand the set of pending effects that a computation may have. + +```scala +// An 'Int' with an empty effect set (`Any`) +val a: Int > Any = 1 + +// Widening the effect set from empty (`Any`) to include `Options` +val b: Int > Options = a + +// Further widening the effect set to include both `Options` and `Tries` +val c: Int > (Options with Tries) = b + +// Directly widening a pure value to have `Options` and `Tries` +val d: Int > (Options with Tries) = 42 +``` + +This contravariant encoding enables a fluent API for effectful code, allowing methods to accept parameters with a specific set of pending effects while also permitting those with fewer or no effects: + +```scala +// The function expects a parameter with both 'Options' and 'Tries' effects pending +def example1(v: Int > (Options with Tries)) = + v.map(_ + 1) + +// A value with only the 'Tries' effect can be automatically widened to include 'Options' +def example2(v: Int > Tries) = example1(v) + +// A pure value can also be automatically widened +def example3 = example1(42) +``` + +Here, `example1` is designed to accept an `Int > (Options with Tries)`. However, thanks to the contravariant encoding of the type-level set of effects, `example2` and `example3` demonstrate that you can also pass in computations with a smaller set of effects—or even a pure value—and they will be automatically widened to fit the expected type. + +## Using effects + +Kyo offers a modular approach to effect management, accommodating both built-in and custom effects through an organized system of object modules. This organization ensures a consistent API and allows developers to focus on building complex applications without worrying about effect management intricacies. + +Importing the corresponding object module into scope brings in the effect and any additional implicits you may need. The naming convention uses lowercase object modules for each effect type. + +```scala +// for 'Options' effect +import kyo.options._ +// for 'Tries' effect +import kyo.tries._ +``` + +For effects that support it, a get method is provided, which permits the extraction of the underlying value from a computation. + +```scala +// Retrieves an 'Int' tagged with 'Options' +val a: Int > Options = Options.get(Some(1)) +``` + +Effect handling is done using the `run` method. Though it's named run, the method doesn't necessarily execute the computation immediately, as it can also be suspended. + +```scala +// Handles 'Options' effect +val b: Option[Int] > Any = Options.run(a) + +// Retrieves pure value as there are no more pending effects +val c: Option[Int] = b.pure +``` + +The order in which you handle effects in Kyo can significantly influence both the type and value of the result. In Kyo, since effects are unordered at the type level, the runtime behavior depends on the sequence in which effects are processed. + +```scala +def optionsFirst(a: Int > (Options with Tries)): Try[Option[Int]] = { + val b: Option[Int] > Tries = Options.run(a) + val c: Try[Option[Int]] > Any = Tries.run(b) + c.pure +} +def triesFirst(a: Int > (Options with Tries)): Option[Try[Int]] = { + val b: Try[Int] > Options = Tries.run(a) + val c: Option[Try[Int]] > Any = Options.run(b) + c.pure +} +``` + +In this example, the order in which effects are handled significantly influences the outcome, particularly when the effects have the ability to short-circuit the computation: + +```scala +val ex = new Exception + +// if the effects don't short-circuit, only the order of nested effects in the result changes +assert(optionsFirst(Options.get(Some(1))) == Success(Some(1))) +assert(optionsFirst(Tries.get(Success(1))) == Success(Some(1))) + +// note how the result type changes from 'Try[Option[T]]' to 'Option[Try[T]]' +assert(triesFirst(Options.get(Some(1))) == Some(Success(1))) +assert(triesFirst(Tries.get(Success(1))) == Some(Success(1))) + +// if there's short-circuiting, the resulting value can be different +assert(optionsFirst(Options.get(None)) == Success(None)) +assert(optionsFirst(Tries.get(Failure(ex))) == Failure(ex)) + +assert(triesFirst(Options.get(None)) == None) +assert(triesFirst(Tries.get(Failure(ex))) == Some(Failure(ex))) +``` + +## Core Effects + +### Options: optional values + +```scala +// 'get' to "extract" the value of an 'Option' +val a: Int > Options = Options.get(Some(1)) + +// 'apply' is the effectful version of 'Option.apply' +val b: Int > Options = Options(1) + +// if 'apply' receives a 'null', it's equivalent to 'Options.get(None)' +assert(Options.run(Options(null)) == Options.run(Options.get(None))) + +// effectful version of `Option.getOrElse` +val c: Int > Options = Options.getOrElse(None, 42) + +// effectful verion of 'Option.orElse' +val d: Int > Options = Options.getOrElse(b, c) +``` + +### Tries: exception handling + +```scala +// 'get' to "extract" the value of an 'Try' +val a: Int > Tries = Tries.get(Try(1)) + +// 'fail' to short-circuit the computation +val b: Int > Tries = Tries.fail(new Exception) + +// 'fail' has an overload that takes an error message +val c: Int > Tries = Tries.fail("failed") + +// 'apply' is the effectful version of 'Try.apply' +val c: Int > Tries = Tries(1) + +// 'apply' automatically catches exceptions. Equivalent to 'Tries.fail(new Exception)' +val d: Int > Tries = Tries(throw new Exception) +``` + +### Aborts: short circuiting + +Both the `Tries` and `Options` effects are internally implemented in terms of `Aborts`, which is the generic implementation for short-circuiting effects. It's equivalent to ZIO's failure channel. + +```scala +// 'get' allows to "extract" the value from an 'Either' +val a: Int > Aborts[String] = Aborts[String].get(Right(1)) + +// short-circuiting via 'Left' +val b: Int > Aborts[String] = Aborts[String].get(Left("failed!")) + +// short-circuiting via 'Fail' +val c: Int > Aborts[String] = Aborts[String].fail("failed!") + +// 'catching' automatically catches exceptions similarly to 'Tries.apply' +val d: Int > Aborts[Exception] = Aborts[Exception].catching(throw new Exception) +``` + +Note how the `Aborts` effect has a type parameter and its methods can only be accessed if the type parameter is provided. + +### Consoles: interaction with the console + +```scala +// reads a line from the console +val a: Int > Consoles = Consoles.readln + +// prints to the stdout +val b: Int > Consoles = Consoles.print("ok") + +// prints to the stdout with a new line +val c: Unit > Consoles = Consoles.println("ok") + +// prints to the stderr +val d: Unit > Consoles = Consoles.printErr("fail") + +// prints to the stderr with a new line +val e: Unit > Consoles = Consoles.printlnErr("fail") + +// runs with the default implicit 'Console' implementation +val f: Int > IOs = Consoles.run(e) + +// explictily setting the 'Console' implementation +val f: Int > IOs = Consoles.run(Console.default)(e) +``` + +Note how `Consoles.run` returns a computation with the `IOs` effect pending, which ensures the implementation of `Consoles` is pure. + +### Clocks: time management + +```scala +// obtain the current time +val a: Instant > Clocks = Clocks.now + +// run with default 'Clock' +val b: Instant > IOs = Clocks.run(a) + +// run with an explicit 'Clock' +val c: Instant > IOs = Clocks.run(Clock.default)(a) +``` + +### Envs: dependency injection + +```scala +``` + License -------