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)