cats-effect icon indicating copy to clipboard operation
cats-effect copied to clipboard

A little more `IO` and a little less `F[_]`

Open armanbilge opened this issue 3 years ago • 12 comments

I think we've all converged on the idea that beginners should definitely start with concrete IO. And that there's no harm in continuing to use concrete IO indefinitely; indeed many teams are doing this.

So this is all well and good. Except our docs are full of stuff like this:

https://github.com/typelevel/cats-effect/blob/3da61a59438da30f9bb01192fec9e690aacc3ce4/std/jvm/src/main/scala/cats/effect/std/Console.scala#L30-L39

Which invariably leads to beginner questions: what is F[_]? To which a quick answer is "replace every F you see with IO".

Well, woops 😛

def myProgram[IO[_]: Console : Monad]: IO[Unit] = ???

I think we should prioritize writing examples with concrete IO, since that is exactly what we are pitching. Providing a second example written in terms of F[_] (possibly via a fancy tabbed switcher on the website) would be reserved for bonus points.

armanbilge avatar Aug 27 '22 22:08 armanbilge

I can work on this one

iRevive avatar Aug 29 '22 06:08 iRevive

On a similar but wider note, what do folks think about having additional type alias / wrappers / subtypes of common data types specific to IO; e.g. ResourceIO, RefIO, FiberIO, etc (better naming welcome)? If many people think this would be a good idea, we could also do the same for fs2.Stream and http4s.HttpRoutes

BalmungSan avatar Aug 29 '22 16:08 BalmungSan

@BalmungSan you mean like these? :) https://github.com/typelevel/cats-effect/blob/27307cfe6cd9ea3cb230a8f9be5e16f624a914cd/core/shared/src/main/scala/cats/effect/package.scala#L72-L74

armanbilge avatar Aug 29 '22 16:08 armanbilge

After a little chat with @armanbilge we came to the realization that the type alias is not enough and probably hurtful in the long run. And having wrappers / subtypes or other alternatives may lead to a lot of work from the maintainers with little reward. So, we both concluded that indeed the best is just to improve the docs to use more concrete IO as a default (it could be good to still preserve the F[_] alternatives in a different tab or something)

Nevertheless, we also came to the conclusion that while that will help people will still ask: "why I need to specify IO?" Like why is Resource[IO, File] and not just Resource[File]? That would probably become a FAQ and its answer should be easy to find and easy to share; also such an answer should be simple. Thus, I think the best would be to explain it in terms of interop only, mention that cats-effect is intended as a building block and that other libraries can reuse most of its components like the typeclasses and the stdlib.

BalmungSan avatar Aug 29 '22 17:08 BalmungSan

I think we've all converged on the idea that beginners should definitely start with concrete IO. And that there's no harm in continuing to use concrete IO indefinitely

I should keep my mouth shut because I don't have time right now to stay and defend my position, but for posterity: I dissent from the first statement because I vigorously dissent from the second.

rossabaker avatar Sep 02 '22 21:09 rossabaker

Hi hello 👋🏻 I just merged #3134, which added IO examples alongside the F[_] examples in a couple of places and was set to resolve this issue. That said, reading through this conversation it seems like there are still open actions? For that reason I'm going to reopen the issue (but please do feel free to close if these are already addressed or would be better addressed independently)

The actions I think we should follow up on:

First

Nevertheless, we also came to the conclusion that while that will help people will still ask: "why I need to specify IO?" Like why is Resource[IO, File] and not just Resource[File]? That would probably become a FAQ and its answer should be easy to find and easy to share; also such an answer should be simple.

Second

I think we've all converged on the idea that beginners should definitely start with concrete IO. And that there's no harm in continuing to use concrete IO indefinitely

I should keep my mouth shut because I don't have time right now to stay and defend my position, but for posterity: I dissent from the first statement because I vigorously dissent from the second.

I think it would be good to have something concrete to point to about when one might choose concrete IO vs F[_]. I think it is kind of hinted at in the polymorphic code section of the tutorial but something more focused and direct could be helpful (I've seen decision factors discussed in discord but I don't think they're "officially" written down anywhere?)

samspills avatar Sep 17 '22 20:09 samspills

Thanks to @samspills for the action items. I think duplicating the examples without offering more guidance only muddies the waters.

To contribute effectively at all layers of the current Cats Effect ecosystem -- applications and libraries -- one needs a grounding in both type classes machinery and IO. We can start in concrete IO, but the newcomer still encounters polymorphic signatures in their autocompletes and compiler output as soon as they get to FS2 or Doobie or http4s. I ran an informal survey at work:

"I find it confusing to go between polymorphic library code and concrete application code."

  • 100% of the more senior channel said no
  • 100% of the channel where people have done FP for less than a year said yes!
    • 100% of these people were fine with using F[_] even as they think "it's just IO".

I'm afraid this this well-intended attempt to shield beginners forgets their experience. I've been around a long time, but a couple years ago, I was a newcomer to Haskell. We tried to making onboarding easier with RIO, a concrete value with IO and Reader capabilities. We eschewed constraints for RIO everywhere. It sucked:

  • All of the libraries had Monad and MonadIO constraints and m return types. I didn't get eased into anything. I was learning two dialects at once.
  • Application code that could have had a mere Monad constraint ends up concrete in RIO. Effects lose their effectiveness when everything is an effect!

When we divide into "polymorphism for me and not for thee," half the developers can't understand half the code. This is the worst place to be, at all points on the learning curve. We should either:

  1. Embrace polymorphism in apps. I've been successful this way for years, with heavy and early contributions from juniors.
  2. Deprecate polymorphism in libraries. Many successful functional languages don't have higher-kinded types.

The second path is a radical departure and available in many other packages. We should be leaning into the first.

rossabaker avatar Sep 22 '22 19:09 rossabaker

I use my polymorphism a lot. I argue heavily that we should NOT give up using the style.

It is immensely useful in regards to the skill ceiling of the language, while in my opinion not meaningfully increasing the skill floor if you just treat the F as IO as was mentioned.

ChristopherDavenport avatar Sep 22 '22 20:09 ChristopherDavenport

So, we both concluded that indeed the best is just to improve the docs to use more concrete IO as a default (it could be good to still preserve the F[_] alternatives in a different tab or something)

This reminds me of how the standard collections library documentation was prior to the rewrite, and at least for me, it never really helped.

morgen-peschke avatar Sep 22 '22 20:09 morgen-peschke

My 2 cents. Even if my target effect is IO I use F[_] in the vast majority of my projects. The major benefit is that I can switch to a different effect any time. Am I using Ask in a new module? No problem, let's switch from IO to Kleisli[IO, A, _] at any moment.

I prefer using specific constraints, for example, function def summary[F[_]: Functor] requires Functor, while def findUser[F[_]: Sync] requires Sync. Based on the signature and constraints, I can assume that the findUser function has some 'side effects'. While Functor implies, the function performs some basic transformations.

With IO everywhere this approach does not work anymore. I cannot make an assumption about the complexity of a function. On the other hand, I rely on an author of a code, the author should use the least possible constraint instead of placing F[_]: Async everywhere. Otherwise, it is not any different from IO.

It's a tricky question about which approach is more suitable for newcomers. IO is more simple indeed. While F[_] forces you to learn some basics.

I believe the most basic examples should be written with plain IO. I do not need to know what Functor or Sync mean when I want to print 'hello world' or send a simple HTTP request and decode a result as JSON. But, ideally, we should provide both examples (IO and tagless final), and explain the tagless final approach in detail.

iRevive avatar Sep 22 '22 20:09 iRevive

100% of these people were fine with using F[_] even as they think "it's just IO".

I've changed my stance on this and, in the context of my company, I recommend using concrete IO, paired with some guidelines around interfaces to avoid IO(...) everywhere.

I've found two issues with explaining code using F[_].

  1. The first is that the idea you need to harp on is that you have these datatypes that compose, rather than side-effects that execute, and it's harder to see that when you don't see the type at hand. This is the more conceptual difficulty, but to be honest I've been very successful teaching this to Scala programmers. I also feel like this is what this discussion is focusing on.
  2. The second, and imho the real issue, is when you need to teach non Scala programmers. The amount of Scala concepts and, more annoyingly, quirks you have to deal with is huge compared to IO and note that isn't an issue when reading code, only when writing it (e.g. getting confused on the syntax for context bounds, knowing when to use a context bound and when not to, knowing that "ambiguous implicits" means to add an ascription, not being able to add a default param that needs an implicit, like ex => ex.pure[F], which you can do in IO). Reading code which is already compiling, you can indeed just mentally replace F[_] with IO, but that's not the case when writing due to the above. Note that most of these issues are Scala issues, so a FP-beginner Scala-fluent programmer can deal with them just fine, but personally we've been hiring a lot of non Scala programmers, and teaching them concrete IO has been more successful than teaching them tagless final. Also note that this experience doesn't translate to Haskell, using this type of polymorphic code maintains pretty much the same experience. So, in my experience , writing concrete IO + reading tagless final is still significantly easier than writing tagless final for non Scala programmers, and that's a significant market for a lot of companies once they scale in size.

EDIT: to be clear, I still think there are significant advantages to having our library ecosystem be based on abstract constraints, for example the fact that there is a mostly consistent vocabulary for actions across the ecosystem is huge, from doobie to cats-parse, and a direct consequence of using cats abstractions

SystemFw avatar Sep 23 '22 10:09 SystemFw

My two cents.

  1. Nobody is suggesting that the libraries ecosystem should embrace concrete IO; I did propose having some kind of alias or something to help users of concrete IO but quickly realized that it was more bad than good. Thus, I rather proposed having a FAQ about it (more about that later).
  2. We should accept that applications being written using a concrete data type is not just a possibility but a fact. As such I think we should not close the doors to that style and folks writing it; that does not mean that we should not give up on tagless style nor stop mentioning their advantages (as well as its disadvantages), but, I do think we need to be more critical about when to recommend it.
  3. However, I don't think we should focus on how application code should be written; both because IMHO is outside of the scope of this issue, and because I think we all can agree that each project has its own specific circumstances which may favor more one style than others.
  4. As Fabio detailed the main problem with tagless style is not really not using IO but rather all the mechanical indirections and the worse UX for developers. Those may not be a big deal once you master the style but they are a big deal when learning, so I do think it increases the learning curve; btw, I think there is another actor to consider in this discussion which is folks that are learning cats-effect on their own without a mentor at work.
  5. After thinking a lot about Ross's point of having two "dialects" being problematic, I concluded that I actually don't buy it (whereas I did agree to it when I first read the comment). First, the issue just goes away if you learn both styles. And since my point is that tagless final is harder to learn than concrete IO and that I consider learning tagless final as "learning concrete IO + learning more stuff", then it means that arguing going directly to tagless final does not solve this issue. Second, both dialects already exist and will continue to do so for multiple reasons, thus I don't see a reason to try to avoid it.
  6. "F[_] is just IO" is a great mental model when reading code / examples, yet the only way a newcomer would know that is by asking in discord. That is why I think it is extremely important to have a FAQ or something about it in the docs. Also, IME, once you say that then the next follow-up question is usually "then why not use IO directly? What is all this F[_] stuff"; which is a question that is hard to answer, because it is very deep and usually you get other questions in the middle relating to type kinds, implicit, syntax, etc (basically Fabio's point again).

Thus, in conclusion, I think that the best path forward is:

  • Having more examples using IO to encourage folks to actually play with the "programs as values" paradigm, which is the main idea, rather than thinking about abstractions; or worse mechanical issues.
  • Since sooner than later they will find F[_], we should have a concise answer which is both easy to share and even more importantly easy to find; e.g. I would love if we mostly never need to link it because most folks would find it on their own. IMHO, that answer can just focus on saying that: cats-effect is a foundational library that allows users to extend it, including providing their own IO implementation. Thus F[_] is there mostly to allow the reuse of the library with any IO. Meaning that essentially every time you see F[_] you can just say IO Even if that is half wrong.
  • Have another in-depth resource on tagless final that covers the topic in detail and that could be linked in the previous answer.

BalmungSan avatar Sep 23 '22 13:09 BalmungSan

I volunteered myself and @valencik to attempt to write some more documentation about this! I think there's a lot to potentially cover here, and I think both the concrete IO and abstract F[_] approaches are reasonable given different constraints. To scope down a bit, we were just going to focus on a slightly tweaked idea of the second action item in my comment above.

The current proposed plan is to target users who are:

  • beginners or newcomers to cats-effect / the Typelevel ecosystem
  • writing personal and/or application code (aka. not writing a library intended to be used within and across the ecosystem)
  • essentially myself 1.5 years ago 😅

and the content I would like to cover is:

  • briefly, pointing to other available resources as well, what are IO and F[_]
  • if something calls for F[_], what does it mean to provide an IO there? What else could be provided?
  • examples of writing some code with IO
  • the same examples using F[_]
    • in particular I want to come up with a set of examples that aren't just showing the code with F[_] literally replacing IO but demonstrating how different effect types could be passed in and what that capability can achieve
  • a purely descriptive discussion about the pros and cons that can come with either approach when writing a first application

To emphasize: I don't want this to be a document that tells someone to definitively use one approach or the other. Since both choices are being made the goal is to document them better, and what their respective strengths are, so that folks making the decision have more information when making it.

samspills avatar Sep 29 '22 00:09 samspills