Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@adamgfraser
Copy link
Contributor

Resolves #5032. This is a little annoying in toy examples but it is hard to argue that there is a possibility of failure and in a more complex application you would like to be aware of that and be able to handle it. Also we have kind of already crossed this bridge with the signature of getStrLn.

@adamgfraser adamgfraser requested a review from jdegoes May 1, 2021 23:28
@jdegoes jdegoes merged commit 210cc37 into zio:master May 2, 2021
@jdegoes
Copy link
Member

jdegoes commented May 2, 2021

🙏

@adamgfraser adamgfraser deleted the 5032 branch May 15, 2021 13:40
@jamesward
Copy link
Contributor

I've been playing with this and it isn't working as I hoped because scala.Console wraps an OutputStream in a PrintStream which:

never throws an IOException; instead, exceptional situations merely set an internal flag that can be tested via the checkError method

So it is true that OutputStream.write can throw an IOException but the PrinterStream gobbles it up. Not sure what the best approach is. It'd be nice to accurately represent reality but that might mean replacing scala.Console / java.lang.System.out with more principled approaches.

@jdegoes
Copy link
Member

jdegoes commented May 17, 2021

We could call checkError on the underlying PrintStream and manually throw our own IOException, or merely re-implement PrintStream atop OutputStream in a way that throws IOException on write errors.

@domdorn
Copy link
Contributor

domdorn commented May 18, 2021

I'm questioning if it is really necessary to add an Error-Type here..
neither scala.Console.println nor System.out.println throw exceptions anywhere.

The only place I could imagine this requirement would be useful is if someone would create a pure CLI/jLine/ncurses like application or something where the console is a actual console, e.g. interfacing over a serial line with some legacy system, 80x25ish... but if one would do that, then console.putStrLn also would need to be wrapped in a blocking {} as those connections are slow i/o.
(IMHO) At the moment, this is just making the life of many people harder than it needs to be.

@jamesward
Copy link
Contributor

I've had issues where the out stream is broken in some way (due to something mutating System.out or some system console issue). It doesn't happen often but when it does, having a way to handle that failure is sure nice. Yeah, this does add some pain though so I'm not sure if it is worth it. I like representing reality as accurately as possible, but sometimes reality is really messy.

@jdegoes
Copy link
Member

jdegoes commented May 18, 2021

Maybe what we should do is reflect println and remove the error type (since it really can't fail) but also have a library (or a method in this service) that can do the printing in a fallible way.

@adamgfraser
Copy link
Contributor Author

I wonder if we should do the same thing with getStrLn. The inconsistency between the operations there is a little strange.

@jamesward
Copy link
Contributor

The underlying APIs aren't homogeneous. System.in is a BufferedReader and BufferedReader.readLine throws an IOException. But System.out and System.err are PrintStreams and hide all IOExceptions on the underlying OutputStream.

@adamgfraser
Copy link
Contributor Author

Understood, but it seems like from the user perspective there are two classes of users: (1) most people just want to do console operations with the assumption that they can't fail, (2) a smaller number of people want to really expose and possibly handle the different ways that console operations can fail. So it seems like maybe we need two classes of operators. The first would be "simple" ones and I think would not expose a typed error at all. The second class would expose an error type and would potentially do additional work to surface the error like in the case of printing to the console.

@jamesward
Copy link
Contributor

Yeah, that sounds good. Console and ConsoleSafe but I do think Console.getStrLn should still expose the IOException.

@adamgfraser
Copy link
Contributor Author

Yes. We could certainly do that and it was the behavior before. I am wondering if getStrLn should get the same treatment. My impression is that the people who are in category (1) only ever do orDie on getStrLn and just complain about having to do that less because getStrLn is less commonly used that putStrLn. So I wonder if when we break these out it makes sense to similarly distinguish between fallible and infallible versions of getStrLn.

@jdegoes
Copy link
Member

jdegoes commented May 19, 2021

How about instead of changing getStrLn, we just add def ! = orDie to ZIO. Then you can write getStrLn ! to make the exception go away.

@jdegoes
Copy link
Member

jdegoes commented May 19, 2021

Then the remaining issue is how to expose the fallible variation of putStrLn, which would presumably bypass PrintWriter. I'd be tempted to call that putStrLnOrFail or something (printLineOrFail in the new renames).

@adamgfraser
Copy link
Contributor Author

I think it is fine to keep it as is. I'm not sure we even need a separate symbolic operator. People have been fine with calling orDie on it so far, though maybe that is generally useful. Just seems a little strange that one console operator exposes an error type and another doesn't, especially when we're saying that both of them can actually fail.

@jamesward
Copy link
Contributor

Depends on what "actually fail" means. If an API we are wrapping hides failure, do we expose the actual possible failures or continue hiding them? For me, 99% of the time if what I'm building needs to write to stdout and can't, I want to know and possibly the only way to know is to fail. But it would be interesting to see how this impacts logging. Cause for most server stuff that'd be my interface to stdout.

@adamgfraser
Copy link
Contributor Author

Yes, what I am saying is I think we want to provide the interface that we think is the "right" interface versus just the interface that lower levels give to us. So I think that means exposing the possibility of failure in both reading and writing and if necessary changing the implementation to support that. Then there is this issue that some people aren't interested in having this failure possibility being exposed, and we would like to do something to make that convenient. So I think we have either:

trait Console {
  def printLine(line: String): UIO[Unit]
  def printLineOrFail(line: String): IO[IOException, Unit]
  def readLine: IO[IOException, Unit]
}

Or:

trait Console {
  def printLine(line: String): UIO[Unit]
  def printLineOrFail(line: String): IO[IOException, Unit]
  def readLine: UIO[Unit]
  def readLineOrFail: IO[IOException, Unit]
}

The second seems slightly more consistent to me (there are two things that can fail, the more general signatures of each reflect that, convenience variants of each are provided that ignore that possibility) but they have the same power since you can always call orDie on readLine in the first version.

@jamesward
Copy link
Contributor

jamesward commented May 19, 2021

Yeah, the uniformity of the second is nice. What about the defaults being the failable ones though?

def println(line: String): IO[IOException, Unit]
def printlnUnsafe(line: String): UIO[Unit]

@domdorn
Copy link
Contributor

domdorn commented May 19, 2021

I propose to keep the methods for the 1.0.x branch like they are (so revert this change) and we can change them in 1.1.x or 2.0.x with a "migration guide". at the moment people get unexpected compile errors when they just "changed a minor version"

@jamesward
Copy link
Contributor

@domdorn The change was already reverted.

@domdorn
Copy link
Contributor

domdorn commented May 19, 2021

then we should release a 1.0.9 before too many people upgrade to 1.0.8, change their code, later upgrade to 1.0.9 and have to change it again. if we do that soon, people will just skip 1.0.8 altogether.

@jdegoes
Copy link
Member

jdegoes commented May 19, 2021

I think these are good candidate changes for 2.x, and maybe the default should be the fallible ones because it is pretty easy to call orDie if that is not desired (and that could be made easier with ! if desired).

@adamgfraser
Copy link
Contributor Author

PR for master is already submitted at #5126 and PR for series/2.x is already at #5124.

However, seems like it could be good to get some alignment on what we want to do here. I have been working on the assumption that we wanted two versions of these operators, but if we want to just have the fallible versions and let people call orDie maybe we should just keep the implementation in 1.0.8 since that already embodies that behavior and is already out there. We could build on that with additional work to implement ! and fail with the IOException in printLine.

I do think the people who want the fallible version are in the substantial minority though and it does create some friction.

@jdegoes
Copy link
Member

jdegoes commented May 19, 2021

I do think the people who want the fallible version are in the substantial minority though and it does create some friction.

This is a good point.

We're at the following place:

  1. Most people want the orDie semantic (?).
  2. The recoverable versions are still useful

If we make any major change, it should be pushed to 2.x. In fact, I think the change with the exception was source incompatible, so maybe we should have pushed that to 2.x (but too late now).

@domdorn
Copy link
Contributor

domdorn commented May 19, 2021

In fact, I think the change with the exception was source incompatible, so maybe we should have pushed that to 2.x (but too late now).

thats why I proposed to make a 1.0.9 release ASAP, then probably a big percentage of users will just skip 1.0.8 and everything will work fine for them like with 1.0.7 (without any need to change any code)

@adamgfraser
Copy link
Contributor Author

Yeah I think the question is just whether the ship has already sailed on that one. If we think that the right end state is for the default versions of these operators to use the orDie semantic then we should revert it and add another variant with the recoverable semantic in the series/2.x branch. If we think the right end state is to have one variant of this that is recoverable and people can call orDie on it then I tend to think we just stick with what we have as it is already out there.

@jdegoes
Copy link
Member

jdegoes commented May 19, 2021

I think with @adamgfraser's recent change in #5128, it means that users who do not want exceptions in getStrLn / putStrLn can now type 1 additional character to get that behavior.

e.g.:

for {
  _ <- putStrLn("What is your name?")
  name <- getStrLn
  _ <- putStrLn("Hello, " + name)
} yield ()

versus:

for {
  _ <- putStrLn("What is your name?")!
  name <- getStrLn!
  _ <- putStrLn("Hello, " + name)!
} yield ()

This seems like a low enough overhead that maybe we don't need two separate versions?

The "multiple versions" path just seems like it is fraught with ever-expanding complexity (do we do that for all the time methods? all the system methods? do we encourage all users to do that for all their services?).

Not 100% sure, however...

@jamesward
Copy link
Contributor

That is what I'd lean towards. :) First, make it honest. Then make it easy.

@adamgfraser
Copy link
Contributor Author

I think it is definitely good to keep these interfaces thin if possible so would be in favor of that.

@jdegoes
Copy link
Member

jdegoes commented May 20, 2021

@adamgfraser Another consideration is that a lot of people who don't like typed errors just use Task everywhere. So maybe this means the IOException appearing in putStrLn really only affects people who (1) like typed errors, (2) want to make print line errors infallible. The latter requirement is possibly satisfied by the new .debug.

@jdegoes
Copy link
Member

jdegoes commented May 20, 2021

So if I can summarize:

  1. Keep putStrLn failing with IOException, and make sure it can actually fail by bypassing PrintWriter.
  2. Encourage ! to die with unexpected errors.
  3. Promote and maybe amplify debug stuff for when you want to print and not worry about errors.

@adamgfraser
Copy link
Contributor Author

@jdegoes Yes I think that is right. It might be nice to have a variant of debug that was a static method so you could do something like zio1 *> ZIO.debug("I'm here!") *> zio2.

@jdegoes
Copy link
Member

jdegoes commented May 20, 2021

@adamgfraser I love that idea!

@jamesward
Copy link
Contributor

Yeah! Maybe the primary use case for putStrLn that can't fail, is debugging. So providing a nicer debugging experience is a great way to add value. Also, I realized that if people want an unfailing stdout println, they can always use scala.Console or System.out.

@domdorn
Copy link
Contributor

domdorn commented May 24, 2021

hmm... if we have a ZIO.debug, will it fallback to zio.logging.log.debug if that is in the environment?

@adamgfraser
Copy link
Contributor Author

No it doesn't have any environmental dependencies.

@catostrophe
Copy link

I've re-read this @jdegoes's thread https://twitter.com/jdegoes/status/1370822616844529671 and I think a checked Java exception shouldn't be put into the E channel just because someone declared it as "checked" 25+ years ago. IOException in Console is not recoverable (or almost never such), so it should be a defect by default.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Console.putStr & Console.putStrLn should have an IOException error

5 participants