Skip to content

Instantly share code, notes, and snippets.

@sidnt
Created December 13, 2020 06:39
Show Gist options
  • Save sidnt/83e5f4da360eab8341216e91c3689c17 to your computer and use it in GitHub Desktop.
Save sidnt/83e5f4da360eab8341216e91c3689c17 to your computer and use it in GitHub Desktop.
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