New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make testing IO programs easier #263
Comments
I don't see what an |
@edmundnoble A user should be able to write code over |
Can you be specific about what would be tested exactly about code running in |
So, basically, Note that one doesn't have to stoop down to using Java libraries for test doubles – we've found it pretty easy to test IO with distage And tagless final situation isn't so bad, quantified implicits can lift nearly all the existing tagless final machinery for |
@edmundnoble A different interpreter doesn't help, because people are using @Kaishh The module approach is interesting. I'll work up an alternative soon. Quantified implicits looks interesting, too, and could make a nice addition to |
@jdegoes could you give us a hint what do you dislike in our approach? At the moment we have several services in production and we use type-level code with cats/zio. And things are going well. To get some impressions regarding our tech you may check a demo repo: https://github.com/pshirshov/izumi-workshop-01 Also there is an interesting example which I wrote in 10 minutes during my workshop: https://github.com/pshirshov/izumi-workshop-01/blob/live-session/app/launcher/src/test/scala/com/github/pshirshov/izumi/workshop/w01/ComplexTest.scala#L91 , note that (1) test suites may be polymorphic on the monad, (2) that example uses dynamic module resolution (aka plugins) but there is an option to keep everything static. Also you may find some slides here (there is some observation of the DI and our answer to microservice/monolith problems): https://github.com/7mind/slides/blob/master/02-roles/target/roles.pdf To be very short our approach is based on:
This allows us to
So we have a huge performance boost. (GC makes an incredible contribution, DI with GC is as cheaper for engineer than typical one as managed memory cheaper than unmanaged.) |
@pshirshov I actually quite like it. This ticket is to essentially create BifunctorIO, which you had to create and maintain on your own because it did not exist in ZIO. I think something like this will end up in most applications of DI or testing, so it may as well live inside ZIO. The particular driving motivation behind this ticket for me is to make it easy to create polymorphic code that, in production, interacts with external, effectful systems; and in unit tests, simulates interaction with external systems. DI can be used to solve part of this problem (namely, the wiring part; and many others besides, of course); but currently the polymorphic part of the problem requires the user create their own abstraction layer around In discussions with @edmundnoble, it occurs to me there's another way to solve the problem that does not require polymorphic machinery. Let's say we have: def getPage(url: URL): IO[HttpError, Html] We have code that uses this to implement a web crawler: def crawl(roots: Set[URL], ...): IO[Exception, Unit] We want to test trait HttpClient[F[_, _]] {
def getPage(url: URL): F[HttpError, Html]
}
def crawl[F[_, _]: HttpClient](roots: Set[URL], ...): F[Exception, Unit] This is not a sufficient constraint for trait HttpClient[F[_, _]] {
def getPage(url: URL): F[HttpError, Html]
}
def crawl[F[_, _]: HttpClient: BifunctorIO](roots: Set[URL], ...): F[Exception, Unit] Now we have a polymorphic method that can be used in production and can be tested. There remains a few problems: first off, the polymorphic machinery is not small and complicates explaining this to a user; second, it's not very easy to write a test The question is, do we need to go final tagless in order to achieve these benefits? What if instead we just changed trait HttpClient {
def getPage(url: URL): IO[HttpError, Html]
} Now def crawl(client: HttpClient, roots: Set[URL], ...): IO[Exception, Unit] The advantage is that we don't need any polymorphic machinery, nor do we need what are essentially lawless type classes; the disadvantage is that due to lack of polymorphism, The approach could probably be improved. def crawl(client: HttpClient, clock: Clock, roots: Set[URL], ...): IO[Exception, Unit] One could improve composition by introducing modules: trait HttpService {
def httpClient: HttpClient
}
trait ClockService {
def clock: Clock
} Now services compose using intersection types, so one can write: def crawl(services: HttpService with ClockService, roots: Set[URL], ...): IO[Exception, Unit] This is probably very easy to teach to beginners when they run into the question, "How can I test some ZIO code I wrote without actually interacting with the real world?" It doesn't require any third-party libraries or any machinery, just basically convention and discipline. The question is whether or not that's a good thing to do, or whether it's better to introduce A final option I've considered is modularizing class MyProgram(ioM: IOModule) {
import ioM._
val io = IO.point("Hello world")
} In this world, Running an The module way of programming is very bulky and cumbersome and not really idiomatic FP Scala. On the other hand, it lets you reuse a lot of machinery (you could then roll your own IO monads with any behavior you want, cheaply and without much ceremony). Overall, all things considered, and having seen the existence of |
You don't need to use final tagless with type classes. Just pass around an Furthermore I have still not seen any code that requires IO to be mocked for testing. @alexknvl and I have talked about one situation where this would be useful: to test that concurrency is working properly, we could interpret an |
@jdegoes aha, I see. I just wish to say that these are bit different matters. Our DI has no contradictions with yours and these approaches would greatly work together (as well our DI works nice with final tagless) Would you like a chat about your needs? We would be happy to contribute our technology into scalaz. This would benefit us both. Scalaz folks would get best-in-the-world DI technology based on an interesting theory (in fact DIStage was just a first practical illustration of the theory behind). We would get recognition - which is very important for us - we are a small company (very small one) struggling for adoption of our tech 😄 |
Sure, that still requires a type class for
Whether you want to call it "mocked" or not is beside the point, though: the main point is that |
That is just a lot of non-sequiturs. You don't need a type class for Also, @pshirshov, I doubt that we need or would benefit from a DI library in Scalaz. |
@edmundnoble I don't care about "mocking IO", I care about mocking hunks of code that users pass into Please prove that you can write a polymorphic You can't do that. You can do nothing with |
@pshirshov Yes, I agree they are quite compatible and I'd be happy to chat about ways we could work together. Appreciate your team's contributions to the library. |
This is not your usual DI. You may call it a "hybrid module system", for example. If you check or try it - I'm sure you would agree that it's far beyond any other "classic" DI and provides unprecedented flexibility while keeping your code typesafe 😄
In our case you may use a typeclass or... just use whatever else you want. It wouldn't break anything.
I'm not sure what exactly I have to prove. My point is: a good DI mechanism would complement type-level approaches very well so I'm showing what we do with zio/cats in our projects and proposing to contribute some stuff. Nothing else. @jdegoes happy to know that you are okay to chat. I'll send you a quick email in case you wouldn't mind. |
def crawl[F[_]: Monad](client: HttpClient[F], roots: Set[URL], ...): F[Unit] = {
roots.toList.traverse_(client.getUrl)
} This is all you've provided. I can adapt the signature exactly as in here. You haven't provided a body for @pshirshov I have looked at the DI framework you're showing. Dependency injection is never a tool, and always a framework. We pass arguments to functions here. Scalaz's core philosophy is fundamentally against DI. I don't know what @jdegoes is thinking. |
In our case it's a non-invasive modular tool which may emit code without any need in runtime support (static API is not published in full yet though we are working on it). In case you point me to our
Is it a dogm? For example we brought 50% performance boost (conservative estimation) to our customer by complementing cats and zio with our DI and bringing some other things. This stuff is already in production so these aren't just words. And we didn't loose any single bit of safety. I guess the "DI" term is spoiled and I should say "module system" |
I really don't care about the runtime parts, could be compile-time only and it'd be the same. It's not a dogma, it's just what DI is; overcomplication. Passing arguments to functions should always work, with the sole exception of type classes (which have proven themselves). I'm kind of amazed that the code benefited performance-wise from DI. That to me indicates severe architecture problems. |
Not true. In case your apps are small - yes. In case your apps are huge - you would have a big dependency graph to manage and transform. What would be the cost of typical refactoring in terms of Computers may process graphs better than people, aren't they?
What about 1M LoC?
I wouldn't agree with you and in case you think in terms of graphs and graph operations you would be able to prove yourself that the cost of explicit manual parameter passing grows exponentially. It's possible to mitigate it but at the end we are always limited by our brainpower. Computers compute better.
Not the code.... The development cost and speed... |
Programs are graphs. Transforming a dependency graph == passing and transforming values. Dependency injection is an inner platform. If you can't manage all of your values, you should consider avenues other than automating the entire practice of programming. |
This is wrong already, because You're stuck.
Nonsense. Code can run in
The number of data types that provide an instance is irrelevant (though will definitely be greater than just 1, due to orphan instances). The only thing that matters is the polymorphism of the client code, which can't exist — and still allow you to take advantage of IO-specific features, such as |
@jdegoes Instead, the For polymorphism, IMHO given working instances for import quantified._
import quantified.Quant._
import cats.effect._
import cats.implicits._
import scalaz.zio.IO
import scala.util.Random
def randomAdderService[F[+_, _]: ConcurrentThrowable: BifunctorCatch: Monad2: MonadTerminate2](initial: Int): F[Nothing, List[Int]] = {
val adder: F[Nothing, Int] =
syncTerminate[F, Int](Random.nextInt())
.map(initial + _)
BifunctorCatch[F].catchAll[Nothing, Throwable, List[Int]] {
for {
max <- SyncThrowable[F].delay(Random.nextInt(20))
fibers <- Traverse[List].traverse(1.to(max).toList)(_ => ConcurrentThrowable[F].start(adder))
res <- fibers.traverse(_.join)
} yield res
} { e => MonadTerminate2[F, Nothing].terminate(e) }
}
def syncTerminate[F[_, _]: SyncThrowable: BifunctorCatch: MonadTerminate2, A](thunk: => A): F[Nothing, A] =
BifunctorCatch[F].catchAll[Nothing, Throwable, A](SyncThrowable[F].delay(thunk)) {
exception => MonadTerminate2[F, Nothing].terminate(exception)
}
type SyncThrowable[F[_, _]] = Sync[F[Throwable, ?]]
def SyncThrowable[F[_, _]: SyncThrowable]: SyncThrowable[F] = implicitly
type ConcurrentThrowable[F[_, _]] = Concurrent[F[Throwable, ?]]
def ConcurrentThrowable[F[_, _]: ConcurrentThrowable]: ConcurrentThrowable[F] = implicitly
//type MonadTerminate2[F[_, _]] = Param[Lambda[E => MonadTerminate[F[E, ?]]]]
type MonadTerminate2[F[_, _]] = Quant[MonadTerminate, F]
def MonadTerminate2[F[_, _]: MonadTerminate2, E]: MonadTerminate[F[E, ?]] = implicitly[MonadTerminate2[F]]
trait MonadTerminate[F[_]] {
def terminate[A](t: Throwable): F[A]
}
object MonadTerminate {
def apply[F[_]: MonadTerminate]: MonadTerminate[F] = implicitly
implicit def monadTerminateIO[E]: MonadTerminate[IO[E, ?]] = new MonadTerminate[IO[E, ?]] {
override def terminate[A](t: Throwable): IO[E, A] = IO.terminate(t)
}
}
trait BifunctorCatch[F[_, _]] {
def catchAll[E1, E, A](f: F[E, A])(h: E => F[E1, A]): F[E1, A]
}
object BifunctorCatch {
def apply[F[_, _]: BifunctorCatch]: BifunctorCatch[F] = implicitly
implicit val bifunctorCatchIO: BifunctorCatch[IO] = new BifunctorCatch[IO] {
override def catchAll[E1, E, A](f: IO[E, A])(h: E => IO[E1, A]): IO[E1, A] = f.catchAll[E1, A](h)
}
} |
I can though why should I in case I may write a tool, cut costs (in US dollars) by half and make customer happy?
In case we speak in form of ultimate imperatives you may wish to consider other platform than github, hmm? Some people may be amazed if some guys speaking in ultimative terms would type their stuff directly into |
Also, what are you talking about? What about "The number of data types that provide an instance is irrelevant" is STRICTLY incorrect. It couldn't be more incorrect. I don't even know how to explain this, because it seems completely obvious. @pshirshov If you can cut costs by getting around programmer incompetence with tooling, go ahead. This is not what scalaz is for. |
Btw: to be clear I don't think DI belongs in Scalaz proper (e.g. |
I'm aware that you love to foster cooperation. Not all cooperation produces better code. Some code deserves to be deleted or not included in the scalaz umbrella. This I believe is an example. |
@edmundnoble It's also obvious that DI has to be a framework. It's acting as a second module system after the compiler is done wiring imports, there's no way to wire "runtime imports" without changing the application flow. The same is the case for implicits though – the moment an inductive implicit appears, programmer's control is reduced to magic rituals.
I take it you don't use scalaz-deriving, et al? |
Nope. If That you want to pretend all operations can fail equally and with the same error type, and that you are willing to lose the ability to track error recovery statically, is a subjective personal preference that I don't have to live with.
You know my views on transformers. They shouldn't be used in Scala. Type classes should be used and always backed by IO (or equivalent, including potentially newtypes).
Nope again. The primary benefit of seeing Seeing Polymorphism promotes principled reasoning and also makes testing easier. Polymorphism around the full set of capabilities offered by IO requires a type class, dictionary, or module. That you personally don't care about the full set of capabilities is irrelevant to me. |
It may apply some aspects to your code:
So, regarding the original question - I would start with a set of typeclasses describing neccessary primitives. It would be very convenient on it's own - to use only a minimal set of zio features. Actually we kinda have it already by implementing cats instances but it isn't enough. |
A. State is not required for the general problem at all, the algorithm can run recursively and simply re-instantiate dependencies. There would be no difference for pure modules. In short, I would like to, please, be presented with the pointTM of what you're trying to get at. State the problem, or the laws that are broken or the laws that should be created!
It's just given as an example of 'magic' that derivations entail.
Blanket statement; please address the point! As of now, I'm presented with zero proof that you have any idea what you're talking about, otherwise you'd be able to provide constructive input. This goes the same for our reddit discussions. If you want to make a stance against tooling – whether IDEs or derivations, just state so upfront. |
Please test this code: def getURL(url: URL): IO[Exception, String] = ???
def extractURLs(root: URL, html: String): List[URL] = ???
final case class Crawl[E, A](error: E, value: A) {
def leftMap[E2](f: E => E2): Crawl[E2, A] = Crawl(f(error), value)
def map[A2](f: A => A2): Crawl[E, A2] = Crawl(error, f(value))
}
object Crawl {
implicit def CrawlMonoid[E: Monoid, A: Monoid]: Monoid[Crawl[E, A]] =
new Monoid[Crawl[E, A]]{
def zero: Crawl[E, A] = Crawl(mzero[E], mzero[A])
def append(l: Crawl[E, A], r: => Crawl[E, A]): Crawl[E, A] =
Crawl(l.error |+| r.error, l.value |+| r.value)
}
}
def crawlIO[E: Monoid, A: Monoid](
seeds : Set[URL],
router : URL => Set[URL],
processor : (URL, String) => IO[E, A]): IO[Exception, Crawl[E, A]] = {
def loop(seeds: Set[URL], visited: Ref[Set[URL]], crawl0: Ref[Crawl[E, A]]): IO[Exception, Crawl[E, A]] =
(IO.parTraverse(seeds) { url =>
for {
html <- getURL(url)
crawl <- process1(url, html)
links <- visited.get.map(extractURLs(url, html).toSet.flatMap(router) diff _)
} yield (crawl, links)
}).map(_.foldMap(identity)).flatMap {
case (crawl1, links) =>
visited.update(_ ++ seeds).flatMap(_ =>
crawl0.update(_ |+| crawl1).flatMap(_ =>
loop(links, visited, crawl0)
)
)
}
def process1(url: URL, html: String): IO[Nothing, Crawl[E, A]] =
processor(url, html).redeemPure(Crawl(_, mzero[A]), Crawl(mzero[E], _))
for {
set <- Ref(Set.empty[URL])
crawlRef <- Ref(mzero[Crawl[E, A]])
crawl <- loop(seeds, set, crawlRef)
} yield crawl
} Your unit test should not interact with the real world. Hint: You can't do it, because it's impossible. Not without refactoring the code to introduce indirection. If you want to test A polymorphic version of def crawl[F[_, _]: HttpClient: Effect, E: Monoid, A: Monoid](
seeds : Set[URL],
router : URL => Set[URL],
processor : (URL, String) => F[E, A]): F[Exception, Crawl[E, A]] = {
def loop(seeds: Set[URL], visited: Set[URL], crawl0: Crawl[E, A]): F[Exception, Crawl[E, A]] = ??? Note that it can work with any So in particular, you can call So while your test will still use or "compile" to The alternatives to type classes over |
I resent even wasting the effort on DI to type this sentence.
You are almost certainly not going to convince me of this, so feel free to ignore my curiosity when I ask to see your methods and means of measurement. |
So it's horseshit because you said, right? Considering all my respect to you - you aren't constructive at all.
The method is simple: automate the most expensive rituals from developer's daily life and decouple teams by introducing a powerful language to build APIs. The measurement methodology is trivial: median feature/fix delivery time and some other KPIs. |
Yeah, so I am going to call shenanigans. Scalaz is not the project for magic woowoo DI nonsense. There are lots of gullible Scala programmers out there, but not here. Try elsewhere. "Try a global variable and make your code buggy. It will save you moneeeeeey!" -- Scalaz. |
There is no
Understood and considered.
You've triggered on prohibited "DI" abbreviation not even trying to understand what are we talking about. There are no "global variables" nor anything people get used to find in DIs. There is an effect-free planner which plans the job to be done, a garbage collector to throw out unneccessary operations and different interpreters allowing you do instantiate the context or write the tree (which would be very similar to one you write manually while wiring your application) or just print the plan. I may give you some relief by telling you you may consider planner a freemonad (and it's not an issue to add such an interface to it) and main provisioner is one of the interpreters. Which may run during compilation just in case. Does it sound better now? |
No I didn't. I was taking the piss. I have seen all the varieties of pretentiously passing function arguments, including the one you are proposing here. |
Could you please point me to such a thing? From what I know there is nothing like that at the moment. Nor for scala nor for anything else. |
The idea of abusing Here's a thing. Never use |
Holy moly. How implicits are related to the thing I'm talking about?... facepalm.jpg |
Because you linked it. link.jpg.png.pdf |
Remember. You would be surprised but it's not about DI over implicits. At all. |
Cool. Show me the code. |
Fine. It's in the repo. |
the |
Which repo? Which code? Please show me the code. |
@tonymorris you have incredible patience. |
At the bare minimum why could you not parameterize https://scastie.scala-lang.org/hOVpVTalSBemLwNBJfM5Zg In the polymorphic case, this function creates Refs, if you were to create a test IO effect, what would a |
Lets say were were to use Effect typeclasses, whats the benefit to implementing our own, versus implementing the cats one for interop? What would the differentiator between the two. |
You can easily write the filthiest possible DI as a Haskell type-class, your point? {-# LANGUAGE UndecidableInstances, FlexibleInstances, AutoDeriveTypeable #-}
import Control.Monad.Reader.Class
import Control.Monad
import Data.Maybe
import Data.TMap as T
import Data.Typeable
class MonadHttp m where
getPage :: String -> m Html
instance (Typeable m, MonadReader TMap m) => MonadHttp m where
getPage s = ($ s) =<< asks (getPage' . fromJust . T.lookup)
data HttpClient m = HttpClient {
getPage' :: String -> m Html
} Made with
There's a GitHub link on the doc site. I trust you'll find it yourself, eventually. |
This might be of interest - https://hackage.haskell.org/package/IOSpec. I am very curious what is possible given a proper final tagless abstraction over IO's concurrency capabilities 👍
Other options are conceptually very similar, but I think final tagless would be the most idiomatic and least error-prone.
❤️ I think the code can be written a bit clearer using macros (I have something similar in leibniz 1, 2). Could be extremely useful if it works consistently well. I actually would love to have something similar in scalaz proper.
Both statements seem correct but miss the point and don't provide an explanation. IIUC you can't use transformers because you would need "quantified implicits" for MTL (or QuantifiedContexts).
That doesn't mean that there is only one possible implementation for some
What if someone uses https://blog.softwaremill.com/what-is-dependency-injection-8c9e7805502f makes a lot of sense. https://izumi.7mind.io/v0.5.50-SNAPSHOT/doc/distage/ needs an explanation of what all that magic transforms into. E.g. what does
You could also implement the same thing using If I understand correctly, DI amounts to: Given a dependency graph that can be represented as a collection of functions However, you lose a clear understanding of what your components actually do and replace explicit dependency passing with some opaque algorithm. This would be my main concern if I was using such a system, especially for testing, because I need a very clear understanding what my code does when I am testing something.
I think that everyone is being a bit too abrasive for no good reason. |
@alexknvl
Isn't that why we're writing tests instead of relying on the compiler to find bugs for us? |
How would you test that nothing in your code uses |
This is the most compelling argument for |
I had assumed a mythical effect typeclass would still have |
Testing IO programs is not easy right now. One must either use heavy-duty, type-unsafe, and dysfunctional mocking machinery from the world of Java, or one must invent a lot of custom and complex machinery for use with final tagless style.
Testing IO programs should be easy, which means that it should be possible to make programs that are polymorphic in the effect type, so that either
IO
may be used, or a hypotheticalTestIO
.This ticket tracks the work involved in order to reach this goal.
IO
)Effect[F[_, _]]
type class, and create an instance of the type class forIO
EffectSyntax[F[_, _]]
class, which can add methods onto anyEffect: F
. Importingscalaz.zio._
should bring the syntax into scope automatically, without requiring additional imports.IO
class intoEffectSyntax[F[_, _]]
, and ensure no code is broken.EffectFunctions
class, which can add top-level helper functions for anyEffect
-like data type.IO
object intoEffectFunctions[F[_, _]]
, have theIO
companion object extendEffectFunctions
(specialized forIO
), and ensure no code is brokenThere are a few details:
Effect
cannot includesync
orasync
, since these methods are incompatible with testing code. However, they can include all other primitive operations that do not wrap effectful code, includingasyncIO
/asyncPure
.Fiber
, which will now have to be parameterized overF[_, _]
; probablyAsync
, which can hideIO
values; and maybeRef
(see next point).Ref
will have to go into the core set ofEffect
operations, e.g.def newRef[A](a: A): F[Nothing, Ref[F, A]]
. This might be enough to makePromise
and then everything else polymorphic in the effect type, which is a desirable goal of this ticket (if not necessary since it can always be done later if difficulties arise).Once all these steps have been completed, then it will be possible to introduce
TestIO
data type, and the user won't have to implement the numerous methods that make IO actually useful. It will only be necessary to implement a few key methods, and of course, we will provideTestIO
separately in a later ticket, to make the task even easier./cc @mmenestret
The text was updated successfully, but these errors were encountered: