Variables
IO is heavily expression based but Python does not allow to define local
variables in expressions. Raffiot provides two types to work around this
limitation: Val and Var. A Val is an immutable variable while a Var is
a mutable variable.
Immutable Variables
Immutable variables can be created very easily by using the class Val:
>>> from raffiot import *
>>> Val(5)
Val(value=5)
Val has most of the usual operations like map, flat_map, zip, etc.
Please have a look the the API for a complete list of
methods.
For example, Python's forbids this syntax:
>>> lambda x:
>>> y = x + 5
>>> print(f"y={y}")
because it only allows lambdas to contain one single expression. To simulate this behaviour, you can use:
>>> lambda x: Val(x+5).map(lambda y: print(f"y={y}"))
We totally agree that the first syntax is far superior, but until Python allows lambda to declare local variables, this is probably the best we can get.
Mutable Variables
Raffiot's mutable variable are not only mutable variables. They are designed
to play nicely with concurrency. Every access to the a Var is exclusive,
which means only one fiber is allowed to read or modify the variable at any
time.
Var.create, Var.create_rs: the only ways to create a mutable variable
Because a Var is not just a value, it can only be created either by the IO
Var.create(a:A) of type IO[Any,None,Var[A]] or the Resource
Var.create(a:A) of type Resource[Any,None,Var[A]]:
>>> from typing import Any
>>> main: IO[Any,None,None] = (
... Var.create(5).flat_map(lambda var1: var1.set(7).then(var1.get()))
... )
>>> main.run(None)
Ok(success=7)
>>> main_rs: Resource[Any,None,None] = (
... Var.create_rs(5).flat_map(lambda var1: var1.set_rs(7).then(var1.get_rs()))
... )
>>> main_rs.use(io.pure).run(None)
Ok(success=7)
As you can see, you can use the methods get/set to get/set the current value
as an IO or get_rs/set_rs to get/set the current value as a Resource.
Alternatively, you can use the method get_and_set and get_and_set_rs to
assign a new value to the variable and get the previous value at the same time.
update: modifying the current value
When you need to update the current value of a variable var:Var[A], use the
update method.
It takes as input a function f: Callable[[A], Tuple[A,B]]. This function will
receive the current value of the variable and must return a pair
(new_value, returned). The new_value will become the new value of the
variable. The value returned serves to output some value if you want to.
>>> main: IO[Any,None,None] = (
... Var.create(5).flat_map(lambda v: v.update(lambda x: (x+1, 2*x)))
... )
>>> main.run(None)
Ok(success=UpdateResult(old_value=5, new_value=6, returned=10))
The functions update_io and update_rs are respectively for IO and
Resource functions.
traverse: creating a new variable from another one.
The traverse methods create a new variable using an existing one:
>>> main: IO[Any,None,None] = (
... Var.create(5)
... .flat_map(lambda var1: var1.traverse(lambda x: io.defer(print, x)
... .then(io.pure(x+1))
... )
... .flat_map(lambda var2: var2.get())
... )
... )
5
Ok(success=6)
zip: Getting the values of a group of variables
The zip class method regroup the values a list of variable. All concurrent
access to the variables are forbidden during the zip to ensure consistent
reading of the variables values:*
>>> main: IO[Any,None,None] = (
... io.zip(Var.create(0), Var.create(1))
... .flat_map(lambda vars:
... Var.zip(vars[0], vars[1])
... )
... )
>>> main.run(None)
Ok(success=[0, 1])
Note: the zip_with instance method is equivalent.
ap: Combining the values of a group of variables
The zip class method regroup the values a list of variable. All concurrent
access to the variables are forbidden during the zip to ensure consistent
reading of the variables values:*
>>> main: IO[Any,None,None] = (
... Var.create(lambda x, y: x + y)
... .flat_map(lambda var_fun:
... io.zip(Var.create(2), Var.create(3))
... .flat_map(lambda vars:
... var_fun.ap(vars[0], vars[1])
... )
... )
... )
>>> main.run(None)
Ok(success=5)