| Name | donotation JSON |
| Version |
0.0.8
JSON |
| download |
| home_page | None |
| Summary | A library that introduces the Haskell-like do notation using a Python decorator. |
| upload_time | 2024-08-13 14:54:29 |
| maintainer | None |
| docs_url | None |
| author | None |
| requires_python | >=3.12 |
| license | None |
| keywords |
|
| VCS |
 |
| bugtrack_url |
|
| requirements |
No requirements were recorded.
|
| Travis-CI |
No Travis.
|
| coveralls test coverage |
No coveralls.
|
# Do-notation
Do-notation is a Python package that introduces Haskell-like do notation using a Python decorator.
## Features
* Haskell-like Behavior: Emulate Haskell's do notation for Python objects that implement the `flat_map` (or `bind`) method.
* Syntactic sugar: Use the `do` decorator to convert generator functions into nested `flat_map` method calls by modifying the Abstract Syntax Tree (AST).
* Simplified Syntax: Write complex monadic `flat_map` sequences in a clean and readable way without needing to define auxillary functions.
## Installation
You can install Do-notation using pip:
```
pip install donotation
```
## Usage
### Basic Example
First, import the `do` decorator from the do-notation package. Then, define a class implementing the `flat_map` method to represent the monadic operations. Finally, use the `do` decorator on the generator function that yields objects of this class.
``` python
from donotation import do
class StateMonad:
def __init__(self, func):
self.func = func
def flat_map(self, func):
def next(state):
n_state, value = self.func(state)
return func(value).func(n_state)
return StateMonad(func=next)
def collect_even_numbers(num: int):
def func(state: set):
if num % 2 == 0:
state = state | {num}
return state, num
return StateMonad(func)
@do()
def example(init):
x = yield collect_even_numbers(init + 1)
y = yield collect_even_numbers(x + 1)
z = yield collect_even_numbers(y + 1)
# The generator function must return a `StateMonad` rather than the
# containerized value itself (unlike in other do-notation implementations).
return collect_even_numbers(z + 1)
state = set[int]()
state, value = example(3).func(state)
print(f'{value=}') # Output will be value=7
print(f'{state=}') # Output will be state={4, 6}
```
In this example, we define a `StateMonad` class that implements a `flat_map` method to represent a state monad.
The helper method `collect_even_numbers` is used to generate a sequence of monadic operations within the generator function `example`, which stores the immediate values if they are even integer.
The `do` decorator converts the generator function `example` into a sequence of `flat_map` calls on the `StateMonad` objects.
### How It Works
The `do` decorator works by substituting the `yield` and `yield from` statements with nested `flat_map` calls using the Abstract Syntax Tree (AST) of the generator function. Here’s a breakdown of the process:
1. AST traversal: Traverse the AST of the generator function to inspect all statements.
2. Yield operation: When an yield operations is encountered, define an nested function containing the remaining statements. This nested function is then called within the `flat_map` method call.
3. If-else statements: If an if-else statement is encountered, traverse its AST to inspect all statements. If an yield statement is found, the nested function for the `flat_map` method includes the rest of the if-else statement and the remaining statements of the generator function.
The above example is conceptually translated into the following nested `flat_map` calls:
``` python
def example_translated(init):
return collect_even_numbers(init + 1).flat_map(lambda x:
collect_even_numbers(x + 1).flat_map(lambda y:
collect_even_numbers(y + 1).flat_map(lambda z:
collect_even_numbers(z + 1)
)
)
)
```
<!-- This translation shows how each yield in the generator function corresponds to a `flat_map` call that takes a lambda function, chaining the monadic operations together. -->
## Yield Placement Restrictions
The yield operations within the generator can only be placed within if-else statements but not within for or while statements. Yield statements within the for or while statement are not substituted by a monadic `flat_map` chaining, resulting in a generator function due to the leftover yield statements. In this case, an exception is raised.
### Good Example
Here’s a good example where the yield statement is only placed within if-else statements:
``` python
@do()
def good_example(init):
if condition:
x = yield collect_even_numbers(init)
else:
x = yield collect_even_numbers(init + 1)
y = yield collect_even_numbers(x + 1)
return collect_even_numbers(y + 1)
result = good_example(3)
```
### Bad Example
Here’s a bad example where the yield statement is placed within a for or while statement:
``` python
@do()
def bad_example(init):
x = init
for _ in range(3):
x = yield collect_even_numbers(x)
return collect_even_numbers(x + 1)
# This will raise an exception due to improper yield placement
result = bad_example(3)
```
## Customization
The `do` decorator can be customized to work with different implementations of the flat map operation.
There are two ways to change the bheavior of the `do` decorator:
### Custom Mehtod Name:
If the method is called "bind" instead of "flat_map", you can specify the method name when creating the decorator instance:
``` python
my_do = do(attr='bind')
@my_do() # converts the generator function to nested `bind` method calls
def example():
# ...
```
### External Flat Map Function:
If the flat map operation is defined as an external function rather than a method of the class, you can define a callback function:
``` python
flat_map = ... # some implementation of the flat map operation
def callback(source, fn):
return flat_map(source, fn)
my_do = do(callback=callback)
@my_do() # calls the callback to perform a flat map operation
def example():
# ...
```
In both cases, the `do` decorator adapts to the specified method name or external function, allowing for flexible integration with different monadic structures.
<!-- ## Decorator Implementation
Here is the pseudo-code of the `do` decorator:
``` python
def do(fn):
def wrapper(*args, **kwargs):
gen = fn(*args, **kwargs)
def send_and_yield(value):
try:
next_val = gen.send(value)
except StopIteration as e:
result = e.value
else:
result = next_val.flat_map(send_and_yield)
return result
return send_and_yield(None)
return wrapper
```
The provided code is a pseudo-code implementation that illustrates the core concept of the `do` decorator.
The main difference between this pseudo-code and the actual implementation is that the function given to the `flat_map` method (i.e. `send_and_yield`) can only be called once in the pseudo-code, whereas in the real implementation, that function can be called arbitrarily many times.
This distinction is crucial for handling monadic operations correctly and ensuring that the `do` decorator works as expected in various scenarios. -->
<!-- ### Translating a Generator Function to nested `flat_map` Calls
To better understand how the `do` decorator translates a generator function into a nested sequence of `flat_map` calls, let's consider the following example function:
``` python
@do()
def example():
x = yield Monad(1)
y = yield Monad(x + 1)
z = yield Monad(y + 1)
return Monad(z + 1)
```
The above function is conceptually translated into the following nested `flat_map` calls:
``` python
def example_translated():
return Monad(1).flat_map(lambda x:
Monad(x + 1).flat_map(lambda y:
Monad(y + 1).flat_map(lambda z:
Monad(z + 1)
)
)
)
```
This translation shows how each yield in the generator function corresponds to a `flat_map` call that takes a lambda function, chaining the monadic operations together. -->
## Type hints
When using the `yield` operator, type checkers cannot infer the correct types for the values returned by it. In the basic example above, a type checker like Pyright may infer `Unknown` for the variables `x`, `y`, and `z`, even though they should be of type `int`.
To address this issue, you can use the `yield from` operator instead of `yield`. The `yield from` operator can be better supported by type checkers, ensuring that the correct types are inferred. To make this work properly, you need to annotate the return type of the `__iter__` method in the monadic class (e.g., `StateMonad`).
Here’s how to set it up:
``` python
from __future__ import annotations
from typing import Callable, Generator
from donotation import do
class StateMonad[S, T]:
def __init__(self, func: Callable[[S], tuple[S, T]]):
self.func = func
# Specifies the return type of the `yield from` operator
def __iter__(self) -> Generator[None, None, T]: ...
def flat_map[U](self, func: Callable[[T], StateMonad[S, U]]):
def next(state):
n_state, value = self.func(state)
return func(value).func(n_state)
return StateMonad(func=next)
@do()
def example(init):
x: int = yield from collect_even_numbers(init+1)
y: int = yield from collect_even_numbers(x+1)
z: int = yield from collect_even_numbers(y+1)
return collect_even_numbers(z+1)
# Correct type hint is inferred
m: StateMonad[int] = example(3)
```
Furthermore, if you are using `flat_map` as the monadic method name, you can use `do_typed` to ensure that the returned object correctly implements the `flat_map` method.
In the example below, type checking fails because the integer `1` does not implement a `flat_map` method.
Instead, the generator function should return a `StateMonad` object.
``` python
from donotation import do_typed
# This will fail type checking since the return value does not implement `flat_map`
@do_typed
def example():
return 1
```
## Limitations
### Local variables
Local variables defined after the point where the `do` decorator is applied to the genertor function cannot be accessed within the generator function.
The following example raises a `NamedError` exception.
``` python
x = 1
@do()
def apply_write():
# NameError: name 'y' is not defined
return Writer(x + y, f'adding {x} and {y}')
y = 2
```
## References
Here are some other Python libraries that implement the do-notation:
* [https://github.com/jasondelaat/pymonad](https://github.com/jasondelaat/pymonad)
* [https://github.com/dry-python/returns](https://github.com/dry-python/returns)
* [https://github.com/TRCYX/py_monad_do](https://github.com/TRCYX/py_monad_do)
* [https://github.com/dbrattli/Expression](https://github.com/dbrattli/Expression)
These libaries implement the `do` decorator as a real generator, similar to the following pseudo-code:
``` python
def do(fn):
def wrapper(*args, **kwargs):
gen = fn(*args, **kwargs)
def send_and_yield(value):
try:
next_val = gen.send(value)
except StopIteration as e:
result = e.value
else:
result = next_val.flat_map(send_and_yield)
return result
return send_and_yield(None)
return wrapper
```
This implementation has the disadvantage that each function given to the `flat_map` method (i.e. `send_and_yield`) can only be called once due to a the instruction pointer of the generator.
This difference is crucial for handling monadic operations correctly and ensuring that the `do` decorator works as expected in various scenarios.
Raw data
{
"_id": null,
"home_page": null,
"name": "donotation",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.12",
"maintainer_email": null,
"keywords": null,
"author": null,
"author_email": "Michael Schneeberger <michael.schneeb@hotmail.com>",
"download_url": "https://files.pythonhosted.org/packages/3b/93/c36284fb105afa5f966b868db9d9e188922617ac48e9defb2e44222d0816/donotation-0.0.8.tar.gz",
"platform": null,
"description": "# Do-notation\n\nDo-notation is a Python package that introduces Haskell-like do notation using a Python decorator. \n\n## Features\n\n* Haskell-like Behavior: Emulate Haskell's do notation for Python objects that implement the `flat_map` (or `bind`) method.\n* Syntactic sugar: Use the `do` decorator to convert generator functions into nested `flat_map` method calls by modifying the Abstract Syntax Tree (AST).\n* Simplified Syntax: Write complex monadic `flat_map` sequences in a clean and readable way without needing to define auxillary functions.\n\n## Installation\n\nYou can install Do-notation using pip:\n\n```\npip install donotation\n```\n\n## Usage\n\n### Basic Example\n\nFirst, import the `do` decorator from the do-notation package. Then, define a class implementing the `flat_map` method to represent the monadic operations. Finally, use the `do` decorator on the generator function that yields objects of this class.\n\n``` python\nfrom donotation import do\n\nclass StateMonad:\n def __init__(self, func):\n self.func = func\n\n def flat_map(self, func):\n def next(state):\n n_state, value = self.func(state)\n return func(value).func(n_state)\n\n return StateMonad(func=next)\n\ndef collect_even_numbers(num: int):\n def func(state: set):\n if num % 2 == 0:\n state = state | {num}\n\n return state, num\n return StateMonad(func)\n\n@do()\ndef example(init):\n x = yield collect_even_numbers(init + 1)\n y = yield collect_even_numbers(x + 1)\n z = yield collect_even_numbers(y + 1)\n\n # The generator function must return a `StateMonad` rather than the \n # containerized value itself (unlike in other do-notation implementations).\n return collect_even_numbers(z + 1)\n\nstate = set[int]()\nstate, value = example(3).func(state)\n\nprint(f'{value=}') # Output will be value=7\nprint(f'{state=}') # Output will be state={4, 6}\n```\n\nIn this example, we define a `StateMonad` class that implements a `flat_map` method to represent a state monad.\nThe helper method `collect_even_numbers` is used to generate a sequence of monadic operations within the generator function `example`, which stores the immediate values if they are even integer.\nThe `do` decorator converts the generator function `example` into a sequence of `flat_map` calls on the `StateMonad` objects. \n\n\n### How It Works\n\nThe `do` decorator works by substituting the `yield` and `yield from` statements with nested `flat_map` calls using the Abstract Syntax Tree (AST) of the generator function. Here\u2019s a breakdown of the process:\n\n1. AST traversal: Traverse the AST of the generator function to inspect all statements.\n2. Yield operation: When an yield operations is encountered, define an nested function containing the remaining statements. This nested function is then called within the `flat_map` method call.\n3. If-else statements: If an if-else statement is encountered, traverse its AST to inspect all statements. If an yield statement is found, the nested function for the `flat_map` method includes the rest of the if-else statement and the remaining statements of the generator function.\n\nThe above example is conceptually translated into the following nested `flat_map` calls:\n\n``` python\ndef example_translated(init):\n return collect_even_numbers(init + 1).flat_map(lambda x: \n collect_even_numbers(x + 1).flat_map(lambda y: \n collect_even_numbers(y + 1).flat_map(lambda z: \n collect_even_numbers(z + 1)\n )\n )\n )\n```\n\n<!-- This translation shows how each yield in the generator function corresponds to a `flat_map` call that takes a lambda function, chaining the monadic operations together. -->\n\n## Yield Placement Restrictions\n\nThe yield operations within the generator can only be placed within if-else statements but not within for or while statements. Yield statements within the for or while statement are not substituted by a monadic `flat_map` chaining, resulting in a generator function due to the leftover yield statements. In this case, an exception is raised.\n\n### Good Example\n\nHere\u2019s a good example where the yield statement is only placed within if-else statements:\n\n``` python\n@do()\ndef good_example(init):\n if condition:\n x = yield collect_even_numbers(init)\n else:\n x = yield collect_even_numbers(init + 1)\n y = yield collect_even_numbers(x + 1)\n return collect_even_numbers(y + 1)\n\nresult = good_example(3)\n```\n\n### Bad Example\n\nHere\u2019s a bad example where the yield statement is placed within a for or while statement:\n\n``` python\n@do()\ndef bad_example(init):\n x = init\n for _ in range(3):\n x = yield collect_even_numbers(x)\n return collect_even_numbers(x + 1)\n\n# This will raise an exception due to improper yield placement\nresult = bad_example(3)\n```\n\n## Customization\n\nThe `do` decorator can be customized to work with different implementations of the flat map operation.\nThere are two ways to change the bheavior of the `do` decorator:\n\n### Custom Mehtod Name:\n\nIf the method is called \"bind\" instead of \"flat_map\", you can specify the method name when creating the decorator instance:\n\n``` python\nmy_do = do(attr='bind')\n\n@my_do() # converts the generator function to nested `bind` method calls\ndef example():\n # ...\n```\n\n### External Flat Map Function:\n\nIf the flat map operation is defined as an external function rather than a method of the class, you can define a callback function:\n\n``` python\nflat_map = ... # some implementation of the flat map operation\n\ndef callback(source, fn):\n return flat_map(source, fn)\n\nmy_do = do(callback=callback)\n\n@my_do() # calls the callback to perform a flat map operation\ndef example():\n # ...\n```\n\nIn both cases, the `do` decorator adapts to the specified method name or external function, allowing for flexible integration with different monadic structures.\n\n\n<!-- ## Decorator Implementation\n\nHere is the pseudo-code of the `do` decorator:\n\n``` python\ndef do(fn):\n def wrapper(*args, **kwargs):\n gen = fn(*args, **kwargs)\n\n def send_and_yield(value):\n try:\n next_val = gen.send(value)\n except StopIteration as e:\n result = e.value\n else:\n result = next_val.flat_map(send_and_yield)\n return result\n\n return send_and_yield(None)\n return wrapper\n```\n\nThe provided code is a pseudo-code implementation that illustrates the core concept of the `do` decorator. \nThe main difference between this pseudo-code and the actual implementation is that the function given to the `flat_map` method (i.e. `send_and_yield`) can only be called once in the pseudo-code, whereas in the real implementation, that function can be called arbitrarily many times.\nThis distinction is crucial for handling monadic operations correctly and ensuring that the `do` decorator works as expected in various scenarios. -->\n\n\n<!-- ### Translating a Generator Function to nested `flat_map` Calls\n\nTo better understand how the `do` decorator translates a generator function into a nested sequence of `flat_map` calls, let's consider the following example function:\n\n``` python\n@do()\ndef example():\n x = yield Monad(1)\n y = yield Monad(x + 1)\n z = yield Monad(y + 1)\n return Monad(z + 1)\n```\n\nThe above function is conceptually translated into the following nested `flat_map` calls:\n\n``` python\ndef example_translated():\n return Monad(1).flat_map(lambda x: \n Monad(x + 1).flat_map(lambda y: \n Monad(y + 1).flat_map(lambda z: \n Monad(z + 1)\n )\n )\n )\n```\n\nThis translation shows how each yield in the generator function corresponds to a `flat_map` call that takes a lambda function, chaining the monadic operations together. -->\n\n## Type hints\n\nWhen using the `yield` operator, type checkers cannot infer the correct types for the values returned by it. In the basic example above, a type checker like Pyright may infer `Unknown` for the variables `x`, `y`, and `z`, even though they should be of type `int`.\n\nTo address this issue, you can use the `yield from` operator instead of `yield`. The `yield from` operator can be better supported by type checkers, ensuring that the correct types are inferred. To make this work properly, you need to annotate the return type of the `__iter__` method in the monadic class (e.g., `StateMonad`).\n\nHere\u2019s how to set it up:\n\n``` python\nfrom __future__ import annotations\nfrom typing import Callable, Generator\nfrom donotation import do\n\nclass StateMonad[S, T]:\n def __init__(self, func: Callable[[S], tuple[S, T]]):\n self.func = func\n\n # Specifies the return type of the `yield from` operator\n def __iter__(self) -> Generator[None, None, T]: ...\n\n def flat_map[U](self, func: Callable[[T], StateMonad[S, U]]):\n def next(state):\n n_state, value = self.func(state)\n return func(value).func(n_state)\n\n return StateMonad(func=next)\n\n@do()\ndef example(init):\n x: int = yield from collect_even_numbers(init+1)\n y: int = yield from collect_even_numbers(x+1)\n z: int = yield from collect_even_numbers(y+1)\n return collect_even_numbers(z+1)\n\n# Correct type hint is inferred\nm: StateMonad[int] = example(3)\n```\n\nFurthermore, if you are using `flat_map` as the monadic method name, you can use `do_typed` to ensure that the returned object correctly implements the `flat_map` method. \n\nIn the example below, type checking fails because the integer `1` does not implement a `flat_map` method.\nInstead, the generator function should return a `StateMonad` object.\n\n``` python\nfrom donotation import do_typed\n\n# This will fail type checking since the return value does not implement `flat_map`\n@do_typed\ndef example():\n return 1\n```\n\n## Limitations\n\n### Local variables\n\nLocal variables defined after the point where the `do` decorator is applied to the genertor function cannot be accessed within the generator function.\nThe following example raises a `NamedError` exception.\n\n``` python\nx = 1\n\n@do()\ndef apply_write():\n # NameError: name 'y' is not defined\n return Writer(x + y, f'adding {x} and {y}')\n\ny = 2\n```\n\n\n## References\n\nHere are some other Python libraries that implement the do-notation:\n\n* [https://github.com/jasondelaat/pymonad](https://github.com/jasondelaat/pymonad)\n* [https://github.com/dry-python/returns](https://github.com/dry-python/returns)\n* [https://github.com/TRCYX/py_monad_do](https://github.com/TRCYX/py_monad_do)\n* [https://github.com/dbrattli/Expression](https://github.com/dbrattli/Expression)\n\nThese libaries implement the `do` decorator as a real generator, similar to the following pseudo-code:\n\n``` python\ndef do(fn):\n def wrapper(*args, **kwargs):\n gen = fn(*args, **kwargs)\n\n def send_and_yield(value):\n try:\n next_val = gen.send(value)\n except StopIteration as e:\n result = e.value\n else:\n result = next_val.flat_map(send_and_yield)\n return result\n\n return send_and_yield(None)\n return wrapper\n```\n\nThis implementation has the disadvantage that each function given to the `flat_map` method (i.e. `send_and_yield`) can only be called once due to a the instruction pointer of the generator.\nThis difference is crucial for handling monadic operations correctly and ensuring that the `do` decorator works as expected in various scenarios.\n",
"bugtrack_url": null,
"license": null,
"summary": "A library that introduces the Haskell-like do notation using a Python decorator.",
"version": "0.0.8",
"project_urls": {
"Homepage": "https://github.com/MichaelSchneeberger/donotation"
},
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "710b7a8b5b35551246fe5cbecd828f64102b42c74e99fc5e6097121a5dc6c1d7",
"md5": "a65ddaf05d3dbdf95ae8e1445495f33b",
"sha256": "09b551aaa7a5838f339962e0a15bb7fad2355da2c4ba9da5b2e1b57e9fe13086"
},
"downloads": -1,
"filename": "donotation-0.0.8-py3-none-any.whl",
"has_sig": false,
"md5_digest": "a65ddaf05d3dbdf95ae8e1445495f33b",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.12",
"size": 10905,
"upload_time": "2024-08-13T14:54:27",
"upload_time_iso_8601": "2024-08-13T14:54:27.988246Z",
"url": "https://files.pythonhosted.org/packages/71/0b/7a8b5b35551246fe5cbecd828f64102b42c74e99fc5e6097121a5dc6c1d7/donotation-0.0.8-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "3b93c36284fb105afa5f966b868db9d9e188922617ac48e9defb2e44222d0816",
"md5": "707c78606cc77576b70409b55a698b36",
"sha256": "b6c747feb5fedbfcab2a016e83ede6fdd8e87336070b93f3bbcc5c35571ea831"
},
"downloads": -1,
"filename": "donotation-0.0.8.tar.gz",
"has_sig": false,
"md5_digest": "707c78606cc77576b70409b55a698b36",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.12",
"size": 11276,
"upload_time": "2024-08-13T14:54:29",
"upload_time_iso_8601": "2024-08-13T14:54:29.388281Z",
"url": "https://files.pythonhosted.org/packages/3b/93/c36284fb105afa5f966b868db9d9e188922617ac48e9defb2e44222d0816/donotation-0.0.8.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-08-13 14:54:29",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "MichaelSchneeberger",
"github_project": "donotation",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"lcname": "donotation"
}