Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: LazyIO #800

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ See [0Ver](https://0ver.org/).

## 0.16.0 WIP

## Features

- Adds `LazyIO` container
- Marks `IO` as `@final`

### Misc

- Makes `_Nothing` a singleton
Expand Down
4 changes: 2 additions & 2 deletions returns/contrib/mypy/_consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
#: Set of full names of our decorators.
TYPED_DECORATORS: Final = frozenset((
'returns.result.safe',
'returns.io.impure',
'returns.io.impure_safe',
'returns.io.io.impure',
'returns.io.ioresult.impure_safe',
'returns.maybe.maybe',
'returns.future.future',
'returns.future.asyncify',
Expand Down
5 changes: 3 additions & 2 deletions returns/contrib/pytest/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ def _trace_function(
arg: Any,
) -> None:
is_desired_type_call = (
event == 'call' and frame.f_code is trace_type.__code__
event == 'call' and
frame.f_code is trace_type.__code__
)
if is_desired_type_call:
current_call_stack = inspect.stack()
Expand All @@ -152,7 +153,7 @@ def containers_to_patch(cls) -> tuple:
RequiresContextFutureResult,
)
from returns.future import FutureResult
from returns.io import _IOFailure, _IOSuccess
from returns.io.ioresult import _IOFailure, _IOSuccess
from returns.result import _Failure, _Success

return (
Expand Down
7 changes: 7 additions & 0 deletions returns/io/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from returns.io.io import IO as IO
from returns.io.io import impure as impure
from returns.io.ioresult import IOFailure as IOFailure
from returns.io.ioresult import IOResult as IOResult
from returns.io.ioresult import IOResultE as IOResultE
from returns.io.ioresult import IOSuccess as IOSuccess
from returns.io.ioresult import impure_safe as impure_safe
228 changes: 228 additions & 0 deletions returns/io/io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
from functools import wraps
from typing import TYPE_CHECKING, Callable, TypeVar

from typing_extensions import final

from returns.interfaces.specific import io
from returns.primitives.container import BaseContainer, container_equality
from returns.primitives.hkt import Kind1, SupportsKind1, dekind

if TYPE_CHECKING:
from returns.io.ioresult import IOResult

_ValueType = TypeVar('_ValueType', covariant=True)
_NewValueType = TypeVar('_NewValueType')

# Result related:
_ErrorType = TypeVar('_ErrorType', covariant=True)
_NewErrorType = TypeVar('_NewErrorType')

# Helpers:
_FirstType = TypeVar('_FirstType')
_SecondType = TypeVar('_SecondType')


@final
class IO(
BaseContainer,
SupportsKind1['IO', _ValueType],
io.IOLike1[_ValueType],
):
"""
Explicit container for impure function results.

We also sometimes call it "marker" since once it is marked,
it cannot be ever unmarked.
There's no way to directly get its internal value.

Note that ``IO`` represents a computation that never fails.

Examples of such computations are:

- read / write to localStorage
- get the current time
- write to the console
- get a random number

Use ``IOResult[...]`` for operations that might fail.
Like DB access or network operations.

See also:
- https://dev.to/gcanti/getting-started-with-fp-ts-io-36p6
- https://gist.github.com/chris-taylor/4745921

"""

_inner_value: _ValueType

#: Typesafe equality comparison with other `Result` objects.
equals = container_equality

def __init__(self, inner_value: _ValueType) -> None:
"""
Public constructor for this type. Also required for typing.

.. code:: python

>>> from returns.io import IO
>>> assert str(IO(1)) == '<IO: 1>'

"""
super().__init__(inner_value)

def map( # noqa: WPS125
self,
function: Callable[[_ValueType], _NewValueType],
) -> 'IO[_NewValueType]':
"""
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.

.. code:: python

>>> def mappable(string: str) -> str:
... return string + 'b'

>>> assert IO('a').map(mappable) == IO('ab')

"""
return IO(function(self._inner_value))

def apply(
self,
container: Kind1['IO', Callable[[_ValueType], _NewValueType]],
) -> 'IO[_NewValueType]':
"""
Calls a wrapped function in a container on this container.

.. code:: python

>>> from returns.io import IO
>>> assert IO('a').apply(IO(lambda inner: inner + 'b')) == IO('ab')

Or more complex example that shows how we can work
with regular functions and multiple ``IO`` arguments:

.. code:: python

>>> from returns.curry import curry

>>> @curry
... def appliable(first: str, second: str) -> str:
... return first + second

>>> assert IO('b').apply(IO('a').apply(IO(appliable))) == IO('ab')

"""
return self.map(dekind(container)._inner_value) # noqa: WPS437

def bind(
self,
function: Callable[[_ValueType], Kind1['IO', _NewValueType]],
) -> 'IO[_NewValueType]':
"""
Applies 'function' to the result of a previous calculation.

'function' should accept a single "normal" (non-container) argument
and return ``IO`` type object.

.. code:: python

>>> def bindable(string: str) -> IO[str]:
... return IO(string + 'b')

>>> assert IO('a').bind(bindable) == IO('ab')

"""
return dekind(function(self._inner_value))

#: Alias for `bind` method. Part of the `IOLikeN` interface.
bind_io = bind

@classmethod
def from_value(cls, inner_value: _NewValueType) -> 'IO[_NewValueType]':
"""
Unit function to construct new ``IO`` values.

Is the same as regular constructor:

.. code:: python

>>> from returns.io import IO
>>> assert IO(1) == IO.from_value(1)

Part of the :class:`returns.interfaces.applicative.ApplicativeN`
interface.
"""
return IO(inner_value)

@classmethod
def from_io(cls, inner_value: 'IO[_NewValueType]') -> 'IO[_NewValueType]':
"""
Unit function to construct new ``IO`` values from existing ``IO``.

.. code:: python

>>> from returns.io import IO
>>> assert IO(1) == IO.from_io(IO(1))

Part of the :class:`returns.interfaces.specific.IO.IOLikeN` interface.

"""
return inner_value

@classmethod
def from_ioresult(
cls,
inner_value: 'IOResult[_NewValueType, _NewErrorType]',
) -> 'IO[Result[_NewValueType, _NewErrorType]]':
"""
Converts ``IOResult[a, b]`` back to ``IO[Result[a, b]]``.

Can be really helpful for composition.

.. code:: python

>>> from returns.io import IO, IOSuccess
>>> from returns.result import Success
>>> assert IO.from_ioresult(IOSuccess(1)) == IO(Success(1))

Is the reverse of :meth:`returns.io.IOResult.from_typecast`.
"""
return IO(inner_value._inner_value) # noqa: WPS437


# Helper functions:

def impure(
function: Callable[..., _NewValueType],
) -> Callable[..., IO[_NewValueType]]:
"""
Decorator to mark function that it returns :class:`~IO` container.

If you need to mark ``async`` function as impure,
use :func:`returns.future.future` instead.
This decorator only works with sync functions. Example:

.. code:: python

>>> from returns.io import IO, impure

>>> @impure
... def function(arg: int) -> int:
... return arg + 1 # this action is pure, just an example
...

>>> assert function(1) == IO(2)

Requires our :ref:`mypy plugin <mypy-plugins>`.

"""
@wraps(function)
def decorator(*args, **kwargs):
return IO(function(*args, **kwargs))
return decorator
Loading