Stateful Goroutines in Idris

Our example demonstrates how to manage state using processes in Idris. This approach aligns with Idris’s functional programming paradigm and its support for concurrent programming through processes.

import Control.Concurrent
import Data.IORef
import System.Random

-- In this example, our state will be owned by a single process.
-- This ensures that the data is never corrupted with concurrent access.
-- Other processes will send messages to the owning process to read or write state.

-- Define message types for read and write operations
data ReadOp = MkReadOp Int (Channel Int)
data WriteOp = MkWriteOp Int Int (Channel Bool)

main : IO ()
main = do
  -- We'll count how many operations we perform
  readOps <- newIORef 0
  writeOps <- newIORef 0

  -- Create channels for read and write requests
  reads <- makeChannel
  writes <- makeChannel

  -- Start the process that owns the state
  _ <- fork $ do
    state <- newIORef (the (List (Int, Int)) [])
    forever $ do
      select
        [ caseChannel reads $ \(MkReadOp key resp) -> do
            s <- readIORef state
            case lookup key s of
              Just val -> channelPut resp val
              Nothing -> channelPut resp 0
        , caseChannel writes $ \(MkWriteOp key val resp) -> do
            modifyIORef state (\s => (key, val) :: (filter (\(k, _) -> k /= key) s))
            channelPut resp True
        ]

  -- Start 100 processes to issue reads
  replicateM_ 100 $ fork $ forever $ do
    key <- randomRIO (0, 4)
    resp <- makeChannel
    channelPut reads (MkReadOp key resp)
    _ <- channelGet resp
    modifyIORef readOps (+1)
    sleepMillis 1

  -- Start 10 processes to issue writes
  replicateM_ 10 $ fork $ forever $ do
    key <- randomRIO (0, 4)
    val <- randomRIO (0, 99)
    resp <- makeChannel
    channelPut writes (MkWriteOp key val resp)
    _ <- channelGet resp
    modifyIORef writeOps (+1)
    sleepMillis 1

  -- Let the processes work for a second
  sleepMillis 1000

  -- Capture and report the op counts
  readOpsFinal <- readIORef readOps
  putStrLn $ "readOps: " ++ show readOpsFinal
  writeOpsFinal <- readIORef writeOps
  putStrLn $ "writeOps: " ++ show writeOpsFinal

-- Helper function to sleep for a given number of milliseconds
sleepMillis : Int -> IO ()
sleepMillis ms = threadDelay (ms * 1000)

This Idris program demonstrates state management using processes and channels. Here’s a breakdown of the key components:

  1. We define ReadOp and WriteOp types to encapsulate read and write requests.

  2. The main function sets up channels for reads and writes, and starts a process to manage the state.

  3. The state-owning process uses a select statement to handle incoming read and write requests.

  4. We start 100 processes for reads and 10 for writes, each sending requests through the appropriate channels.

  5. After letting the processes run for a second, we report the number of read and write operations performed.

Running this program would show the number of read and write operations completed in one second. The exact numbers may vary, but you’d see output similar to:

readOps: 71708
writeOps: 7177

This process-based approach in Idris provides a way to manage shared state safely in a concurrent environment. It’s particularly useful when dealing with complex state management scenarios or when working with multiple channels. The choice between this approach and other concurrency patterns depends on the specific requirements of your program and what feels most natural for ensuring correctness.