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, "\""
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
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.