kungfu-fp 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. kungfu_fp-1.0.0/.github/actions/install-dependencies/action.yml +15 -0
  2. kungfu_fp-1.0.0/.github/workflows/ci.yml +64 -0
  3. kungfu_fp-1.0.0/.gitignore +7 -0
  4. kungfu_fp-1.0.0/.pre-commit-config.yaml +73 -0
  5. kungfu_fp-1.0.0/LICENSE +21 -0
  6. kungfu_fp-1.0.0/PKG-INFO +102 -0
  7. kungfu_fp-1.0.0/docs/advanced.md +30 -0
  8. kungfu_fp-1.0.0/docs/index.md +13 -0
  9. kungfu_fp-1.0.0/docs/option.md +30 -0
  10. kungfu_fp-1.0.0/docs/result.md +100 -0
  11. kungfu_fp-1.0.0/docs/sum.md +43 -0
  12. kungfu_fp-1.0.0/docs/unwrapping.md +89 -0
  13. kungfu_fp-1.0.0/examples/mappings.py +26 -0
  14. kungfu_fp-1.0.0/examples/misc.py +12 -0
  15. kungfu_fp-1.0.0/examples/option_unwrapping.py +25 -0
  16. kungfu_fp-1.0.0/examples/pattern_matching.py +39 -0
  17. kungfu_fp-1.0.0/examples/quasi_functor.py +10 -0
  18. kungfu_fp-1.0.0/examples/sum.py +44 -0
  19. kungfu_fp-1.0.0/examples/unwrapping.py +19 -0
  20. kungfu_fp-1.0.0/kungfu/__init__.py +27 -0
  21. kungfu_fp-1.0.0/kungfu/library/__init__.py +34 -0
  22. kungfu_fp-1.0.0/kungfu/library/caching.py +46 -0
  23. kungfu_fp-1.0.0/kungfu/library/error/__init__.py +3 -0
  24. kungfu_fp-1.0.0/kungfu/library/error/error.py +103 -0
  25. kungfu_fp-1.0.0/kungfu/library/error/error.pyi +25 -0
  26. kungfu_fp-1.0.0/kungfu/library/functor.py +75 -0
  27. kungfu_fp-1.0.0/kungfu/library/lazy/__init__.py +5 -0
  28. kungfu_fp-1.0.0/kungfu/library/lazy/lazy.py +26 -0
  29. kungfu_fp-1.0.0/kungfu/library/lazy/lazy_coro.py +51 -0
  30. kungfu_fp-1.0.0/kungfu/library/lazy/lazy_coro_result.py +157 -0
  31. kungfu_fp-1.0.0/kungfu/library/misc.py +50 -0
  32. kungfu_fp-1.0.0/kungfu/library/monad/__init__.py +12 -0
  33. kungfu_fp-1.0.0/kungfu/library/monad/option.py +59 -0
  34. kungfu_fp-1.0.0/kungfu/library/monad/result.py +190 -0
  35. kungfu_fp-1.0.0/kungfu/library/sum/__init__.py +3 -0
  36. kungfu_fp-1.0.0/kungfu/library/sum/sum.py +90 -0
  37. kungfu_fp-1.0.0/kungfu/library/sum/sum.pyi +20 -0
  38. kungfu_fp-1.0.0/kungfu/library/unwrapping.py +55 -0
  39. kungfu_fp-1.0.0/kungfu/py.typed +1 -0
  40. kungfu_fp-1.0.0/kungfu/utilities/__init__.py +13 -0
  41. kungfu_fp-1.0.0/kungfu/utilities/misc.py +20 -0
  42. kungfu_fp-1.0.0/kungfu/utilities/runtime_generic.py +84 -0
  43. kungfu_fp-1.0.0/kungfu/utilities/singleton/__init__.py +3 -0
  44. kungfu_fp-1.0.0/kungfu/utilities/singleton/singleton.py +19 -0
  45. kungfu_fp-1.0.0/kungfu/utilities/singleton/singleton.pyi +7 -0
  46. kungfu_fp-1.0.0/pyproject.toml +102 -0
  47. kungfu_fp-1.0.0/readme.md +93 -0
  48. kungfu_fp-1.0.0/tests/caching_test.py +31 -0
  49. kungfu_fp-1.0.0/tests/lazy_coro_result_test.py +49 -0
  50. kungfu_fp-1.0.0/tests/misc_test.py +32 -0
  51. kungfu_fp-1.0.0/tests/result_test.py +84 -0
  52. kungfu_fp-1.0.0/tests/sum_test.py +71 -0
  53. kungfu_fp-1.0.0/tests/test_unwrap_error.py +71 -0
  54. kungfu_fp-1.0.0/tests/typing_test.py +10 -0
  55. kungfu_fp-1.0.0/tests/unwrapping_test.py +26 -0
  56. kungfu_fp-1.0.0/uv.lock +371 -0
@@ -0,0 +1,15 @@
1
+ name: Install uv and dependencies
2
+
3
+ runs:
4
+ using: composite
5
+ steps:
6
+ - uses: hynek/setup-cached-uv@v2
7
+ with:
8
+ cache-suffix: -3.13-cache
9
+ cache-dependency-path: "**/uv.lock"
10
+
11
+ - name: "Install dependencies"
12
+ run: |
13
+ uv venv --python 3.13
14
+ uv sync --all-groups --dev
15
+ shell: bash
@@ -0,0 +1,64 @@
1
+ name: CI
2
+
3
+ 'on':
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ defaults:
9
+ run:
10
+ shell: bash
11
+
12
+ jobs:
13
+ lock-file:
14
+ name: "Lock uv"
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: asottile/workflows/.github/actions/fast-checkout@v1.8.1
18
+ - uses: ./.github/actions/install-dependencies
19
+ - run: uv lock --locked
20
+
21
+ linting:
22
+ name: "Linting"
23
+ runs-on: ubuntu-latest
24
+ needs: lock-file
25
+ steps:
26
+ - uses: asottile/workflows/.github/actions/fast-checkout@v1.8.1
27
+ - uses: ./.github/actions/install-dependencies
28
+
29
+ - name: Load ruff cache
30
+ id: cached-ruff
31
+ uses: actions/cache@v4
32
+ with:
33
+ key: ruff-3.13-${{ runner.os }}-${{ hashFiles('pyproject.toml') }}
34
+ path: .ruff_cache
35
+
36
+ - name: "Run ruff"
37
+ run: uvx ruff check .
38
+
39
+ type-checking:
40
+ name: "Type-checking"
41
+ runs-on: ubuntu-latest
42
+ needs: lock-file
43
+ steps:
44
+ - uses: asottile/workflows/.github/actions/fast-checkout@v1.8.1
45
+ - uses: ./.github/actions/install-dependencies
46
+ - run: uv run pyright --level error
47
+
48
+ tests:
49
+ name: "Testing"
50
+ runs-on: ubuntu-latest
51
+ needs: lock-file
52
+ steps:
53
+ - uses: asottile/workflows/.github/actions/fast-checkout@v1.8.1
54
+ - uses: ./.github/actions/install-dependencies
55
+
56
+ - name: Load pytest cache
57
+ id: cached-pytest
58
+ uses: actions/cache@v4
59
+ with:
60
+ key: pytest-3.13-${{ runner.os }}
61
+ path: .pytest_cache
62
+
63
+ - name: "Run pytest"
64
+ run: uv run pytest tests
@@ -0,0 +1,7 @@
1
+ __pycache__
2
+ .mypy_cache
3
+ .DS_Store
4
+ .coverage
5
+ dist/
6
+ .pytest_cache
7
+ .ruff_cache
@@ -0,0 +1,73 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v6.0.0
4
+ hooks:
5
+ - id: check-merge-conflict
6
+ stages: [ pre-commit, pre-push ]
7
+ - id: check-ast
8
+ stages: [ pre-commit ]
9
+ - id: trailing-whitespace
10
+ stages: [ pre-commit ]
11
+ - id: end-of-file-fixer
12
+ stages: [ pre-commit ]
13
+
14
+ - repo: local
15
+ hooks:
16
+ - id: uv
17
+ name: uv-lock
18
+ description: "Run uv lock"
19
+ entry: uv lock --locked
20
+ language: system
21
+ pass_filenames: false
22
+ types: [ python ]
23
+ stages: [ pre-commit ]
24
+
25
+ - id: pyright
26
+ name: pyright
27
+ description: "Run 'pyright' for Python type checking"
28
+ entry: uv run pyright kungfu --level error
29
+ pass_filenames: false
30
+ language: system
31
+ types: [ python ]
32
+ stages: [ pre-commit ]
33
+
34
+ - id: pytest
35
+ name: pytest
36
+ description: "Run tests for testing code"
37
+ entry: uv run pytest tests
38
+ language: system
39
+ pass_filenames: false
40
+ types: [ python ]
41
+ stages: [ pre-commit ]
42
+
43
+ - id: ruff
44
+ name: ruff-isort
45
+ description: "Run 'ruff --select I --select F401 --fix' for extremely fast Python sorting imports"
46
+ entry: uv run ruff check --select I --select F401 --fix
47
+ language: system
48
+ types: [ python ]
49
+ stages: [ pre-commit ]
50
+
51
+ - id: ruff
52
+ name: ruff-sortall
53
+ description: "Run 'ruff check --select RUF022 --fix' for extremely fast Python sorting dunder alls"
54
+ entry: uv run ruff check --select RUF022 --fix
55
+ language: system
56
+ types: [ python ]
57
+ stages: [ pre-commit ]
58
+
59
+ - id: ruff
60
+ name: ruff-format
61
+ description: "Run 'ruff format' for extremely fast Python formatting"
62
+ entry: uv run ruff format
63
+ language: system
64
+ types: [ python ]
65
+ stages: [ pre-commit ]
66
+
67
+ - id: ruff
68
+ name: ruff-check
69
+ description: "Run 'ruff-format' for extremely fast Python linting"
70
+ entry: uv run ruff check --fix
71
+ language: system
72
+ types: [ python ]
73
+ stages: [ pre-commit ]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Arseny Kriuchkov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: kungfu-fp
3
+ Version: 1.0.0
4
+ Author: timoniq
5
+ License-File: LICENSE
6
+ Requires-Python: <4.0,>=3.13
7
+ Requires-Dist: typing-extensions>=4.15.0
8
+ Description-Content-Type: text/markdown
9
+
10
+ # kungfu
11
+
12
+ ⚙️ Functional typing in Python!
13
+
14
+ ```python
15
+ get_user()
16
+ .then(get_posts)
17
+ .ensure(lambda posts: len(posts) > 0, "User has no posts")
18
+ .map(lambda posts: sum(post.views for post in posts) / len(posts))
19
+ .unwrap()
20
+ ```
21
+
22
+ ## why kungfu?
23
+
24
+ kungfu is based on the belief that raising exceptions should be avoided. So it defines a set of functional types needed to write better code. This type strategy grants you with higher control over your runtime.
25
+
26
+ Panicking is the last recourse but for some obscure reason spawning exceptions each time you encounter something other than the all-successful behaviour became a norm in python code.
27
+
28
+ Let's fix this up and instead of panicking do treat error-state as equal to successful-state. kungfu provides you with all you need to migrate to functional typing approach
29
+
30
+ Improving control flow will definitely result in avoiding logical errors and getting better code readability: you start to see each kind of behaviour you get from the function.
31
+
32
+ why "kungfu" as a name? writing in kungfu requires some discipline. kung fu literally meaning "hard work" reflects on this concept. through the hard work on gaining control over our types we get clarity and power
33
+
34
+ ## (📖) documentation
35
+
36
+ See [documentation](/docs/index.md)
37
+
38
+ See [examples](/examples/)
39
+
40
+ ## examples
41
+
42
+ **howto** build monad chains:
43
+
44
+ ```python
45
+ def get_user(user_id: int) -> Result[User, str]:
46
+ ...
47
+
48
+ def get_posts(user: User) -> Result[list[Post], str]:
49
+ ...
50
+
51
+ def get_average_views(user_id: int) -> Result[int, str]:
52
+ return (
53
+ get_user(user_id)
54
+ .then(get_posts)
55
+ .ensure(len, "User has no posts")
56
+ .map(lambda posts: sum(post.views for post in posts) / len(posts))
57
+ )
58
+
59
+ avg_views = get_average_views().unwrap()
60
+ ```
61
+
62
+ **howto** detalize function result:
63
+
64
+ ```python
65
+ @unwrapping
66
+ def send_funds(
67
+ sender_id: int,
68
+ receiver_id: int,
69
+ amount: decimal.Decimal,
70
+ ) -> Result[TransactionID, str]:
71
+ sender = get_user(sender_id).expect("Sender is undefined")
72
+ receiver = get_user(receiver_id).expect("Receiver is undefined")
73
+
74
+ if sender.get_balance().expect("Could not get sender balance") < amount:
75
+ return Error("Sender has not enough funds to complete transaction")
76
+
77
+ return Ok(
78
+ create_transaction(sender, receiver, amount)
79
+ .unwrap()
80
+ .transaction_id
81
+ )
82
+ ```
83
+
84
+ Contributions are welcome
85
+
86
+ MIT licensed
87
+
88
+ ---
89
+
90
+ # awesome kungfu projects
91
+
92
+ [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/sindresorhus/awesome)
93
+
94
+ These projects are awesome to extend kungfu environment:
95
+
96
+ * **[nodnod](https://github.com/timoniq/nodnod)** - dependency injection with efficient nodes computation agents and dependency scoping
97
+
98
+ * **[combinators.py](https://github.com/prostomarkeloff/combinators.py)** - build explicit functional pipelines
99
+
100
+ These projects implement kungfu environment in their specific utility cases:
101
+
102
+ * **[telegrinder](https://github.com/timoniq/telegrinder)** - telegram bot framework
@@ -0,0 +1,30 @@
1
+ # kungfu - advanced
2
+
3
+ In order to use advanced methods, some theory needs to be explained.
4
+
5
+ Monad (a concept that kungfu is build on), requires its implementation to have a `bind` method. In `kungfu`, its name is `then` which expresses this idea in terms of imperative reality much better. We can *link* a monad (a result / an option) to a functor that will transform a value but preserve the original error type.
6
+
7
+ ```python
8
+ x: Result[int, str]
9
+
10
+ def invalid_bind(value: int) -> Result[int, RuntimeError]:
11
+ # Here, the error type is changed. Therefore, this binding procedure cannot be used with the `x` instance
12
+ ...
13
+
14
+ def normal_bind(value: int) -> Result[int, str]:
15
+ """Multiplies odd numbers by 2"""
16
+ if x // 2 == 0:
17
+ return Error("value cannot be even")
18
+ return x * 2
19
+
20
+
21
+ x = Ok(3)
22
+
23
+ x.then(normal_bind) # Ok(6) ( Result[int, str] )
24
+ x.then(normal_bind).then(normal_bind) # Error("value cannot be even")
25
+
26
+ x = Error("Something has already went wrong ..")
27
+
28
+ x.then(normal_bind) # Error("Something has already went wrong ..")
29
+ x.then(normal_bind).then(normal_bind) # Error("Something has already went wrong ..")
30
+ ```
@@ -0,0 +1,13 @@
1
+ # kungfu - documentation
2
+
3
+ Welcome! Here is the list of concepts implemented by kungfu in an order suggested to learn this library:
4
+
5
+ 1. [Result](/docs/result.md)
6
+
7
+ 2. [Option](/docs/option.md)
8
+
9
+ 3. [Unwrapping](/docs/unwrapping.md)
10
+
11
+ 4. [Sum](/docs/sum.md)
12
+
13
+ 5. [Advanced practices](/docs/advanced.md)
@@ -0,0 +1,30 @@
1
+ # kungfu - option
2
+
3
+ `Option` is derived from [Result](/docs/result.md), but its error state is secluded to only one possible - the `Nothing` state. The value state of Option is called `Some`.
4
+ You may be used to use None python value to create such sort of ambiguation, but this is not good for the programming environment we are trying to create as it lacks functional features.
5
+ Due to the fact, that `Option` is derived from simple Result, we can use all of the features of Result in Option. We can create functional maps, unwrap variables and preserve control flow because of avoiding spawning exceptions what we would do, for example, in case when we would require variable not to be None in that paradigm we are trying to do away with.
6
+
7
+ In order to work with Option we will need to import such components as `Option`, `Some`, `Nothing`:
8
+
9
+ ```python
10
+ from kungfu import Option, Some, Nothing
11
+ ```
12
+
13
+ Let's create a function that will receive an option string and map its value into an uppercase version if it is in the state of `Some`:
14
+
15
+ ```python
16
+ def shout(msg: Option[str]) -> None:
17
+ print(msg.map(str.upper).unwrap_or("RRr"), "!!")
18
+
19
+ shout(Nothing()) # RRr !!
20
+ shout(Some("arseny")) # ARSENY !!
21
+ ```
22
+
23
+ ---
24
+
25
+ As Option is just a Result lacking an error type, an instance of `Result[T, Err]` can be casted into an instance of `Option[T]` with a simple cast expression (Nothing can be passed like this, because it suppresses arguments it receives on initialization):
26
+
27
+ ```python
28
+ def to_option(result: Result[T, Err]) -> Option[T]:
29
+ return result.cast(Some, Nothing)
30
+ ```
@@ -0,0 +1,100 @@
1
+ # kungfu - Result
2
+
3
+ ## Theory
4
+
5
+ `Result` is a key object needed to create names in two states: in a state of an error and in a state of a value.
6
+
7
+ Probably, you may be used to spawn exceptions as soon as you encounter an error. This is a bad practice. Fntypes advices strongly against raising exceptions. Why?
8
+
9
+ It's because they ruin control flow completely: exceptions, raised in a function, cannot be type-hinted, therefore we cannot see and guarantee to handle all the exceptions that appear from it. Thus, this makes us create abstract exception handlers *just in case*, which is definitely must be perceived as a bad practice.
10
+
11
+ In order to keep our control flow in a good form, we are offered to use Result monad - which is an entity that can obtain 2 states: a value - when the function proceeded successfully, or an error of a specific type - in case a function encountered a problem.
12
+
13
+ Therefore, two classes in `kungfu` exist to represent these states:
14
+
15
+ 1. `kungfu.Ok` - representing a successful case
16
+
17
+ 2. `kungfu.Error` - representing an error
18
+
19
+ An ambiguative state of those two states is called `Result`.
20
+
21
+ ## Application
22
+
23
+ Let's create a function which is going to divide two numbers, and instead of raising an exception when the divisor is zero, its going to form an error state of a result. Otherwise, `Ok` with a resulting number is returned.
24
+
25
+ ```python
26
+ from kungfu import Result, Ok, Error
27
+
28
+ # Result takes 2 type arguments:
29
+ # first one is a type of value on success,
30
+ # the second one is a type of error
31
+ def divide(a: int, b: int) -> Result[float, str]:
32
+ if b == 0:
33
+ return Error("Divisor cannot be zero")
34
+ return Ok(a / b)
35
+ ```
36
+
37
+ Now, when the function is called, it returns a Result of one of two possible values. Let's try different ways of handling it:
38
+
39
+ ```python
40
+ x = divide(6, 2) # <Result: Ok(3)>
41
+ y = divide(3, 0) # <Result: Error("Divisor cannot be zero")>
42
+
43
+ # Function unwrap, tranforms this call back to two states: of an exception or an actual value,
44
+ # Unless we are in a safe `unwrapped` scope.
45
+ x.unwrap() # 3
46
+ y.unwrap() # This is going to raise an exception: UnwrapError("Divisor cannot be zero")
47
+
48
+ # Unwrap with an alternative *value*
49
+ x.unwrap_or(10) # 3
50
+ y.unwrap_or(10) # 10
51
+
52
+ x.unwrap_or_none() # 3
53
+ y.unwrap_or_none() # None
54
+
55
+ # Unwrap with an alternative *result*
56
+ x.unwrap_or_other(divide(8, 4)) # <Result: Ok(3)>
57
+ y.unwrap_or_other(divide(8, 4)) # <Result: Ok(2)>
58
+
59
+ # Map - apply map on result value returning a result of an altered value type
60
+ # If result is in error state, mapper is not going to be applied
61
+ lst = ["Eniki", "Beniki", "Eli", "Vareniki"]
62
+
63
+ x.map(lambda n: lst[n]) # <Result: Ok("Vareniki")>
64
+ y.map(lambda n: lst[n]) # <Result: Error("Divisor cannot be zero")>
65
+
66
+ x.map(lambda n: lst[n]).map(str.upper) # <Result: Ok("VARENIKI")>
67
+
68
+ # More map functions are going to be presented in 'docs/advanced'
69
+
70
+ x.expect("Division failure") # Raises an exception: UnwrapError("Division failure")
71
+ # Expect is needed to transform error types
72
+
73
+ # .then - is an essential operation to compose multiple result returning functions
74
+ # Probably you may know it as bind operation
75
+ # Error type of those must be same !
76
+ # Argument is of value type, returning result can be of a different type
77
+ queue = []
78
+ IndexType = type("IndexType", (int,), {})
79
+
80
+ def send_to_queue(n: int) -> Result[int, str]:
81
+ if len(queue) > 9:
82
+ return Error("Too many numbers in queue!")
83
+ queue.append(n)
84
+ return Ok(len(queue) - 1) # index in queue
85
+
86
+ x.then(send_to_queue) # <Result: Ok(IndexType(0))>
87
+ x.then(send_to_queue).unwrap() # IndexType(1)
88
+
89
+
90
+ # Cast
91
+ # Through cast Result type can be converted to other two-states object
92
+ # By default casts for `ok` and `error` states are echo-functions.
93
+ # Thus, only one cast may be set
94
+
95
+ # Cast returns a union of two states' types
96
+ x.cast() # Will return the same union of Result[float, str]
97
+ x.cast(Some, lambda _: Nothing()) # Will cast it into a Option-like type (quite useful)
98
+ # OR a more elegant way (thanks to Nothing-type argument suppression)
99
+ x.cast(Some, Nothing)
100
+ ```
@@ -0,0 +1,43 @@
1
+ # kungfu - Sum
2
+
3
+ `Sum` is a functional replacement for `Union` typehint. It has methods to enhance the control flow over union types. Some of these methods use head-tail notation due to the type-hinting restrictions.
4
+
5
+ Sum can contain multiple types (let the number of types be called N), therefore it can be contracted to N-1 states. Let's review the methods we can use to do so.
6
+
7
+ Head-tail notation is a notation that splits any set of data in two parts: head - the first element, and tail, that is a set of everything else. Therefore, if we consider a `Sum[A, B, C]`, its head will be `A`, and tail `[B, C]`.
8
+
9
+ ## `.only(type = default to *head*)`
10
+
11
+ Only contracts the sum to a single type and returns a `Result[type, str]`
12
+
13
+ ```python
14
+ x: Sum[A, B, C]
15
+
16
+ x.only(B) # Result[B, str]
17
+ x.only() # Result[A, str] (heading type of sum is A)
18
+ ```
19
+
20
+ Now, when you understand the concept, the syntaxic replacement can be intruduced:
21
+
22
+ ```python
23
+ x[B].expect("Can only be B")
24
+ # Can be used interchangably with
25
+ x.only(B).expect("Can only be B")
26
+ ```
27
+
28
+ ## `.detach()`
29
+
30
+ Head ensures that sum is not of *head* type and returns a result with a value-state of sum of *tail* types.
31
+
32
+ ```python
33
+ x.detach() # Result[Sum[B, C], str]
34
+ ```
35
+
36
+ ## `.v`
37
+
38
+ Returns a union (basically an intersection of all types of sum).
39
+
40
+ ```python
41
+ x.v # Union[A, B, C]
42
+ ```
43
+
@@ -0,0 +1,89 @@
1
+ # kungfu - unwrapping
2
+
3
+ Unwrapping is a way to manage control flow designed with kungfu monads in a more convenient manner.
4
+
5
+ In theory, unwrapping is needed to create a syntaxic sugar that functions like `bind` method that defines a monad. In kungfu `bind` is known as `.then`. What it does, is it creates a binding between two functions returning a result of the same error type or option. If the current monad is in the state of error, the error is returned, if not, the value is passed into the function that is an argument in `then`. Let's look to the following example:
6
+
7
+ ```python
8
+ def func1(a: int) -> Result[str, str]:
9
+ if a == 0:
10
+ return Error("a cannot be 0")
11
+ return Ok(str(a))
12
+
13
+ def func2(a: int) -> Result[str, TypeError]:
14
+ ...
15
+
16
+ def func3(b: str) -> Result[int, str]:
17
+ if not b.isdigit():
18
+ return Error("Not a digit")
19
+ return Ok(int(b))
20
+
21
+
22
+ x: Result[int, str] = Ok(11)
23
+
24
+ x.then(func1) # "11"
25
+ x.then(func2) # Type checker: wrong error type
26
+ x.then(func1).then(func3) # 11
27
+
28
+ y: Result[int, str] = Error("Some error")
29
+
30
+ y.then(func1) # Error("Some error")
31
+ y.then(func1).then(func3) # Error("Some error")
32
+ ```
33
+
34
+ Now, after we have understanding what `then` (or `bind`) is needed for, we can also comprehend the idea of a syntaxic sugar to apply this idea in a single scope, with no need to create multiple functions.
35
+
36
+ The only thing we should bear in mind is that the error type must be always same. For `Option` error type is None. If we need to change error type, we can use `expect` function.
37
+
38
+ In order to do that, `unwrapping` decorator is used:
39
+
40
+
41
+ ```python
42
+ from kungfu import unwrapping, Result, Ok, Error, Option
43
+
44
+
45
+ class User:
46
+ ...
47
+
48
+ def get_balance(self) -> Result[decimal.Decimal, str]:
49
+ ...
50
+
51
+
52
+ @dataclass
53
+ class Transaction:
54
+ transaction_id: TransactionID
55
+
56
+
57
+ def get_user(sender_id: int) -> Option[User]:
58
+ ...
59
+
60
+
61
+ def create_transaction(sender: User, receiver: User, amount: decimal.Decimal) -> Result[Transaction, str]:
62
+ ...
63
+
64
+
65
+ @unwrapping
66
+ def send_funds(
67
+ sender_id: int,
68
+ receiver_id: int,
69
+ amount: decimal.Decimal,
70
+ ) -> Result[TransactionID, str]:
71
+ sender = get_user(sender_id).expect("Sender is undefined") # marker 1
72
+ receiver = get_user(receiver_id).expect("Receiver is undefined")
73
+ if sender.get_balance().unwrap() < amount: # marker 2
74
+ return Error("Sender has not enough funds to complete transaction")
75
+
76
+ return Ok(
77
+ create_transaction(sender, receiver, amount)
78
+ .unwrap()
79
+ .transaction_id
80
+ )
81
+ ```
82
+
83
+ In this example we have a function which is wrapped in unwrapping decorator, it means that all methods like `unwrap`, `expect` are now covered and if any monad on which such methods are called is in the state of error, it will result in returning an error state out of the function obtaining the scope.
84
+
85
+ Let's look onto markers.
86
+
87
+ marker 1. Here we see that get_user returns an Option, which is of error type None, it doesn't correspond to an error type `str` of `Result[TransactionID, str]`. Thus, we have to change it and do `expect`. Into expect we pass the new error which is of type `str`. If error will pop out of `get_user`, an error will be immediately returned out of `send_funds`.
88
+
89
+ marker 2. Here, out of `get_balance` we get a result (`Result[decimal.Decimal, str]`) with a corresponding error type `str`. Thats the reason we can just do `unwrap`, and the error, if it appears, will be returned *as it is* out of the function `send_funds`.
@@ -0,0 +1,26 @@
1
+ from kungfu import Error, Nothing, Option, Result, Some
2
+
3
+
4
+ def map_error(result: Result[int, str]) -> Option[int]:
5
+ return result.cast(Some, Nothing)
6
+
7
+
8
+ def map_value(result: Result[int, str]) -> Result[str, str]:
9
+ return result.map(str)
10
+
11
+
12
+ def map_or(result: Result[int, str]) -> int:
13
+ return result.map_or(0, lambda x: x + 1).unwrap()
14
+
15
+
16
+ def get_n() -> Result[int, str]:
17
+ return Error("Something happened")
18
+
19
+
20
+ n = get_n()
21
+
22
+
23
+ print(map_error(n)) # Nothing()
24
+ print(map_value(n)) # Error("Something happened")
25
+ print(map_or(n)) # 0
26
+ print(n.cast().cast().cast()) # Error("Something happened")
@@ -0,0 +1,12 @@
1
+ from kungfu import Error, Ok, Pulse
2
+
3
+
4
+ def send_message(text: str) -> Pulse[str]:
5
+ if not text:
6
+ return Error("text is empty")
7
+ return Ok()
8
+
9
+
10
+ result = send_message("hi!!!")
11
+
12
+ print(result)
@@ -0,0 +1,25 @@
1
+ import random
2
+
3
+ from kungfu import Error, Nothing, Ok, Option, Result, Some, is_err, unwrapping
4
+
5
+
6
+ @unwrapping
7
+ def calculate_something(arr: list[Option[int]]) -> Result[int, str]:
8
+ if is_err(arr[0]):
9
+ return Error("Invalid first element")
10
+ s = arr[0].unwrap()
11
+ for coeff in arr[1:]:
12
+ s += s * coeff.unwrap_or(0)
13
+ return Ok(s)
14
+
15
+
16
+ @unwrapping
17
+ def calculate_over_something(x: Option[int], count: int) -> Option[int]:
18
+ lst: list[Option[int]] = [x]
19
+ for i in range(count):
20
+ lst.append(Some(lst[len(lst) - 1].unwrap_or(0) + random.randint(15, 19)))
21
+ return Some(calculate_something(lst).unwrap())
22
+
23
+
24
+ print(calculate_over_something(Some(10), 10).unwrap_or_none()) # *big number*
25
+ print(calculate_over_something(Nothing(), 10).unwrap_or_none()) # None