Closed jdegoes closed 1 year ago
Perhaps I'm short-sighted, but I can hardly think of an useful non-side-effectful ZIO function. As you mentioned, in practice side-effects are embedded everywhere. For instance, ZIO.succeed
is normally accompanied by a side-effectful IO in another branch of the same block of code:
val result: ZIO[SideEffect, E, A] = if(condition) ZIO.succeed(value) else doSomething()
Another concern is type aliases, will UIO continue to require Any
as environment or SideEffect
instead?
val a: ZIO[SideEffect, Nothing, Unit] = UIO.effectTotal(println("Hello"))
val b: ZIO[Any, Nothing, Unit] = UIO.succeed(())
In my opinion, to add three (maybe more?) new type aliases to solve this problem is a terrible idea.
I think if this will get implemented, it will influence the architecture of ZIO apps in a good way. SideEffect
will nudge programmers to capture side effects (via ZIO.effectTotal
& co.) only in the lowest layer(s) of the app. I think this is good from architectural point of view, and will also help people who come new to an already existing ZIO app to comprehend the code base.
EDIT: to expand on my point above, this is not about dividing ZIO functions into side-effectful and non-side-effectful, like @luis3m suggests, but about where can capturing side-effects (methods like effectTotal
, ...) hide.
After some time thinking about this, I think I finally understand what @sideeffffect is suggesting. The idea is to track standalone side-effects with this proposed new SideEffect
service which will motive people to move these side-effects to services.
Move from:
object App {
val findPermissions: ZIO[CacheService with SideEffect, DBException, Permissions] =
for {
id <- cacheService.getLastId
user <- ZIO.effect(getUserFromDB(id))
perms = user.permissions
} yield perms
}
To write this:
object userRepository {
type UserRepository = Has[UserRepository.Service]
object UserRepository {
trait Service { def findUserById(userId: UserId): IO[DBException, User] }
}
def findUserById(userId: UserId): ZIO[UserRepository, DBException, User] =
ZIO.accessM[UserRepository](_.get.findUserById(userId))
}
object App {
val findPermissions: ZIO[CacheService with UserRepository, DBException, Permissions] =
for {
id <- cacheService.getLastId
user <- userRepository.findUserById(id)
perms = user.permissions
} yield perms
}
If that's what you are suggesting then I also think that will influence the architecture of ZIO apps in a good way. Nonetheless, I'm wondering how would you define an implementation for UserRepository.Service
without resorting to ZIO.effect
and others? By using any of these the method signature will change to ZIO[CacheService with UserRepository with SideEffect, DBException, Permissions]
.
Am I correct @sideeffffect?
@luis3m
No, If you create an instance of CacheService
and supply it with a SideEffect
instance such that CacheService
uses it but doesn't expose it to clients, then there won't be a SideEffect
dependency on clients. I've wrote about this pattern a bit in the second section of izumi zio.Has-support docs (err, nothing self-contained to link to). Basically any service with dependencies on the methods:
trait Service[R] {
def x: URIO[R, X]
}
Can be turned into a service that requires no dependencies, by wrapping each method in .provide
:
def supply(myDep: MyDep)(serviceMyDep: Service[MyDep]): Service[Any] = new Service[Any] {
def x: URIO[Any, X] = serviceMyDep.x.provide(myDep)
}
This allows you turn unprincipled Reader-based apps that spill all their innards at every corner into principled layered apps that allow you to hide lower-level dependencies (such as SideEffect) with high-level services (such as CacheService)
@neko-kai like this, I suppose:
object userRepository {
type UserRepository = Has[UserRepository.Service]
object UserRepository {
trait Service {
def findUserById(userId: UserId): IO[DBException, User]
}
object Service {
def live(sideEffect: SideEffect.Service): Service = new Service {
def findUserById(userId: UserId): IO[DBException, User] =
sideEffect.effect(getUserFromDB(userId))
}
}
val any: ZLayer[UserRepository, Nothing, UserRepository] =
ZLayer.requires[UserRepository]
val live: ZLayer[SideEffect, Nothing, UserRepository] =
ZLayer.fromService[SideEffect.Service, UserRepository.Service](Service.live(_))
}
def findUserById(userId: UserId): ZIO[UserRepository, DBException, User] =
ZIO.accessM[UserRepository](_.get.findUserById(userId))
}
object App {
val findPermissions: ZIO[CacheService with UserRepository, DBException, Permissions] =
for {
id <- cacheService.getLastId
user <- userRepository.findUserById(id)
perms = user.permissions
} yield perms
}
Right?
@luis3m yep, I think we're on the same page :smile:
Btw here's the PR where I attempted to implement this https://github.com/zio/zio/pull/1564
Was there any follow-up on this idea now that ZIO 2 is out and stable? Considering how the whole Environment handling has been simplified — e.g. no more Has, things like Scope or Blocking — would this proposal be reconsidered?
I don't think this makes sense in ZIO. We have implemented the Unsafe
interface as an alternative, more flexible way of controlling the usage of code that is potentially unsafe, we have a variety of constructors such as ZIO.succeed
that allow importing side effecting code into ZIO, and if anything we have reduced the need for users to make this distinction by making all effect constructors lazy.
This could certainly be implemented in user land by creating services representing the desired capabilities and using these services instead of ordinary ZIO constructors, but I think the value of this for side effects in general as opposed to specific capabilities such as needing a cache service or needing a scope is limited.
This issue is more a thought experiment than anything else. But I think it's useful to jot this down somewhere since I did work on this concept last year, at the time when we introduced ZIO Environment, and others have since opened tickets and pull requests (@sideeffffect).
The core idea: ZIO Environment has grown powerful enough that it's possible to easily track side-effects in the type system, without effect polymorphism.
How?
Imagine defining a
SideEffect
service that is capable of converting side-effects into functional effects:Given such a service—which is
sealed
and cannot be implemented by code outside ZIO—it would then be possible to retrofit all existing (side-effect) constructors to use the service:Now any introduction of a side-effect now forces a dependency on the
SideEffect
service.Then, in turn, the only method that could provide an implementation of such a service would be the
unsafeRun*
family of methods available on aRuntime
:This could not really be done before because of the inability to do partial provision on environments of unknown shape, but that's been rectified with
Has
/ZLayer
.Cheating: The possible ways of "cheating" are identical to and already exist in tagless-final: mainly by creating and passing along new interfaces (which are implemented by
SideEffect
but called something else), or just accidentally or intentionally embedding side-effects in code that's supposed to be pure.Pros
Cons