IO
is ugly.
Why? Let me illustrate it with the example.
Imagine we have this beautiful pure function:
def can_book_seats(
number_of_seats: int,
reservation: 'Reservation',
) -> bool:
return reservation.capacity >= number_of_seats + reservation.booked
What’s good about it? We can test it easily. Even without setting up any testing framework, simple doctests will be enough.
This code is beautiful, because it is simple.
We can later use its result to notify users about their booking request:
def notify_user_about_booking_result(is_successful: bool) -> 'MessageID':
...
notify_user_about_booking_result(is_successful) # works just fine!
But, imagine that our requirements had changed. And now we have to grab the number of already booked tickets from some other provider and fetch the maximum capacity from the database:
import requests
import db
def can_book_seats(
number_of_seats: int,
place_id: int,
) -> bool:
capacity = db.get_place_capacity(place_id) # sql query
booked = requests('https://partner.com/api').json()['booked'] # http req
return capacity >= number_of_seats + booked
Now testing this code will become a nightmare! It will require to setup:
real database and tables
fixture data
requests
mocks for different outcomes
and the whole Universe!
Our complexity has sky-rocketed!
And the most annoying part is that all other functions
that call can_book_seats
now also have to do the same setup.
It seams like IO
is indelible mark (some people also call it “effect”).
And at some point it time we will start to mix pure and impure code together.
Well, our IO
mark is indeed indelible and should be respected.
Once you have an IO
operation you can mark it appropriately.
And it infects all other functions that call it.
And impurity becomes explicit:
import requests
import db
from returns.io import IO
def can_book_seats(
number_of_seats: int,
place_id: int,
) -> IO[bool]:
capacity = db.get_place_capacity(place_id) # sql query
booked = requests('https://partner.com/api').json()['booked']
return IO(capacity >= number_of_seats + booked)
Now this function returns IO[bool]
instead of a regular bool
.
It means, that it cannot be used where regular bool
can be:
def notify_user_about_booking_result(is_successful: bool) -> 'MessageID':
...
is_successful: IO[bool] = can_book_seats(number_of_seats, place_id)
notify_user_about_booking_result(is_successful) # Boom!
# => Argument 1 has incompatible type "IO[bool]"; expected "bool"
See? It is now impossible for a pure function to use IO[bool]
.
It is impossible to unwrap or get a value from this container.
Once it is marked as IO
it will never return to the pure state.
Well, there’s a hack actually:
unsafe_perform_io
It also needs to be explicitly mapped to produce new IO
result:
message_id: IO['MessageID'] = can_book_seats(
number_of_seats,
place_id,
).map(
notify_user_about_booking_result,
)
Or it can be annotated to work with impure results:
def notify_user_about_booking_result(
is_successful: IO[bool],
) -> IO['MessageID']:
...
is_successful: IO[bool] = can_book_seats(number_of_seats, place_id)
notify_user_about_booking_result(is_successful) # Works!
Now, all our impurity is explicit. We can track it, we can fight it, we can design it better. By saying that, it is assumed that you have a functional core and imperative shell.
We also have this handy decorator to help you with the existing impure things in Python:
from returns.io import impure
name: IO[str] = impure(input)('What is your name?')
You can also decorate your own functions
with @impure
for better readability and clearness:
import requests
from returns.io import impure
@impure
def get_user() -> 'User':
return requests.get('https:...').json()
Typing will only work correctly if decorator_plugin is used. This happens due to mypy issue.
What kind of input parameter should
my function accept IO[T]
or simple T
?
It really depends on your domain / context.
If the value is pure, than use raw unwrapped values.
If the value is fetched, input, received, selected, than use IO
container.
Most web applications are just covered with IO
.
As we state in Composition docs we allow to compose different containers together.
That’s where this question raises:
should I apply IO
to all Result
or only to the value part?
Short answer: we prefer IO[Result[A, B]]
and sticking to the single version allows better composition.
Long answer. Let’s see these two examples:
from returns.io import IO, impure
from returns.result import Result, safe
def get_user_age() -> Result[IO[int], ValueError]:
# Safe, but impure operation:
prompt: IO[str] = impure(input)("What's your age?")
# Pure, but unsafe operation:
return safe(int)(prompt)
In this case we return Result[IO[int], ValueError]
,
since IO
operation is safe and cannot throw.
import requests
from returns.io import IO, impure
from returns.result import Result, safe
@impure
@safe
def get_user_age() -> IO[Result[int, Exception]]:
# We ask another micro-service, it is impure and can fail:
return requests.get('https://...').json()
In this case the whole result is marked as impure. Since its failures are impure as well.
The similar question is about IO
and Maybe
composition.
Let’s illustrate it with the code example:
from returns.maybe import Maybe, Nothing
from returns.io import IO
def maybe_ask_user(should_ask: bool) -> Maybe[IO[str]]:
if should_ask:
return Maybe.new(IO(input('Asking!')))
return Nothing
In this example IO
might not happen at all.
from returns.maybe import Maybe, Nothing
from returns.io import IO
def ask_user() -> IO[Maybe[str]]:
prompt = input('Asking!')
if prompt:
return Maybe.new(IO(prompt))
return IO(Nothing)
In this second case, we always do IO
, but we return Nothing
if user inputs an empty string
(because we need this business logic for some reason).
Our design decision was not let people unwrap IO
containers,
so it will indeed infect the whole call-stack with its effect.
Otherwise, people might hack the system in some dirty (from our point of view) but valid (from the python’s point of view) ways.
Warning:
Of course, you can directly access
the internal state of the IO with `._internal_state`,
but your are considered to be a grown-up!
Use wemake-python-styleguide to restrict `._` access in your code.
IO
(inner_value)[source]¶Bases: returns.primitives.container.GenericContainerOneSlot
Explicit marker for impure function results.
We call it “marker” since once it is marked, it cannot be unmarked.
IO
is also a container.
But, it is different in a way
that it cannot be unwrapped / rescued / fixed.
There’s no way to directly get its internal value.