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
requests mocks for different outcomes
and the whole Universe!
Our complexity has sky-rocketed!
And the most annoying part is that all other functions
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.
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
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
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:
IO container also needs to be explicitly
mapped to produce new
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
@impure for better readability and clearness:
import requests from returns.io import impure @impure def get_user() -> 'User': return requests.get('https:...').json()
What kind of input parameter should
my function accept
IO[T] or simple
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
Most web applications are just covered with
As we state in Composition docs we allow to compose different containers together.
and sticking to the single version allows better composition.
The same rule is applied to
Maybe and all other containers we have.
IO at the top level is easier
because you can
join things easily.
And other containers not always make sense.
If some operation performs
IO it should mark all internals.
Our design decision was not let people unwrap
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.
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.
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.
Applies function to the inner value.
Applies ‘function’ to the contents of the IO instance and returns a new IO object containing the result. ‘function’ should accept a single “normal” (non-container) argument and return a non-container result.