-
-
Save sidnt/83e5f4da360eab8341216e91c3689c17 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import zio._ | |
object helloR extends App { | |
/** what is R | |
* so R is the environment required to make the effect runnable | |
* with the help of R type parameter to ZIO, we "declare" a dependency on R | |
* iow, that zio, might use references to R, inside its implementation | |
* we don't know from the outside, if it does / will do. | |
* the implementation might as well throw the r:R | |
* how it uses the reference, is upto the implementation | |
* . | |
* but, suffices to say, that at the very least, we have declared that | |
* this zio depends on R, and it will not run unless it is provided an r:R | |
* so to make it runnable, we will have to provide an instance r:R | |
* . | |
* now given that a dependency on R is declared, | |
* how to we access it in the implementation? | |
* for that, we have the access/M method | |
*/ | |
def makeMessage(i:Int):String = "the int> " + i | |
/** we declare in z1 at TL | |
* that it depends on Int and produces a String */ | |
val z1: URIO[Int,String] = for { | |
i <- ZIO.access[Int](identity) | |
m = makeMessage(i) | |
} yield m | |
val z1b = for { | |
i <- ZIO.environment[Int] | |
m = makeMessage(i) | |
} yield m | |
val z2 = for { | |
m: String <- z1 | |
_ <- UIO(println(m)) | |
} yield () | |
val z2b = z2.provide(99) | |
// val z2b: URIO[Int with Console,Unit] = for { | |
// m <- z1 | |
// _ <- UIO(println(m)) | |
// } yield () | |
val z3 = z2b.exitCode | |
// def run(args: List[String]) = z3 | |
/** we can declare a dependency on a product type as well | |
* eg a case class | |
*/ | |
case class User(name:String,age:Int) | |
val u1 = User("Chet", 42) | |
def makeMessage(u:User):String = "user name is: " + u.name + " with age: " + u.age | |
val bz1 = for { | |
u <- ZIO.access[User](identity) | |
m = makeMessage(u) | |
} yield m | |
/* notice how the type infers in bz2 */ | |
val bz2 = for { | |
m <- bz1 | |
_ <- UIO(println(m)) | |
} yield () | |
val bz3 = bz2.provide(u1).exitCode | |
// def run(args: List[String]) = bz3 | |
/** but in reality, it's not that simple | |
* let's introduce another product type | |
* and try to depend on it | |
*/ | |
case class Country(name:String, foundedIn: Short, population:Long) | |
val c1 = Country("India", 1947, 13500000000L) | |
def generateMessageWith(u:User,c:Country):String = "user name is: " + u.name + " with age: " + u.age + "\nlives in: " + c | |
val cz1:URIO[User with Country, String] = for { | |
u <- ZIO.access[User](identity) | |
c <- ZIO.access[Country](identity) | |
m = generateMessageWith(u,c) | |
} yield m | |
val cz2: ZIO[User with Country, Nothing, Unit] = for { | |
m <- cz1 | |
_ <- UIO(println(m)) | |
} yield () | |
// val uwc: User with Country = ??? | |
// val cz3 = cz2.provide(uwc) | |
// def run(args: List[String]) = cz3 | |
/** looks like it is difficult to instantiate a | |
* uwc: User with Country | |
* . | |
* to solve this, we use the type `Has[A]` | |
*/ | |
val hu1: Has[User] = Has(u1) //introduction of has | |
/* we can get the instance back from the Has instance: */ | |
val gu1: User = hu1.get //elimination of has | |
val hc1: Has[Country] = Has(c1) | |
/* horizontal composition of Has instances */ | |
val hu1wc1: Has[User] with Has[Country] = hu1 ++ hc1 | |
val gbhu = hu1wc1.get[User] | |
/** but how do we use hu1wc1 in a ZIO's R? */ | |
// URIO[Has[User] with Has[Country], String] | |
val dz1 = for { | |
u1 <- ZIO.access[Has[User]](hu => hu.get) | |
c1 <- ZIO.access[Has[Country]](hc => hc.get) | |
m = "using has: " + generateMessageWith(u1,c1) | |
} yield m | |
val dz2 = for { | |
m <- dz1 | |
_ <- UIO(println(m)) | |
} yield () | |
val dz3 = dz2.provide(hu1wc1).exitCode | |
// def run(args: List[String]) = dz3 | |
/** great | |
* . | |
* but in the above last example, | |
* both the components in the environment | |
* were independent of each other | |
* and present quite orthogonally to each other | |
* in the environment | |
* . | |
* this might not always be the case | |
* eg, what if we deem it sensible | |
* to want to design our app | |
* such that, it needs an environment E, but | |
* make the construction of an e:E, in turn, | |
* itself depends on a value of another type? e0:E0 | |
* and let's say even the construction of e0 is effectful? | |
* . | |
* a large part of app bootup | |
* is just about instantiating its dependencies in the first place | |
* before an app can start delivering its domain logic/services | |
* it needs that its dependencies be up and running | |
* . | |
* sure this dependency bootup can be modeled as z:ZIO[..] | |
* where we can even use fork join etc to do this in parallel | |
* d0 <- TaskR[Any,D1] | |
* d1 <- TaskR[D1,D2] | |
* d2 <- TaskR[D2,D3] | |
* ... yield finalCompleteEnvironment | |
* and then provide(finalCompleteEnvironment) to run the app finally | |
* . | |
* but maybe the handy ZLayer datatype is just for this | |
* ZLayer looks like it is essentially a zio eventually | |
*/ | |
val zl1: ZLayer[Int,Nothing,String] = ZLayer.fromFunctionMany[Int,String](makeMessage) | |
/** here is an instantiated layer that takes an Int and produces a String | |
* from a function applied on that int to get the string | |
* even though there's no actual int present in the scene here | |
* just the wiring is present, through the supplied makeMessage fx | |
* . | |
* how do we provide it the int, and | |
* how do we use the string it produces | |
* . | |
* apparently there's no provide on ZLayer | |
* and no flatMap even :( | |
* . | |
* looks like with, >>> we can feed a Zlayer to another | |
* but what type of ZLayer exactly? | |
* at least the edge types should fit? | |
* if an intLayer produces an Int in ROut, we should be able to feed it to | |
* a layer requiring an Int in RIn, like zl1 here | |
* . | |
*/ | |
/** note the type of intLayer1/2 | |
* but what constructor of ZLayer we use | |
* to satisfy the type constraints? | |
*/ | |
// val intLayer2: ZLayer[Any,Nothing,Int] = ??? | |
val intLayer1: ZLayer[Any,Nothing,Int] = ZLayer(Managed.fromEffect(UIO(171))) | |
val zl2:ZLayer[Any,Nothing,String] = intLayer1 >>> zl1 | |
/** zl2 doesn't require anything | |
* but produces String as ROut | |
* how do we use that string in a ZIO? | |
*/ | |
val fz1:URIO[String,Unit] = for { | |
m <- ZIO.access[String](identity) | |
_ <- UIO(println(m)) | |
} yield () | |
val fz2 = fz1.provideLayer(zl2) | |
val fz3 = fz2.exitCode | |
// def run(args: List[String]) = fz3 | |
/** there is some reasoning to be done, | |
* to arrive at using Has and ZLayer together | |
* even though they can be used independently | |
* . | |
* we are skipping that here | |
* we just take the word here that | |
* using ZLayer with Has[A] is the best way | |
* for not only declaring dependencies | |
* but also mixing/matching them around easily | |
* . | |
* let's look at an example | |
*/ | |
val layer1HasInt = ZLayer.succeed(199) | |
val ezl1: ZLayer[Has[Int],Nothing,Has[String]] = ZLayer.fromService(makeMessage) | |
val ezl2: ZLayer[Any,Nothing, Has[String]] = layer1HasInt >>> ezl1 | |
// val ezl3: ZLayer[Int,Nothing,Has[String]] = ZLayer.fromFunction[Int,String](makeMessage) | |
// val ezl4: ZLayer[Any,Nothing, Has[String]] = intLayer1 >>> ezl3 | |
val accessStringFromHasStringInR = ZIO.access[Has[String]](_.get) | |
val ez1:ZIO[Has[String],Nothing,Unit] = for { | |
m <- accessStringFromHasStringInR | |
_ <- UIO(println(m)) | |
} yield () | |
val ez2 = ez1.provideLayer(ezl2) | |
// val ez2 = ez1.provideLayer(ezl4) // also works | |
val ez3 = ez2.exitCode | |
def run(args: List[String]) = ez3 | |
// def fz1:ZIO[Has[User] with Has[Country], Nothing, Unit] = ??? | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment