algae


Namealgae JSON
Version 1.1.0 PyPI version JSON
download
home_pagehttps://github.com/lucaruzzola/algae
SummaryAlgebraic Data Types for Python
upload_time2023-03-17 20:19:28
maintainer
docs_urlNone
authorLuca Ruzzola
requires_python>=3.8,<4.0
license
keywords python adt algebraic types functional programming data structures
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # algae
Algebraic Data Types for Python

![option](docs/option.png)

This repository contains an implementation from scratch of simple algebraic data types in Python. 

The following types are implemented and tested:
 - Either
 - Option
 - Try

This library is inspired by Scala's implementation of these structures.

Feel free to open an issue or [send me an email](mailto:lucaruzzola@gmail.com) 
in case you'd like to contribute or if you see something that can be improved.

## Installation
This project is published on [PyPi](https://pypi.org/project/algae/) as `algae` so you can easily install it with `pip` as:
```shell
pip install algae
```
or with `poetry` as:
```shell
poetry add algae
```

## Setup

### Poetry
This project uses [poetry](https://github.com/python-poetry/poetry) to manage its dependencies.
Please refer to [poetry's official doc](https://python-poetry.org/docs/) for more info.


## Usage Examples

### Either

`Either` represents a value that can assume one of two types.

Concrete instances are of type `Left` or `Right`.

As an example, let's consider the case of making HTTP calls which might return a 
status code representing an error as the url is user-defined. 
If a call is successful, we want to return the JSON from the response, but if it's
not we'll map it to an internal error message.

The examples use this [example server](https://jsonplaceholder.typicode.com).

```python
import requests
from algae.either import Left, Right, Either
from typing import Dict, Any

def map_response_to_msg(response: requests.models.Response):
    return f"The {response.request.method} request to {response.url} couldn't be completed " \
    f"and returned a {response.status_code} status_code"

def call_and_check(url: str) -> Either[str, Dict[Any, Any]]:
    response = requests.get(url)
    return Right(response.json()) if response.ok else Left(map_response_to_msg(response))
```

Users of this method will then be able to further chain operations which can result in 2 different results easily,
keeping track of the error message identifying the step that returned something unexpected in the chain.

```python
base_url = "https://jsonplaceholder.typicode.com"
users_json = call_and_check(f"{base_url}/users")
posts = users_json.flat_map(lambda json: call_and_check(f"{base_url}/posts?userId={json[0]['id']}"))
```

Lastly, we'll log the content of the `Either`at the appropriate level in each case; the contained string in the `Left` 
case at `warn`, or the `msg` field of the JSON dictionary in the `Right` case at `info`.

```python
from logging import getLogger

logger = getLogger()

posts.fold(logger.warning, lambda x: logger.info(x[0]["title"]))
```

The above example enters the `Right` branch of the `Either`, change the `base_url` to `$base_url/pizza` to get a `Left` at the first stage.

Please note that this is different from the case where an `Exception` is raised, which better fits the `Try` structure 
described below.

### Option

`Option` represents an optional value, its concrete instances are 
of type `Nothing` or `Some`.

As an example, let's consider the case of checking for a variable in a dictionary.
Normally, a default value of `None` is returned if the request key is not present in the dictionary,
however this requires the user of method returning such a value to check explicitly the content of the return variable.

Further, multiple calls of this type cannot be chained together, and the value needs to be checked every time.
Using `Option` we can instead reason using the type directly, and demanding to it the checking steps.

```python
from algae.option import Option

d = {"food": "Pizza"}

result = Option.apply(d.get("another_key"))

awesomize = lambda x: x + "is awesome" 

msg = result.map(awesomize)
```

This way we didn't need to check whether the key was present in the dictionary or not.
Finally, we can get a default value to go from an `Option` to a `str`.

```python
msg.fold("Pizza is incredible anyways!", lamdba x: x + ", but fries are good too!")
```

The final `msg` will be `Pizza is incredible anyways!`.

If instead we had looked for the `food` key, `msg` would have been `Pizza is awesome, but fries are good too!`

### Try

`Try` represents a computation that can either fail (raising an Exception) or return the resulting value.

Concrete instances are of type `Failure` or `Success`.

As an example, let's see the case of a function that can raise an Exception:
``` python
import math

def unsafe_computation(value: int):
    return math.log(value)  # this throws an Exception if value is <= 0
```

Upon calling this function with `value` <= 0 we'll see:

```python
unsafe_computation(0)
```
```shell
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: math domain error
```

To make this computation safe, even for `value` <= 0, we'll wrap its execution with Try:
```python
from algae.try_ import Try

safe_result = Try.apply(unsafe_computation, 0)
```

`safe_result` will be of type `Failure`, containing the Exception.
In case it was called on proper input:
```python
safe_result = Try.apply(unsafe_computation, 1)
```

`safe_result` will be of type Success and it will contain the proper result.

Please notice that you need to pass the function and any function arguments, named and not, as arguments to 
`Try.apply()` rather than passing `f(args)`.

Alternatively, you can use this syntax:
```python
safe_result = Try.apply(lambda: unsafe_computation(0))
```

Using `Try`, an appropriate return type can be used for methods that might fail and raise an `Exeception`, 
leaving the user in charge of easily dealing with the subsequent behavior, for example:

```python
Try.apply(unsafe_computation, 1).map(lambda x: x + 1)
```

Special thanks to [David Cuthbert](https://github.com/dacut) for letting me have the "algae" name on PyPI.

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/lucaruzzola/algae",
    "name": "algae",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.8,<4.0",
    "maintainer_email": "",
    "keywords": "python,ADT,algebraic,types,functional,programming,data,structures",
    "author": "Luca Ruzzola",
    "author_email": "lucaruzzola@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/e6/24/49c3e6fe37dd9431d65914be3efaac31df36595b74621b9ed3c8b385f2d5/algae-1.1.0.tar.gz",
    "platform": null,
    "description": "# algae\nAlgebraic Data Types for Python\n\n![option](docs/option.png)\n\nThis repository contains an implementation from scratch of simple algebraic data types in Python. \n\nThe following types are implemented and tested:\n - Either\n - Option\n - Try\n\nThis library is inspired by Scala's implementation of these structures.\n\nFeel free to open an issue or [send me an email](mailto:lucaruzzola@gmail.com) \nin case you'd like to contribute or if you see something that can be improved.\n\n## Installation\nThis project is published on [PyPi](https://pypi.org/project/algae/) as `algae` so you can easily install it with `pip` as:\n```shell\npip install algae\n```\nor with `poetry` as:\n```shell\npoetry add algae\n```\n\n## Setup\n\n### Poetry\nThis project uses [poetry](https://github.com/python-poetry/poetry) to manage its dependencies.\nPlease refer to [poetry's official doc](https://python-poetry.org/docs/) for more info.\n\n\n## Usage Examples\n\n### Either\n\n`Either` represents a value that can assume one of two types.\n\nConcrete instances are of type `Left` or `Right`.\n\nAs an example, let's consider the case of making HTTP calls which might return a \nstatus code representing an error as the url is user-defined. \nIf a call is successful, we want to return the JSON from the response, but if it's\nnot we'll map it to an internal error message.\n\nThe examples use this [example server](https://jsonplaceholder.typicode.com).\n\n```python\nimport requests\nfrom algae.either import Left, Right, Either\nfrom typing import Dict, Any\n\ndef map_response_to_msg(response: requests.models.Response):\n    return f\"The {response.request.method} request to {response.url} couldn't be completed \" \\\n    f\"and returned a {response.status_code} status_code\"\n\ndef call_and_check(url: str) -> Either[str, Dict[Any, Any]]:\n    response = requests.get(url)\n    return Right(response.json()) if response.ok else Left(map_response_to_msg(response))\n```\n\nUsers of this method will then be able to further chain operations which can result in 2 different results easily,\nkeeping track of the error message identifying the step that returned something unexpected in the chain.\n\n```python\nbase_url = \"https://jsonplaceholder.typicode.com\"\nusers_json = call_and_check(f\"{base_url}/users\")\nposts = users_json.flat_map(lambda json: call_and_check(f\"{base_url}/posts?userId={json[0]['id']}\"))\n```\n\nLastly, we'll log the content of the `Either`at the appropriate level in each case; the contained string in the `Left` \ncase at `warn`, or the `msg` field of the JSON dictionary in the `Right` case at `info`.\n\n```python\nfrom logging import getLogger\n\nlogger = getLogger()\n\nposts.fold(logger.warning, lambda x: logger.info(x[0][\"title\"]))\n```\n\nThe above example enters the `Right` branch of the `Either`, change the `base_url` to `$base_url/pizza` to get a `Left` at the first stage.\n\nPlease note that this is different from the case where an `Exception` is raised, which better fits the `Try` structure \ndescribed below.\n\n### Option\n\n`Option` represents an optional value, its concrete instances are \nof type `Nothing` or `Some`.\n\nAs an example, let's consider the case of checking for a variable in a dictionary.\nNormally, a default value of `None` is returned if the request key is not present in the dictionary,\nhowever this requires the user of method returning such a value to check explicitly the content of the return variable.\n\nFurther, multiple calls of this type cannot be chained together, and the value needs to be checked every time.\nUsing `Option` we can instead reason using the type directly, and demanding to it the checking steps.\n\n```python\nfrom algae.option import Option\n\nd = {\"food\": \"Pizza\"}\n\nresult = Option.apply(d.get(\"another_key\"))\n\nawesomize = lambda x: x + \"is awesome\" \n\nmsg = result.map(awesomize)\n```\n\nThis way we didn't need to check whether the key was present in the dictionary or not.\nFinally, we can get a default value to go from an `Option` to a `str`.\n\n```python\nmsg.fold(\"Pizza is incredible anyways!\", lamdba x: x + \", but fries are good too!\")\n```\n\nThe final `msg` will be `Pizza is incredible anyways!`.\n\nIf instead we had looked for the `food` key, `msg` would have been `Pizza is awesome, but fries are good too!`\n\n### Try\n\n`Try` represents a computation that can either fail (raising an Exception) or return the resulting value.\n\nConcrete instances are of type `Failure` or `Success`.\n\nAs an example, let's see the case of a function that can raise an Exception:\n``` python\nimport math\n\ndef unsafe_computation(value: int):\n    return math.log(value)  # this throws an Exception if value is <= 0\n```\n\nUpon calling this function with `value` <= 0 we'll see:\n\n```python\nunsafe_computation(0)\n```\n```shell\nTraceback (most recent call last):\n  File \"<stdin>\", line 1, in <module>\nValueError: math domain error\n```\n\nTo make this computation safe, even for `value` <= 0, we'll wrap its execution with Try:\n```python\nfrom algae.try_ import Try\n\nsafe_result = Try.apply(unsafe_computation, 0)\n```\n\n`safe_result` will be of type `Failure`, containing the Exception.\nIn case it was called on proper input:\n```python\nsafe_result = Try.apply(unsafe_computation, 1)\n```\n\n`safe_result` will be of type Success and it will contain the proper result.\n\nPlease notice that you need to pass the function and any function arguments, named and not, as arguments to \n`Try.apply()` rather than passing `f(args)`.\n\nAlternatively, you can use this syntax:\n```python\nsafe_result = Try.apply(lambda: unsafe_computation(0))\n```\n\nUsing `Try`, an appropriate return type can be used for methods that might fail and raise an `Exeception`, \nleaving the user in charge of easily dealing with the subsequent behavior, for example:\n\n```python\nTry.apply(unsafe_computation, 1).map(lambda x: x + 1)\n```\n\nSpecial thanks to [David Cuthbert](https://github.com/dacut) for letting me have the \"algae\" name on PyPI.\n",
    "bugtrack_url": null,
    "license": "",
    "summary": "Algebraic Data Types for Python",
    "version": "1.1.0",
    "split_keywords": [
        "python",
        "adt",
        "algebraic",
        "types",
        "functional",
        "programming",
        "data",
        "structures"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "bf1f0a03e4f53677ad94e48d887e8e1020b3765e843fd6f19c7131177cc42621",
                "md5": "efc4464a3e922fd2b49acc905a5ec18d",
                "sha256": "287ed39f9b650d8de1056a95ee57ace560086df19ff8087eefe7e2db6b133c30"
            },
            "downloads": -1,
            "filename": "algae-1.1.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "efc4464a3e922fd2b49acc905a5ec18d",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8,<4.0",
            "size": 7101,
            "upload_time": "2023-03-17T20:19:25",
            "upload_time_iso_8601": "2023-03-17T20:19:25.622193Z",
            "url": "https://files.pythonhosted.org/packages/bf/1f/0a03e4f53677ad94e48d887e8e1020b3765e843fd6f19c7131177cc42621/algae-1.1.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "e62449c3e6fe37dd9431d65914be3efaac31df36595b74621b9ed3c8b385f2d5",
                "md5": "b0a18c6e6a0f6f9f31932aacbff15efe",
                "sha256": "a4e75dde2b5c290d6226c6358d58ebcb5a0cc5177326acf362d58d9f87dacd67"
            },
            "downloads": -1,
            "filename": "algae-1.1.0.tar.gz",
            "has_sig": false,
            "md5_digest": "b0a18c6e6a0f6f9f31932aacbff15efe",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8,<4.0",
            "size": 7021,
            "upload_time": "2023-03-17T20:19:28",
            "upload_time_iso_8601": "2023-03-17T20:19:28.007504Z",
            "url": "https://files.pythonhosted.org/packages/e6/24/49c3e6fe37dd9431d65914be3efaac31df36595b74621b9ed3c8b385f2d5/algae-1.1.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-03-17 20:19:28",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "github_user": "lucaruzzola",
    "github_project": "algae",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "algae"
}
        
Elapsed time: 0.04576s