Skip to content

WIP: LazyIO #800

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
@@ -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
4 changes: 2 additions & 2 deletions returns/contrib/mypy/_consts.py
Original file line number Diff line number Diff line change
@@ -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',
5 changes: 3 additions & 2 deletions returns/contrib/pytest/plugin.py
Original file line number Diff line number Diff line change
@@ -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()
@@ -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 (
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
230 changes: 16 additions & 214 deletions returns/io.py → returns/io/ioresult.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
from abc import ABCMeta
from functools import wraps
from inspect import FrameInfo
from typing import Any, Callable, ClassVar, List, Optional, Type, TypeVar, Union
from typing import (
TYPE_CHECKING,
Any,
Callable,
ClassVar,
List,
Optional,
Type,
TypeVar,
Union,
)

from typing_extensions import final

from returns.interfaces.specific import io, ioresult
from returns.interfaces.specific import ioresult
from returns.primitives.container import BaseContainer, container_equality
from returns.primitives.hkt import (
Kind1,
Kind2,
SupportsKind1,
SupportsKind2,
dekind,
)
from returns.primitives.hkt import Kind2, SupportsKind2, dekind
from returns.result import Failure, Result, Success

if TYPE_CHECKING:
from returns.io.io import IO

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

@@ -28,211 +35,6 @@
_SecondType = TypeVar('_SecondType')


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


# IO and Result:

class IOResult(
88 changes: 88 additions & 0 deletions returns/io/lazyio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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
from returns.io.io import IO

_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 LazyIO(
BaseContainer,
SupportsKind1['LazyIO', _ValueType],
io.IOLike1[_ValueType],
):
"""
"""

_inner_value: Callable[['LazyIO'], IO[_ValueType]]

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

def __init__(self, inner_value: Callable[[], IO[_ValueType]]) -> None:
"""
Public constructor for this type. Also required for typing.
.. code:: python
>>> from returns.io import LazyIO
>>> assert LazyIO(lambda: 1)() == 1
"""
super().__init__(inner_value)

def __call__(self) -> IO[_ValueType]:
"""
Executes the wrapped ``IO`` action.
"""
return self._inner_value()

def map( # noqa: WPS125
self,
function: Callable[[_ValueType], _NewValueType],
) -> 'LazyIO[_NewValueType]':
return LazyIO(lambda: IO(function(self()._inner_value)))

def apply(
self,
container: Kind1['LazyIO', Callable[[_ValueType], _NewValueType]],
) -> 'LazyIO[_NewValueType]':
return self.map(dekind(container)()._inner_value) # noqa: WPS437

def bind(
self,
function: Callable[[_ValueType], Kind1['LazyIO', _NewValueType]],
) -> 'LazyIO[_NewValueType]':
return function(self()._inner_value)



# Helper functions:

def impure_lazy(
function: Callable[..., _NewValueType],
) -> Callable[..., LazyIO[_NewValueType]]:
"""
"""
@wraps(function)
def decorator(*args, **kwargs):
return LazyIO(lambda: IO(function(*args, **kwargs)))
return decorator
Empty file added returns/io/lazyio_result.py
Empty file.
4 changes: 2 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -54,10 +54,10 @@ per-file-ignores =
# We allow reexport:
returns/pointfree/__init__.py: F401, WPS201
returns/methods/__init__.py: F401, WPS201
returns/pipeline.py: F401
returns/context/__init__.py: F401, WPS201
returns/io/__init__.py: F401
returns/pipeline.py: F401
# Disable some quality checks for the most heavy parts:
returns/io.py: WPS402
returns/iterables.py: WPS234
# Interfaces and asserts can have assert statements:
returns/interfaces/*.py: S101