Context

The time has come to talk about the third type parameter of an IO. In the type IO[R,E,A], R is the type of the context. The context is a value that is always accessible to any IO. You can think of it as a local global variable. I assure you this sentence make sense!

run : executing an IO in a given context.

We have called the method run many times. And every time we gave it None as argument. The argument you give to run is actually the context value. You take any value you want as the context. Usually the context is a value you want to be accessible from everywhere.

Global variables are indeed accessible from everywhere but they are very annoying because they can only have one value at a time. Furthermore every change made to the global variable by a part of your code will affect all other parts reading this global variable.

On the opposite side, local variable are nice because every part of your code can have its own dedicated local variable. But they are annoying because to make them accessible everywhere, you have to pass it to every functions as parameters. This is error prone and pollutes your code.

Given an IO named main, when you call run with some context r, the value r is accessible from everywhere in main. The context behaves like a global variable inside the running IO. But you can pass every call to run a different context. The context behaves like a local variable between different calls to run.

read : accessing the shared context.

To access the context, just call the function io.read. Its result is the context:

>>> main : IO[int,None,int] = io.read
>>> main.run(5)
Ok(success=5)
>>> main.run(77)
Ok(success=77)

This example is indeed trivial. But imagine that main can be a very big program. You can call io.read anywhere in main to get the context. It saves you the huge burden of passing the context from run to every function until it reaches the location of io.read.

contra_map_read : transforming the shared context.

As I said, the context behaves as a local global variable. With io.read you have seen its global behaviour. The method contra_map_read transforms the context, but only for the IO it is being called on. Note that the context is transformed before the IO is executed.

The method contra_map_read is very useful when you need to alter the context. For example, your program may start with None as context, read its configuration to instantiate the services it wants to inject and then change to context to pass these services everywhere for dependency injection.

>>> main : IO[str,None,int] = io.read.contra_map_read(lambda s: len(s))
>>> main.run("Hello")
Ok(success=5)

defer_read : doing something context-dependent later.

The function io.defer_read is useful when you it is easier to implement an IO as a normal function and then transform it into an IO. io.defer_read, like io.defer, takes as first argument the function you want to execute later. But unlike io.defer, this function:

  • has access to the context
  • returns a Result[E,A]

The last point is important because it means the function can raise errors while io.defer can only raise panics.

>>> def f(context:int, i: int) -> Result[str,int]:
...   if context > 0:
...     return result.ok(context + i)
...   else:
...     return result.error("Ooups!")
>>> main : IO[int,None,int] = io.defer_read(f, 5)
>>> main.run(10)
Ok(success=15)
>>> main.run(-1)
Error(error='Ooups!')

defer_read_io : computing a context-dependent IO later.

The function io.defer_read_io is the IO counterpart of io.defer_read. It is useful when the context-aware function you want to execute later returns an IO.

>>> def f(context:int, i:int) -> IO[None,str,int]:
...   if context > 0:
...     return io.pure(context + i)
...   else:
...     return io.error("Ooups!")
>>> main : IO[int,None,int] = io.defer_read_io(f, 5)
>>> main.run(10)
Ok(success=15)
>>> main.run(-1)
Errors(errors=['Ooups!'])

Use Case: Dependency Injection

As a demonstration of how dependency injection works in Raffiot, create and fill the file dependency_injection.py as below:

from raffiot import *

import sys
from dataclasses import dataclass
from typing import List

@dataclass
class NotFound(Exception):
  url: str

class Service:
  def get(self, path: str) -> IO[None,NotFound,str]:
    pass

class HttpService(Service):
  def __init__(self, host: str, port: int) -> None:
    self.host = host
    self.port = port
  
  def get(self, path: str) -> IO[None,NotFound,str]:
    url = f"http://{self.host}:{self.port}/{path}"

    if path == "index.html":
      response = io.pure(f"HTML Content of url {url}")
    elif path == "index.md":
      response = io.pure(f"Markdown Content of url {url}")
    else:
      response = io.error(NotFound(url))
    return io.defer(print, f"Opening url {url}").then(response)

class LocalFileSytemService(Service):
  def get(self, path: str) -> IO[None,NotFound,str]:
    url = f"/{path}"

    if path == "index.html":
      response = io.pure(f"HTML Content of file {url}")
    elif path == "index.md":
      response = io.pure(f"Markdown Content of file {url}")
    else:
      response = io.error(NotFound(url))
    return io.defer(print, f"Opening file {url}").then(response)


main : IO[Service,NotFound,List[str]] = (
  io.read
  .flat_map(lambda service:
    service.get("index.html")
    .flat_map(lambda x:
      service.get("index.md")
      .flat_map(lambda y: io.defer(print, "Result = ", [x,y]))
    )
  )
)

if len(sys.argv) >= 2 and sys.argv[1] == "http":
  service = HttpService("localhost", 80)
else:
  service = LocalFileSytemService()

main.run(service)

Running the program with the HTTPService gives:

$ python dependency_injection.py http
Opening url http://localhost:80/index.html
Opening url http://localhost:80/index.md
Result =  ['HTML Content of url http://localhost:80/index.html', 'Markdown Content of url http://localhost:80/index.md']

While running it with the LocalFileSytemService gives:

$ python dependency_injection.py localfs
Opening file /index.html
Opening file /index.md
Result =  ['HTML Content of file /index.html', 'Markdown Content of file /index.md']

Once again, main is still a very short IO, so it may be not trivial how much the context is a life saver. But imagine the main IO to be one of your real program. It would be much bigger, and passing the context as global or local variables would be a much bigger problem.