Hi, I recently posted about this on reddit but did not receive a response so I thought I would try posting here with some slight adjustments.
tl;dr: is there an idiomatic way to detect and gracefully handle EOF from non-blocking IO?
I've recently started going through "Nim in Action" to get acquainted with Nim and I had a question about the best way to handle EOF when reading from stdin. In the chat server code from Chapter 3, the following code is presented to read in messages that the user types:
Code 0
while true:
let message = spawn stdin.readLine()
# do other code here that doesn't use `message`
echo "Sending \"", ^message, "\""
I have a few issues with this code: As is, sending an EOF (i.e. typing ^D) causes the program to crash instead of exiting gracefully. After some reading, it seems that EOF can be detected by catching EOFError . With that in mind, I tried to modify the code as follows:
Code 1
while true:
try:
let message = spawn stdin.readLine()
# do other code here that doesn't use `message`
echo "Sending message \"", ^message, "\""
except EOFError:
break
however this code does not work (the exception is still not caught). I believe this is because exceptions from other threads cannot be caught (please correct me if I am wrong). If I change the code to not use spawn then it works as expected, however the call to readLine becomes blocking which is undesireable. I also would prefer to avoid break and codify the EOF in the loop condition if possible instead.
I am aware that there is another version of readLine which returns a boolean indicating if EOF has been reached. This is much more in line with what I am looking for, however I am not sure how it would interact idiomatically with the use of spawn. Would something like this work?
Code 2
while true:
var message: string
let isNotEOF = spawn stdin.readLine(message)
# do other code here that doesn't use `message`
if not ^isNotEOF:
break
echo "Sending message \"", message, "\""
This still does not seem ideal in that message still must be mutable and we still need break, however it seems like an improvement over the other two options.
Am I missing something? Is there a better way? Thanks for taking the time to read this!
Why not simply place the stdin.readLine in another proc?
proc readLineSafe(): Option[string] =
try: return some(stdin.readLine()) except EOFError: return none[string]()
Then spawn that proc.
Thanks for the reply! That's a direction I hadn't thought of, unfortunately it gives me an error:
Error: cannot create a flowVar of type: Option[system.string]
From the manual here: https://nim-lang.github.io/Nim/manual_experimental.html#parallel-amp-spawn-spawn-statement
"Due to technical limitations not every type T is possible in a data flow variable: T has to be of the type ref, string, seq or of a type that doesn't contain a type that is garbage collected."
I'm guessing that's the problem here. Is there a way to work around the limitation in this case?
That error's from compiler/lowerings.nim , where we find this test for flowVars:
proc flowVarKind(t: PType): TFlowVarKind =
if t.skipTypes(abstractInst).kind in {tyRef, tyString, tySequence}: fvGC
elif containsGarbageCollectedRef(t): fvInvalid
else: fvBlob
In English: strings are OK. seqs are OK. Refs are OK. Anything not containing a GC'd thing is not OK.
So what are Option[string]s? To simplify the code in lib/pure/options.nim slightly,
type
Option*[T] = object
val: T
has: bool
Options are objects. They are, critically, not ref Objects. And they contain strings, which are GC'd. So they get classified as fvInvalid
Here's some slightly long code that duplicates sufficient code from lib/pure/options.nim to run the example, while using ref options so that the example actually works:
import strformat, threadpool
import typetraits
type
SomePointer = ref | ptr | pointer
type
OptionObj*[T] = object
when T is SomePointer:
val: T
else:
val: T
has: bool
Option[T] = ref OptionObj[T]
proc some*[T](val: T): Option[T] =
when T is SomePointer:
assert(not val.isNil)
new(result)
result.val = val
else:
new(result)
result.has = true
result.val = val
proc none*[T]: Option[T] =
new(result)
when T isnot SomePointer:
result.has = false
proc get*[T](self: Option[T]): T =
if self.isNone:
raise newException(UnpackError, "Can't obtain a value from a `none`")
self.val
proc isSome*[T](self: Option[T]): bool {.inline.} =
when T is SomePointer:
result = not self.val.isNil
else:
result = self.has
proc readLineSafe(): Option[string] =
try: return some(stdin.readLine()) except EOFError: return none[string]()
while true:
let message = spawn readLineSafe()
let input = ^message
if input.isSome:
echo &"Sending \"{input.val}\""
else: break
Thanks for the detailed reply! Rather than redefine all the details of Option, I defined
type OptRef[T] = ref Option[T]
proc readLineSafe(s: File): OptRef[string] =
result = new(Option[string])
try:
result[] = some(s.readLine())
except EOFError:
result[] = none(string)
Then in my main code:
let messageOptRef = spawn stdin.readLineSafe()
if (^messageOptRef)[].isNone:
break
let message = (^messageOptRef)[].get()
echo "Sending message \"", message, "\""
It works!
Maybe it's because I'm not used to the ergonomics of Nim but I feel like this is kind of a hack :( is there not a more straightforward way to do this? Or is this considered idiomatic?
2) As a result of (1), I now have to either a) wrap all the code between reading in the line and using it in a try-catch block (which is undesirable) in order to ensure that message is in scope or b) declare message as var outside of the try-catch block and initialize it in the block, which is undesirable because it allows message to be mutable.
This is not correct. You can use the following idiom to export a safe immutable value to the outer scope:
let foo = try:
42
except:
-1
You are quite right, that's my mistake. However I'm still having trouble seeing how we could apply that here to get the desired result due to the interaction of spawn and try-except. For instance, I don't think we could do
let message = try:
spawn stdin.readLine()
except:
???
because 1) an exception thrown from readLine won't be catchable here (I think) and 2) what would we put in the except?
If we extract this all into a separate function and spawn that, we're back to returning an Option[string] which as we've seen can't be returned from a spawn ed function. I guess one option would be to use some kind of "indicator" string value to signify EOF (perhaps a literal EOF?) instead of using an Option.