Search code examples
asynchronoustimeoutdelaynim-lang

How do I execute a proc with a delay in nim?


I was looking to get a bit more into async in nim with the C-backend and have a fair bit of experience with Javascript. There functionality such as setTimeout is available to execute a function at a later time.

Example:

setTimeout(() => console.log("Potato"), 1000)

What does the equivalent in nim look like?


Solution

  • If you want a quick solution, just go with 1). If you're curious about the async fundamentals, read the entire post.

    1) With Async Syntax + SleepAsync

    The simplest one is just using an async proc and sleepasync:

    import std/asyncdispatch
    
    proc afterX(wait: int) {.async.} =
      await sleepAsync(wait)
      echo "Stuff Happened"
    
    let future: Future[void] = afterX(500)
    
    ... You can do more stuff in between getting the future and waiting for it ...
    
    waitFor future
    

    The async pragma changes the proc to return a Future of whatever it was returning. In this case nothing, so void. This future in your "global", synchronous context you can then just waitFor.

    Note that waitFor is not similar to JS await. JS has no equivalent to it as it does not have this level of access to the context it is executed in. waitFor is for waiting for a task to complete outside of an async-context and it will block the thread to do so. Nim's await is what functions the same as JS's await and that can only be used within async procs.

    2) Without Async Syntax: addTimer

    Nim has a proc setTimer which you might stumble over if you search for timeout-related keywords in the docs.

    That does what we want, but with very different, more low-level syntax, that functions almost the same as 1:

    import std/asyncdispatch
    
    let echoMe: Callback = proc(fd: AsyncFD): bool =
      echo "Stuff Happened"
      return true
    
    addTimer(500, true, echoMe)
    
    ... You can do more stuff in between registering the callback and polling for it ...
    
    poll()
    

    AsyncFD here is AsyncFileDescriptor (or Handle on Windows). When doing async IO operations, this parameter is filled with the handle to whatever that IO operation is being done on. Not relevant for us in this scenario, but useful to know.

    AddTimer "registers" the echoMe proc on a the dispatcher (the sort of event-loop) to execute later. poll then fetches that proc and executes it.

    As an aside and not relevant for the discussion:

    The boolean used by addTimer defines whether echoMe should be removed from the dispatcher/event-loop after it was executed the first time. So false means to execute it over and over again with time delays in between.

    The boolean returned by echoMe also defines whether it should deregistered from the dispatcher/event-loop, but does so after every execution. This only matters if addTimer was given true for its boolean in the first place. So true from echoMe means "Even if I am allowed to execute over and over, stop that now, deregister me from the event-loop".

    Why are these two the same? They look different!

    When poll fetches a proc, it blocks the thread until it can get a task or until it times out. Which is eerily similar to waitFor. That is because waitFor is just a disguised call to poll run in a while-loop until the future.finished boolean flips from "false" to "true".

    Both of these do what we want, albeit async does it with nicer syntax:

    • Register a task (echoMe) on a queue via a "task dispatcher" (The async pragma / addTimer) to execute later. The dispatcher functions as a sort-of event-loop.
    • Enable you to do whatever you want in the meantime
    • Force you to fetch the task from the queue (poll, waitFor) and execute it implicitly

    The main difference is that with the Async-Syntax, waiting for N milliseconds is part of the task itself (it is within echoMe via sleepAsync), while with addTimer the waiting is done before the task is executed during polling.