pythonix


Namepythonix JSON
Version 1.1.1 PyPI version JSON
download
home_pagehttps://github.com/jhok2013/pythonix
SummaryRust and Gleam like functional programming in Python, complete with Results, pipes, and currying
upload_time2024-05-23 15:57:21
maintainerNone
docs_urlNone
authorjhok2013
requires_python<4.0,>=3.10
licenseApache-2.0
keywords functional pipes result error-as-values rust gleam
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # PYTHONIX

A collection of functional modules in Python 10 inspired by Gleam and Rust.
If you like functional oriented programming, but have to program in Python, then check this out.

The goal is to take some great features of Rust like strong types, errors by value with Results
and Options, error unpacking like in Go, and some other great features from Gleam like piping with `|>` and classes as
modules, and combine them for use in Python without any external dependencies.

Here are examples of my favorite features:

## Pipes and Pipe Likes

`P` and `Piper` are special wrapper classes the take what's on the left, and put it into the function on the right.

You can take statements like this:

```python
data = list(range(0, 300))
total = reduce(lambda x, y: x + y, map(lambda x: x + 10, filter(lambda x: x % 2 == 0), data))
print(total)
```

And turn them into more readable statements like this:

```python
(
    Piper(range(0,10))
    >> list
    >> op.where(fn(int, bool)(lambda x: x % 2 == 0))
    >> op.map_over(fn(int, int)(lambda x: x + 10))
    |  print
    > op.fold(fn(int, int, int)(lambda x, y: x + y))
)    
```

Most of the functions in pythonix are curried and have the subject as the last argument.
Curried means the arguments are passed in as function calls, and having the subject last
makes piping possible without changing Python's syntax. To make functions that work with
piping, take your functions that are like this:

```python
def get(data: list[int], index: int | slice) -> int:
    return data[index]

first = get([1, 2, 3], 0)

```

And make them like this.

```python
def get(index: int | slice):

    def inner(data: list[int]):

        return data[index]

    return inner 

first = get(0)([1, 2, 3])

```

Or if that's too much, use the `curry` decorators to make it easier.

```python
@curry.two
@curry.first_to_end
def get(data: list[int], index: int | slice) -> int:
    return data[index]

first = get(0)([1, 2, 3])

```

Back to Piping, there are three functions worth knowing with `Piper`.

1. `>>`: Put the value inside `Piper` into the function, and return a new `Piper` with the result.
2. `|`: Put the value inside `Piper` into the function, but keep `Piper` the same.
3. `>`: Put the value inside `Piper` into the function, and only return the result. Useful for exiting
the Piper's context and returning the final result.

If the operators aren't working for whatever reason, you can always use the `bind`, `do`, and `apply` methods, which map
to `>>`, `|`, and `>` respectively.

You can also use the `P` operator with pipes for quick bespoke piping of values. Like so:

```python
(
    range(0, 10)
    |P| list
    |P| op.where(fn(int, bool)(lambda x: x % 2 == 0))
    |P| op.map_over(fn(int, int)(lambda x: x + 10))
    |P| op.fold(fn(int, int, int)(lambda x, y: x + y))
    |P| print
)
```

It doesn't always save space, but it does make it easier to read
sequential function calls. Because the functions are decoupled
from their objects, you can pipe arbitrary functions over anything.

## Obvious Errors and Nulls

One of my favorite features of Rust is handling Exceptions as values
rather than try catching and throwing them. It's great because it makes
it very obvious when and how things can go wrong and encourages you
to handle the errors intentionally.

It is a little more verbose though, but the tradeoff is worth it.

### Error Catching

Instead of doing this:

```python
def get_customer_data(customer_id: int) -> dict:
    try:
        customer_data = get_data(customer_id)
        return customer_data
    except ValueError as e:
        print("Wrong id")

data: dict = get_customer_data(10)

```

You do this instead:

```python
def get_customer_data(customer_id: int) -> Res[dict, ValueError]:
    try:
        customer_data = get_data(customer_id)
        return Ok(customer_data)
    except ValueError as e:
        return Err(e)

data: Res[dict, ValueError] = get_customer_data(10)

```

Or you can do this automatically with a decorator:

```python
@res.safe(ValueError)
def get_customer_data(customer_id: int) -> dict:
    return get_data(customer_id)

data: Res[dict, ValueError] = get_customer_data(10)

```

Now it's obvious when things can go wrong and your type hints on your IDE will 
show you when things can fail.

### Error Handling

You can handle errors with pattern matching a la Rust, unpacking a la Go, or
with the `res` module a la Gleam.

Here is an example with pattern matching:

```python
data: Res[dict, ValueError] = get_customer_data(10)

match data:
    case Ok(customer_data):
        return customer_data
    case Err(e):
        logging.error(e)
        raise e
    case _:
        raise TypeError('Something went wrong')

```

This is great for being thorough with your results. You can see each case
and easily unpack the data from Ok and Err. It also makes it easy to apply
a default case or handle complex situations.

But what if I want something simple and fast like in Go? Say no more.

Try this instead:

```python

    data, err = unpack(get_customer_data(10))
    if err is not None:
        logging.error(e)
        raise e
    if data is None:
        raise TypeError('Something went wrong')

```

But wait?! In Go I can do `val, err = could_fail()`. Why do I have to use `unpack`?

It's a python thing. Because `Res` is actually `Ok | Err`, the type hints don't work
correctly if you unpack them normally, even if you have an `__iter__` method set up,
and base classes for `__iter__` and blah blah blah.

In short, I had to choose between better unpacking or better pattern matching. I chose
pattern matching because I think it looks neat, and you only sacrifice one function to
get it done.

Plus, it's easy to apply functions to results with `P` and `Piper`, remember?

```python
data, err = get_customer_data(10) |P| unpack
if err is not None:
    logging.error(e)
    raise e
if data is None:
    raise TypeError('Something went wrong')
```

You can also handle results with the `res` module, which has a lot of utilties
for unwrapping, handling, and transforming results safely. The module shamelessly
stolen from Rust's excellent methods, but implemented like Gleam.

Here is an example:

```python
data = res.unwrap(get_customer_data(10))
```

The above example will give you the Ok data if any, or raise the E instead. You can
also unwrap the err with `unwrap_err`. Since this is such a common thing, there is
a shorthand variant called `q` and `qe` which are unwrap and unwrap_err respectively.
`q` and `qe` are inspired by `?` in Rust.

The res module has a lot inside. Here is an example of an entire flow where we
are getting customer ids, and then getting total orders from the customer data.
There are a lot of steps that can fail, so we use `q` to unwrap the errors
and `safe` to catch them as we do. We can also combine multiple errors into one
with `combine_errors`.

```python
@res.safe(HTTPError, ValueError)
def get_customer_data(customer_id: int) -> dict:
    return get_data(customer_id)

@res.combine_errors(ValueError(), True)
@res.safe(HTTPError, ValueError, Nil)
def accumulate_customer_orders() -> int:
    customer_ids: list[int] = (
        Piper(get(customer_endoint))
        >> fn(Response, dict)(lambda response: response.json())
        >> op.item('ids')
        > q
    ) 
    total_orders = (
        Piper(customer_ids)
        >> op.map_over(get_customer_data)
        >> op.where(res.is_ok)
        >> op.map_over(q)
        >> op.map_over(op.item('orders'))
        >> op.map_over(q)
        > op.fold(fn(int, int, int)(lambda x, y: x + y))
    )
    return total_orders

def main():
    current_orders: Res[int, ValueError] = accumulate_customer_orders()
    match current_orders:
        case Ok(orders):
            print(f'Currently there are {orders} orders')
        case Err(e):
            logging.error(e)
            ping_slack_error_channel(str(e))
            raise e

```

### Null Handling

You handle `None` values the same way you handle Exceptions, by using
decorators or functions to catch values that could be None, and then
use pattern matching, unpacking, or the `res` module to go from there.

Here are some ways you can catch null values:

If a function value could be `None`, you can use the `some` function to
catch that and return a `Res[T, Nil]` result, which can be abbreviated to
`Opt[T]`.

```python
val: str | None = {'hello': 'world'}.get('hello')
opt: Opt[str] = some(val)
```

For function calls that could return `None`, you can have them return `Opt[T]`
instead.

```python
# With some
def get_hello(data: dict[str]) -> Opt[str]:
    return some(data.get('hello'))

hello: Opt[str] = get_hello({'hello': 'world'})

# With ok and err
def get_hola(data: dict[str]) -> Res[str, Nil]:
    try:
        return ok(Nil)(data['hola'])
    except KeyError as e:
        return err(str)(Nil(str(e)))

hola: Res[str, Nil] = get_hola({'hola': 'mundo'})
# Res[str, Nil] is the same as Opt[str]
```

Or you can use the `res.null_safe` or `res.null_and_error_safe` decorators to do that for you.

```python
@null_safe
def get_hello(data: dict[str]) -> str | None:
    return data.get('hello')

hello: Opt[str] = get_hello({'hello': 'world'})

@null_and_error_safe(KeyError)
def get_hola(data: dict[str]) -> str | None:
    return data['hola']

hola: Res[str, Nil] = get_hola({'hola': 'mundo'})
# Res[str, Nil] is the same as Opt[str]
```

Using these patterns makes it almost impossible to have unexpected or unhandled null
values in your code. Isn't that great?!

## Additional Features

Some other notable features include:

    * Log concatentation with the `trail` module
    * Pipeable asserts with `prove`
    * Supplement modules for common data structures with `pair`, `tup`, `dict_utils`, and `deq`.
    * Custom operator decorators with `grammar`
    * Type hinted lambda functions with `fn`

Each module is available for import like this:

```python
from pythonix.prelude import *
```

Import all from prelude will include all of the essentials like `Piper`, `P`, common `res` classes and functions, `fn`, etc.

Or you can specify a particular module like this:

```python
import pythonix.op as op
import pythonix.tup as tup
import pythonix.deq as deq
```

All the modules are fully tested, promote immutability, fully type checked and transparent, and fully documented.

Enjoy!

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/jhok2013/pythonix",
    "name": "pythonix",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<4.0,>=3.10",
    "maintainer_email": null,
    "keywords": "functional, pipes, result, error-as-values, Rust, Gleam",
    "author": "jhok2013",
    "author_email": "jhok2013@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/e3/ba/ee4290d8160bf18b1364237a027d41855728fd7370dbd25eddaa52d82197/pythonix-1.1.1.tar.gz",
    "platform": null,
    "description": "# PYTHONIX\n\nA collection of functional modules in Python 10 inspired by Gleam and Rust.\nIf you like functional oriented programming, but have to program in Python, then check this out.\n\nThe goal is to take some great features of Rust like strong types, errors by value with Results\nand Options, error unpacking like in Go, and some other great features from Gleam like piping with `|>` and classes as\nmodules, and combine them for use in Python without any external dependencies.\n\nHere are examples of my favorite features:\n\n## Pipes and Pipe Likes\n\n`P` and `Piper` are special wrapper classes the take what's on the left, and put it into the function on the right.\n\nYou can take statements like this:\n\n```python\ndata = list(range(0, 300))\ntotal = reduce(lambda x, y: x + y, map(lambda x: x + 10, filter(lambda x: x % 2 == 0), data))\nprint(total)\n```\n\nAnd turn them into more readable statements like this:\n\n```python\n(\n    Piper(range(0,10))\n    >> list\n    >> op.where(fn(int, bool)(lambda x: x % 2 == 0))\n    >> op.map_over(fn(int, int)(lambda x: x + 10))\n    |  print\n    > op.fold(fn(int, int, int)(lambda x, y: x + y))\n)    \n```\n\nMost of the functions in pythonix are curried and have the subject as the last argument.\nCurried means the arguments are passed in as function calls, and having the subject last\nmakes piping possible without changing Python's syntax. To make functions that work with\npiping, take your functions that are like this:\n\n```python\ndef get(data: list[int], index: int | slice) -> int:\n    return data[index]\n\nfirst = get([1, 2, 3], 0)\n\n```\n\nAnd make them like this.\n\n```python\ndef get(index: int | slice):\n\n    def inner(data: list[int]):\n\n        return data[index]\n\n    return inner \n\nfirst = get(0)([1, 2, 3])\n\n```\n\nOr if that's too much, use the `curry` decorators to make it easier.\n\n```python\n@curry.two\n@curry.first_to_end\ndef get(data: list[int], index: int | slice) -> int:\n    return data[index]\n\nfirst = get(0)([1, 2, 3])\n\n```\n\nBack to Piping, there are three functions worth knowing with `Piper`.\n\n1. `>>`: Put the value inside `Piper` into the function, and return a new `Piper` with the result.\n2. `|`: Put the value inside `Piper` into the function, but keep `Piper` the same.\n3. `>`: Put the value inside `Piper` into the function, and only return the result. Useful for exiting\nthe Piper's context and returning the final result.\n\nIf the operators aren't working for whatever reason, you can always use the `bind`, `do`, and `apply` methods, which map\nto `>>`, `|`, and `>` respectively.\n\nYou can also use the `P` operator with pipes for quick bespoke piping of values. Like so:\n\n```python\n(\n    range(0, 10)\n    |P| list\n    |P| op.where(fn(int, bool)(lambda x: x % 2 == 0))\n    |P| op.map_over(fn(int, int)(lambda x: x + 10))\n    |P| op.fold(fn(int, int, int)(lambda x, y: x + y))\n    |P| print\n)\n```\n\nIt doesn't always save space, but it does make it easier to read\nsequential function calls. Because the functions are decoupled\nfrom their objects, you can pipe arbitrary functions over anything.\n\n## Obvious Errors and Nulls\n\nOne of my favorite features of Rust is handling Exceptions as values\nrather than try catching and throwing them. It's great because it makes\nit very obvious when and how things can go wrong and encourages you\nto handle the errors intentionally.\n\nIt is a little more verbose though, but the tradeoff is worth it.\n\n### Error Catching\n\nInstead of doing this:\n\n```python\ndef get_customer_data(customer_id: int) -> dict:\n    try:\n        customer_data = get_data(customer_id)\n        return customer_data\n    except ValueError as e:\n        print(\"Wrong id\")\n\ndata: dict = get_customer_data(10)\n\n```\n\nYou do this instead:\n\n```python\ndef get_customer_data(customer_id: int) -> Res[dict, ValueError]:\n    try:\n        customer_data = get_data(customer_id)\n        return Ok(customer_data)\n    except ValueError as e:\n        return Err(e)\n\ndata: Res[dict, ValueError] = get_customer_data(10)\n\n```\n\nOr you can do this automatically with a decorator:\n\n```python\n@res.safe(ValueError)\ndef get_customer_data(customer_id: int) -> dict:\n    return get_data(customer_id)\n\ndata: Res[dict, ValueError] = get_customer_data(10)\n\n```\n\nNow it's obvious when things can go wrong and your type hints on your IDE will \nshow you when things can fail.\n\n### Error Handling\n\nYou can handle errors with pattern matching a la Rust, unpacking a la Go, or\nwith the `res` module a la Gleam.\n\nHere is an example with pattern matching:\n\n```python\ndata: Res[dict, ValueError] = get_customer_data(10)\n\nmatch data:\n    case Ok(customer_data):\n        return customer_data\n    case Err(e):\n        logging.error(e)\n        raise e\n    case _:\n        raise TypeError('Something went wrong')\n\n```\n\nThis is great for being thorough with your results. You can see each case\nand easily unpack the data from Ok and Err. It also makes it easy to apply\na default case or handle complex situations.\n\nBut what if I want something simple and fast like in Go? Say no more.\n\nTry this instead:\n\n```python\n\n    data, err = unpack(get_customer_data(10))\n    if err is not None:\n        logging.error(e)\n        raise e\n    if data is None:\n        raise TypeError('Something went wrong')\n\n```\n\nBut wait?! In Go I can do `val, err = could_fail()`. Why do I have to use `unpack`?\n\nIt's a python thing. Because `Res` is actually `Ok | Err`, the type hints don't work\ncorrectly if you unpack them normally, even if you have an `__iter__` method set up,\nand base classes for `__iter__` and blah blah blah.\n\nIn short, I had to choose between better unpacking or better pattern matching. I chose\npattern matching because I think it looks neat, and you only sacrifice one function to\nget it done.\n\nPlus, it's easy to apply functions to results with `P` and `Piper`, remember?\n\n```python\ndata, err = get_customer_data(10) |P| unpack\nif err is not None:\n    logging.error(e)\n    raise e\nif data is None:\n    raise TypeError('Something went wrong')\n```\n\nYou can also handle results with the `res` module, which has a lot of utilties\nfor unwrapping, handling, and transforming results safely. The module shamelessly\nstolen from Rust's excellent methods, but implemented like Gleam.\n\nHere is an example:\n\n```python\ndata = res.unwrap(get_customer_data(10))\n```\n\nThe above example will give you the Ok data if any, or raise the E instead. You can\nalso unwrap the err with `unwrap_err`. Since this is such a common thing, there is\na shorthand variant called `q` and `qe` which are unwrap and unwrap_err respectively.\n`q` and `qe` are inspired by `?` in Rust.\n\nThe res module has a lot inside. Here is an example of an entire flow where we\nare getting customer ids, and then getting total orders from the customer data.\nThere are a lot of steps that can fail, so we use `q` to unwrap the errors\nand `safe` to catch them as we do. We can also combine multiple errors into one\nwith `combine_errors`.\n\n```python\n@res.safe(HTTPError, ValueError)\ndef get_customer_data(customer_id: int) -> dict:\n    return get_data(customer_id)\n\n@res.combine_errors(ValueError(), True)\n@res.safe(HTTPError, ValueError, Nil)\ndef accumulate_customer_orders() -> int:\n    customer_ids: list[int] = (\n        Piper(get(customer_endoint))\n        >> fn(Response, dict)(lambda response: response.json())\n        >> op.item('ids')\n        > q\n    ) \n    total_orders = (\n        Piper(customer_ids)\n        >> op.map_over(get_customer_data)\n        >> op.where(res.is_ok)\n        >> op.map_over(q)\n        >> op.map_over(op.item('orders'))\n        >> op.map_over(q)\n        > op.fold(fn(int, int, int)(lambda x, y: x + y))\n    )\n    return total_orders\n\ndef main():\n    current_orders: Res[int, ValueError] = accumulate_customer_orders()\n    match current_orders:\n        case Ok(orders):\n            print(f'Currently there are {orders} orders')\n        case Err(e):\n            logging.error(e)\n            ping_slack_error_channel(str(e))\n            raise e\n\n```\n\n### Null Handling\n\nYou handle `None` values the same way you handle Exceptions, by using\ndecorators or functions to catch values that could be None, and then\nuse pattern matching, unpacking, or the `res` module to go from there.\n\nHere are some ways you can catch null values:\n\nIf a function value could be `None`, you can use the `some` function to\ncatch that and return a `Res[T, Nil]` result, which can be abbreviated to\n`Opt[T]`.\n\n```python\nval: str | None = {'hello': 'world'}.get('hello')\nopt: Opt[str] = some(val)\n```\n\nFor function calls that could return `None`, you can have them return `Opt[T]`\ninstead.\n\n```python\n# With some\ndef get_hello(data: dict[str]) -> Opt[str]:\n    return some(data.get('hello'))\n\nhello: Opt[str] = get_hello({'hello': 'world'})\n\n# With ok and err\ndef get_hola(data: dict[str]) -> Res[str, Nil]:\n    try:\n        return ok(Nil)(data['hola'])\n    except KeyError as e:\n        return err(str)(Nil(str(e)))\n\nhola: Res[str, Nil] = get_hola({'hola': 'mundo'})\n# Res[str, Nil] is the same as Opt[str]\n```\n\nOr you can use the `res.null_safe` or `res.null_and_error_safe` decorators to do that for you.\n\n```python\n@null_safe\ndef get_hello(data: dict[str]) -> str | None:\n    return data.get('hello')\n\nhello: Opt[str] = get_hello({'hello': 'world'})\n\n@null_and_error_safe(KeyError)\ndef get_hola(data: dict[str]) -> str | None:\n    return data['hola']\n\nhola: Res[str, Nil] = get_hola({'hola': 'mundo'})\n# Res[str, Nil] is the same as Opt[str]\n```\n\nUsing these patterns makes it almost impossible to have unexpected or unhandled null\nvalues in your code. Isn't that great?!\n\n## Additional Features\n\nSome other notable features include:\n\n    * Log concatentation with the `trail` module\n    * Pipeable asserts with `prove`\n    * Supplement modules for common data structures with `pair`, `tup`, `dict_utils`, and `deq`.\n    * Custom operator decorators with `grammar`\n    * Type hinted lambda functions with `fn`\n\nEach module is available for import like this:\n\n```python\nfrom pythonix.prelude import *\n```\n\nImport all from prelude will include all of the essentials like `Piper`, `P`, common `res` classes and functions, `fn`, etc.\n\nOr you can specify a particular module like this:\n\n```python\nimport pythonix.op as op\nimport pythonix.tup as tup\nimport pythonix.deq as deq\n```\n\nAll the modules are fully tested, promote immutability, fully type checked and transparent, and fully documented.\n\nEnjoy!\n",
    "bugtrack_url": null,
    "license": "Apache-2.0",
    "summary": "Rust and Gleam like functional programming in Python, complete with Results, pipes, and currying",
    "version": "1.1.1",
    "project_urls": {
        "Homepage": "https://github.com/jhok2013/pythonix",
        "Repository": "https://github.com/jhok2013/pythonix"
    },
    "split_keywords": [
        "functional",
        " pipes",
        " result",
        " error-as-values",
        " rust",
        " gleam"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "1adb2a1fe4239d13dd1bf92e2bad7efdc7806164fad70d255a3e2b8f6f304878",
                "md5": "894bd9ca880cf7825850b851f935affb",
                "sha256": "c58db3850a536a1848ccaf119017db4f8640dac6c27020409c4deb61c11e13b2"
            },
            "downloads": -1,
            "filename": "pythonix-1.1.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "894bd9ca880cf7825850b851f935affb",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<4.0,>=3.10",
            "size": 37164,
            "upload_time": "2024-05-23T15:57:19",
            "upload_time_iso_8601": "2024-05-23T15:57:19.375481Z",
            "url": "https://files.pythonhosted.org/packages/1a/db/2a1fe4239d13dd1bf92e2bad7efdc7806164fad70d255a3e2b8f6f304878/pythonix-1.1.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "e3baee4290d8160bf18b1364237a027d41855728fd7370dbd25eddaa52d82197",
                "md5": "885c801ce7ebf9eb7ade6b009ea99b4c",
                "sha256": "0d6a3654513f00918082618946eb8d3d7bcb95126a9a8df80ed6318568f31fb7"
            },
            "downloads": -1,
            "filename": "pythonix-1.1.1.tar.gz",
            "has_sig": false,
            "md5_digest": "885c801ce7ebf9eb7ade6b009ea99b4c",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4.0,>=3.10",
            "size": 31759,
            "upload_time": "2024-05-23T15:57:21",
            "upload_time_iso_8601": "2024-05-23T15:57:21.351927Z",
            "url": "https://files.pythonhosted.org/packages/e3/ba/ee4290d8160bf18b1364237a027d41855728fd7370dbd25eddaa52d82197/pythonix-1.1.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-05-23 15:57:21",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "jhok2013",
    "github_project": "pythonix",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "pythonix"
}
        
Elapsed time: 0.23329s