donotation


Namedonotation JSON
Version 0.0.8 PyPI version JSON
download
home_pageNone
SummaryA library that introduces the Haskell-like do notation using a Python decorator.
upload_time2024-08-13 14:54:29
maintainerNone
docs_urlNone
authorNone
requires_python>=3.12
licenseNone
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"
}
        
Elapsed time: 0.41701s