├── .bumpversion.cfg ├── .cspell_dict.txt ├── .github ├── dependabot.yml └── workflows │ ├── github-pages.yml │ └── main.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── Makefile ├── README.md ├── cspell.config.yaml ├── docs ├── Makefile └── source │ ├── about.md │ ├── conf.py │ └── index.rst ├── pyproject.toml ├── src └── category_theory │ ├── __init__.py │ ├── applicative.py │ ├── core.py │ ├── functor.py │ ├── monoid.py │ ├── operations.py │ └── par_operations.py └── tests ├── test_applicative.py ├── test_functor.py ├── test_monoid.py ├── test_operations.py └── test_par_operations.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 2021.0.2 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version="{current_version}" 8 | replace = version="{new_version}" 9 | 10 | [bumpversion:file:src/python/ap_features/__init__.py] 11 | search = __version__ = "{current_version}" 12 | replace = __version__ = "{new_version}" 13 | -------------------------------------------------------------------------------- /.cspell_dict.txt: -------------------------------------------------------------------------------- 1 | Bartosz 2 | chunk 3 | chunkified 4 | chunkify 5 | codecov 6 | dask 7 | fillvalue 8 | Finsberg 9 | foldr 10 | functor 11 | Functor 12 | Functors 13 | funtors 14 | mapsto 15 | maybeint 16 | Milewski 17 | monoid 18 | Monoid 19 | monoids 20 | Monoids 21 | morphism 22 | morphisms 23 | parop 24 | rightarrow 25 | semigroup 26 | viewcode 27 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/github-pages.yml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Setup Python 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: "3.10" 18 | 19 | - name: Upgrade pip 20 | run: | 21 | python3 -m pip install --upgrade pip 22 | 23 | - name: Install dependencies 24 | run: | 25 | sudo apt install -y pandoc 26 | python3 -m pip install ".[doc]" 27 | 28 | - name: Build docs 29 | run: | 30 | make docs 31 | 32 | - name: Deploy 33 | uses: peaceiris/actions-gh-pages@v4 34 | with: 35 | github_token: ${{ secrets.GITHUB_TOKEN }} 36 | publish_dir: ./docs/build/html 37 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.10"] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install package 18 | run: | 19 | python -m pip install -e ".[test]" 20 | - name: Test with pytest 21 | run: | 22 | python -m pytest 23 | 24 | - name: Extract Coverage 25 | run: | 26 | python3 -m coverage report | sed 's/^/ /' >> $GITHUB_STEP_SUMMARY 27 | python3 -m coverage json 28 | export TOTAL=$(python3 -c "import json;print(json.load(open('coverage.json'))['totals']['percent_covered_display'])") 29 | echo "total=$TOTAL" >> $GITHUB_ENV 30 | 31 | - name: Upload HTML report. 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: html-report 35 | path: htmlcov 36 | 37 | - name: Create coverage Badge 38 | if: github.ref == 'refs/heads/main' 39 | uses: schneegans/dynamic-badges-action@v1.7.0 40 | with: 41 | auth: ${{ secrets.GIST_SECRET }} 42 | gistID: a7290de789564f03eb6b1ee122fce423 43 | filename: category-theory-python-coverage.json 44 | label: Coverage 45 | message: ${{ env.total }}% 46 | minColorRange: 50 47 | maxColorRange: 90 48 | valColorRange: ${{ env.total }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | docs/source/README.md 3 | docs/source/category_theory.rst 4 | docs/source/modules.rst 5 | # Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode 6 | # Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode 7 | 8 | ### Python ### 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | share/python-wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .nox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | *.py,cover 58 | .hypothesis/ 59 | .pytest_cache/ 60 | cover/ 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Django stuff: 67 | *.log 68 | local_settings.py 69 | db.sqlite3 70 | db.sqlite3-journal 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | .pybuilder/ 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # IPython 90 | profile_default/ 91 | ipython_config.py 92 | 93 | # pyenv 94 | # For a library or package, you might want to ignore these files since the code is 95 | # intended to run in multiple environments; otherwise, check them in: 96 | # .python-version 97 | 98 | # pipenv 99 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 100 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 101 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 102 | # install all needed dependencies. 103 | #Pipfile.lock 104 | 105 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 106 | __pypackages__/ 107 | 108 | # Celery stuff 109 | celerybeat-schedule 110 | celerybeat.pid 111 | 112 | # SageMath parsed files 113 | *.sage.py 114 | 115 | # Environments 116 | .env 117 | .venv 118 | env/ 119 | venv/ 120 | ENV/ 121 | env.bak/ 122 | venv.bak/ 123 | 124 | # Spyder project settings 125 | .spyderproject 126 | .spyproject 127 | 128 | # Rope project settings 129 | .ropeproject 130 | 131 | # mkdocs documentation 132 | /site 133 | 134 | # mypy 135 | .mypy_cache/ 136 | .dmypy.json 137 | dmypy.json 138 | 139 | # Pyre type checker 140 | .pyre/ 141 | 142 | # pytype static type analyzer 143 | .pytype/ 144 | 145 | # Cython debug symbols 146 | cython_debug/ 147 | 148 | ### VisualStudioCode ### 149 | .vscode/* 150 | !.vscode/settings.json 151 | !.vscode/tasks.json 152 | !.vscode/launch.json 153 | !.vscode/extensions.json 154 | *.code-workspace 155 | 156 | # Local History for Visual Studio Code 157 | .history/ 158 | 159 | ### VisualStudioCode Patch ### 160 | # Ignore all local history of files 161 | .history 162 | .ionide 163 | 164 | # End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode 165 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - id: check-docstring-first 9 | - id: debug-statements 10 | 11 | - repo: https://github.com/astral-sh/ruff-pre-commit 12 | # Ruff version. 13 | rev: 'v0.11.9' 14 | hooks: 15 | # Run the linter. 16 | - id: ruff 17 | args: [ --fix ] 18 | # Run the formatter. 19 | - id: ruff-format 20 | 21 | - repo: https://github.com/asottile/add-trailing-comma 22 | rev: v3.1.0 23 | hooks: 24 | - id: add-trailing-comma 25 | 26 | - repo: https://github.com/pre-commit/mirrors-mypy 27 | rev: v1.15.0 28 | hooks: 29 | - id: mypy 30 | 31 | - repo: https://github.com/streetsidesoftware/cspell-cli 32 | rev: v9.0.1 33 | hooks: 34 | - id: cspell 35 | files: src/|docs/source/|tests|README.md 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Henrik Finsberg 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. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | 4 | define PRINT_HELP_PYSCRIPT 5 | import re, sys 6 | 7 | for line in sys.stdin: 8 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 9 | if match: 10 | target, help = match.groups() 11 | print("%-20s %s" % (target, help)) 12 | endef 13 | export PRINT_HELP_PYSCRIPT 14 | 15 | help: 16 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 17 | 18 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 19 | 20 | clean-build: ## remove build artifacts 21 | rm -fr build/ 22 | rm -fr dist/ 23 | rm -fr .eggs/ 24 | find . -name '*.egg-info' -exec rm -fr {} + 25 | find . -name '*.egg' -exec rm -f {} + 26 | rm -f src/category_theory/cost_terms.* 27 | 28 | clean-pyc: ## remove Python file artifacts 29 | find . -name '*.pyc' -exec rm -f {} + 30 | find . -name '*.pyo' -exec rm -f {} + 31 | find . -name '*~' -exec rm -f {} + 32 | find . -name '__pycache__' -exec rm -fr {} + 33 | 34 | clean-test: ## remove test and coverage artifacts 35 | rm -fr .tox/ 36 | rm -f .coverage 37 | rm -fr htmlcov/ 38 | rm -fr .pytest_cache 39 | 40 | lint: ## check style with flake8 41 | python -m flake8 src/category_theory tests 42 | 43 | type: ## Run mypy 44 | python3 -m mypy src/category_theory tests 45 | 46 | test: ## run tests on every Python version with tox 47 | python3 -m pytest 48 | 49 | spell: ## Run spell checker 50 | cspell-cli lint src/category_theory/* -c cspell.config.yaml --color 51 | 52 | docs: ## generate Sphinx HTML documentation, including API docs 53 | rm -f docs/source/category_theory.rst 54 | rm -f docs/source/modules.rst 55 | for file in README.md; do \ 56 | cp $$file docs/source/. ;\ 57 | done 58 | sphinx-apidoc -o docs/source src/category_theory 59 | $(MAKE) -C docs clean 60 | $(MAKE) -C docs html 61 | 62 | show-docs: 63 | open docs/build/html/index.html 64 | 65 | release: dist ## package and upload a release 66 | python3 -m twine upload -u ${PYPI_USERNAME} -p ${PYPI_PASSWORD} dist/* 67 | 68 | dist: clean ## builds source and wheel package 69 | python setup.py sdist 70 | python setup.py bdist_wheel 71 | ls -l dist 72 | 73 | install: clean ## install the package to the active Python's site-packages 74 | python3 -m pip install . 75 | 76 | dev: clean ## Just need to make sure that libfiles remains 77 | python3 -m pip install -e ".[dev]" 78 | pre-commit install 79 | 80 | bump: ## Bump version 81 | bump2version patch 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/finsberg/category-theory-python/main.svg)](https://results.pre-commit.ci/latest/github/finsberg/category-theory-python/main) 2 | [![CI](https://github.com/finsberg/category-theory-python/actions/workflows/main.yml/badge.svg)](https://github.com/finsberg/category-theory-python/actions/workflows/main.yml) 3 | [![github pages](https://github.com/finsberg/category-theory-python/actions/workflows/github-pages.yml/badge.svg)](https://github.com/finsberg/category-theory-python/actions/workflows/github-pages.yml) 4 | [![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/finsberg/a7290de789564f03eb6b1ee122fce423/raw/category-theory-python-coverage.json)](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/finsberg/a7290de789564f03eb6b1ee122fce423/raw/category-theory-python-coverage.json) 5 | 6 | # Category Theory for python programmers 7 | 8 | This is my attempt to implement some of the common structures from Category theory in python. Note that I am a beginner in Category theory and will use this repo as way to learn it. 9 | 10 | Topics to be covered are 11 | 12 | - [x] Monoids 13 | - [ ] Functors (in progress) 14 | - [ ] Applicative Functors (in progress) 15 | - [ ] Monads 16 | 17 | This list might be more detailed in the future. 18 | 19 | My assessment is manly based on the book called [Category Theory for Programmers](https://bartoszmilewski.com/2014/10/28/category-theory-for-programmers-the-preface/) by Bartosz Milewski and the podcast [LambdaCast](https://soundcloud.com/lambda-cast) 20 | 21 | 22 | ## Typing 23 | 24 | I will also use this platform to investigate some of the more advanced features of python's typing system, and will therefore only support the latest version of python. 25 | 26 | ## Testing 27 | 28 | I am definitely not capable of finding good test cases that can really test the corner cases of my implementation. Therefore I will try to use (and learn) property-based testing using [Hypothesis](https://hypothesis.readthedocs.io/en/latest/). 29 | 30 | 31 | ## Contributing 32 | This repo is manly for me to learn about these concepts in Category theory. If anyone finds this useful that is great. If you spot any bugs or mistakes I have made I would be grateful if you could file an issue (or even better : first file an issue and then submit a PR). 33 | -------------------------------------------------------------------------------- /cspell.config.yaml: -------------------------------------------------------------------------------- 1 | dictionaryDefinitions: 2 | - name: category_theory 3 | path: .cspell_dict.txt 4 | addWords: true 5 | dictionaries: 6 | - category_theory 7 | ignoreWords: [quickstart, toctree, maxdepth, genindex, modindex] 8 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/source/about.md: -------------------------------------------------------------------------------- 1 | # Category theory 2 | Category theory is probably the most abstract form of mathematics 3 | there is. It is the theory that tries to unite all areas of mathematics. 4 | It turns out that many concepts that arises in different areas of mathematics 5 | share some common properties. That is, if your know about a relation in 6 | set theory, then there is a similar relation in logic, or algebra. 7 | 8 | From a programmers perspective, category theory is interesting because 9 | we can use it to study type theory. 10 | 11 | ## Category 12 | 13 | A category is a collection of objects that are linked by arrows. 14 | We typically refer the the arrows as morphisms, and if you have knowledge 15 | of higher mathematics, you know that there exists different types of morphisms, 16 | such as isomorphisms, homomorphisms, homeomorphism, etc. 17 | 18 | In the category of sets the objects would typically be sets, and the arrows 19 | could be regular functions, i.e mappings from one set into another. 20 | 21 | 22 | There are two axioms for a category 23 | 24 | 1. If there exist $f : a \mapsto b$ and $g : b \mapsto c$ then there exist 25 | $h : a \mapsto c$, such that $h = g \circ f$ (the composition). 26 | 2. For each object $x$ in the category there exist an arrow, called the 27 | identity arrow $1_x: x \mapsto x$, such that for every morphism 28 | $f : a \mapsto b$ we have $1_x \circ f = f$ and for every morphism 29 | $g : b \mapsto x$ we have $g \circ 1_x = g$. 30 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | # -- Path setup -------------------------------------------------------------- 7 | # If extensions (or modules to document with autodoc) are in another directory, 8 | # add these directories to sys.path here. If the directory is relative to the 9 | # documentation root, use os.path.abspath to make it absolute, like shown here. 10 | # 11 | # import os 12 | # import sys 13 | # sys.path.insert(0, os.path.abspath('.')) 14 | # -- Project information ----------------------------------------------------- 15 | from typing import List 16 | 17 | project = "Category Theory for Python programmers" 18 | copyright = "2021, Henrik Finsberg" 19 | author = "Henrik Finsberg" 20 | 21 | # The full version, including alpha/beta/rc tags 22 | release = "0.1.0" 23 | 24 | 25 | # -- General configuration --------------------------------------------------- 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be 28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 29 | # ones. 30 | extensions = [ 31 | "sphinx.ext.autodoc", 32 | "sphinx.ext.viewcode", 33 | "sphinx.ext.napoleon", 34 | "myst_parser", 35 | ] 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ["_templates"] 38 | 39 | # List of patterns, relative to source directory, that match files and 40 | # directories to ignore when looking for source files. 41 | # This pattern also affects html_static_path and html_extra_path. 42 | exclude_patterns: List[str] = [] 43 | 44 | 45 | # -- Options for HTML output ------------------------------------------------- 46 | 47 | # The theme to use for HTML and HTML Help pages. See the documentation for 48 | # a list of builtin themes. 49 | # 50 | html_theme = "sphinx_book_theme" 51 | 52 | # Add any paths that contain custom static files (such as style sheets) here, 53 | # relative to this directory. They are copied after the builtin static files, 54 | # so a file named "default.css" will overwrite the builtin "default.css". 55 | html_static_path = ["_static"] 56 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Category Theory for Python programmers documentation master file, created by 2 | sphinx-quickstart on Sun Oct 10 12:47:37 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Category Theory for Python programmers's documentation! 7 | ================================================================== 8 | 9 | This is my attempt to learn category theory by implementing some of the 10 | common patterns in python. 11 | 12 | .. toctree:: 13 | :maxdepth: 1 14 | 15 | about 16 | README 17 | category_theory 18 | modules 19 | 20 | 21 | 22 | Indices and tables 23 | ================== 24 | 25 | * :ref:`genindex` 26 | * :ref:`modindex` 27 | * :ref:`search` 28 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "category_theory" 7 | version = "0.1.0" 8 | description = "Learning platform for category theory in python" 9 | authors = [{name = "Henrik Finsberg", email = "henriknf@simula.no"}] 10 | license = {text = "MIT"} 11 | classifiers = [ 12 | "License :: OSI Approved :: MIT License", 13 | "Programming Language :: Python :: 3", 14 | "Programming Language :: Python :: 3 :: Only", 15 | "Programming Language :: Python :: 3.10 :: Only", 16 | ] 17 | keywords = ["category theory", "functional programming"] 18 | requires-python = ">=3.10" 19 | dependencies = ["dask"] 20 | 21 | [project.readme] 22 | file = "README.md" 23 | content-type = "text/markdown" 24 | 25 | [project.urls] 26 | Homepage = "https://github.com/finsberg/category-theory-python" 27 | 28 | [project.optional-dependencies] 29 | dev = [ 30 | "black", 31 | "bump2version", 32 | "flake8", 33 | "mypy", 34 | "pre-commit", 35 | ] 36 | doc = [ 37 | "myst-parser", 38 | "sphinx", 39 | "sphinx-book-theme", 40 | ] 41 | test = [ 42 | "hypothesis", 43 | "pytest", 44 | "pytest-cov", 45 | ] 46 | 47 | [tool.setuptools] 48 | package-dir = {"" = "src"} 49 | zip-safe = false 50 | license-files = ["LICENSE"] 51 | include-package-data = false 52 | 53 | [tool.setuptools.packages.find] 54 | where = ["src"] 55 | namespaces = false 56 | 57 | [tool.flake8] 58 | exclude = "docs" 59 | ignore = "E203, E266, E501, W503, E731" 60 | max-line-length = "88" 61 | max-complexity = "18" 62 | select = "B,C,E,F,W,T4" 63 | 64 | [tool.aliases] 65 | test = "pytest" 66 | 67 | [tool.pytest.ini_options] 68 | addopts = "--cov=src/category_theory --cov-report html --cov-report xml --cov-report term-missing --hypothesis-show-statistics -v" 69 | testpaths = ["tests"] 70 | 71 | [tool.mypy] 72 | files = ["src/category_theory"] 73 | ignore_missing_imports = true 74 | exclude = "docs" 75 | 76 | [tool.coverage.report] 77 | exclude_lines = [ 78 | "@abstractmethod", 79 | "@abc.abstractmethod", 80 | "raise NotImplementedError", 81 | ] 82 | 83 | 84 | 85 | [tool.ruff] 86 | 87 | # Exclude a variety of commonly ignored directories. 88 | exclude = [ 89 | ".bzr", 90 | ".direnv", 91 | ".eggs", 92 | ".git", 93 | ".hg", 94 | ".mypy_cache", 95 | ".nox", 96 | ".pants.d", 97 | ".pytype", 98 | ".ruff_cache", 99 | ".svn", 100 | ".tox", 101 | ".venv", 102 | "__pypackages__", 103 | "_build", 104 | "buck-out", 105 | "build", 106 | "dist", 107 | "node_modules", 108 | "venv", 109 | ] 110 | 111 | # Same as Black. 112 | line-length = 100 113 | 114 | # Assume Python 3.10. 115 | target-version = "py310" 116 | 117 | [tool.ruff.lint] 118 | # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. 119 | select = ["E", "F"] 120 | ignore = ["E402", "E741", "E743", "E731"] 121 | 122 | # Allow autofix for all enabled rules (when `--fix`) is provided. 123 | fixable = ["A", "B", "C", "D", "E", "F"] 124 | unfixable = [] 125 | 126 | # Allow unused variables when underscore-prefixed. 127 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 128 | 129 | 130 | [tool.ruff.lint.mccabe] 131 | # Unlike Flake8, default to a complexity level of 10. 132 | max-complexity = 10 133 | -------------------------------------------------------------------------------- /src/category_theory/__init__.py: -------------------------------------------------------------------------------- 1 | from . import core 2 | from . import functor 3 | from . import monoid 4 | from . import operations 5 | from .operations import fold 6 | from .operations import foldr 7 | from .par_operations import fold as par_fold 8 | 9 | # from .par_operations import foldr as par_foldr 10 | 11 | __all__ = ["core", "monoid", "functor", "operations", "fold", "foldr", "par_fold"] 12 | -------------------------------------------------------------------------------- /src/category_theory/applicative.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from . import functor 4 | from .core import Applicative 5 | from .operations import is_nothing 6 | 7 | a = typing.TypeVar("a") 8 | b = typing.TypeVar("b") 9 | 10 | 11 | class Maybe(Applicative[a], functor.Maybe[a]): 12 | @staticmethod 13 | def pure(value: a) -> "Maybe[a]": 14 | return maybe(value) 15 | 16 | 17 | class Just(Maybe, functor.Just[a]): 18 | def __init__(self, value: a) -> None: 19 | super().__init__(value) 20 | 21 | def apply(self, func: "Applicative[typing.Callable[[a], b]]") -> "Maybe[b]": 22 | if isinstance(func, _Nothing): 23 | return Nothing 24 | return maybe(func.value(self.value)) 25 | 26 | 27 | class _Nothing(Maybe, functor._Nothing[a]): 28 | def __init__(self, value: None = None) -> None: 29 | super().__init__(value) 30 | 31 | def apply(self, func: "Applicative[typing.Callable[[a], b]]") -> "Maybe[b]": 32 | return Nothing 33 | 34 | 35 | Nothing: Maybe[typing.Any] = _Nothing() 36 | 37 | 38 | def maybe(value: typing.Optional[a]) -> Maybe[a]: 39 | if is_nothing(value): 40 | return Nothing 41 | return Just(value) 42 | 43 | 44 | class Validation(Applicative): 45 | pass 46 | -------------------------------------------------------------------------------- /src/category_theory/core.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from abc import ABC 3 | from abc import abstractmethod 4 | 5 | a = typing.TypeVar("a") 6 | b = typing.TypeVar("b") 7 | 8 | 9 | class Atomic: 10 | def __init__(self, value: typing.Any) -> None: 11 | self.value = value 12 | 13 | def __repr__(self) -> str: 14 | return f"{self.__class__.__name__}({type(self.value)})" 15 | 16 | def __eq__(self, other: object) -> bool: 17 | if not isinstance(other, type(self)): 18 | return False 19 | return self.value == other.value 20 | 21 | 22 | class Monoid(ABC, typing.Generic[a], Atomic): 23 | r""" 24 | A monoid is a type :math:`a` equipped with a binary 25 | operation :math:`+ : a \times a \rightarrow a` such that the two 26 | following axioms holds: 27 | 28 | - :math:`x` is associative: :math:`(a + b) + c = a + (b + c)` 29 | 30 | - There exists an identity element :math:`e`, 31 | such that `a + e = e + a = a` 32 | 33 | In this interface the binary operation used is `+`, so that 34 | any class that wants inherit from `Monoid` need to implement 35 | `__add__` and `__radd__`. For the identity element we require 36 | a `staticmethod` called `e`. 37 | 38 | A key features of the Monoid structure which separates it from a 39 | semigroup is the presence of an identity element. 40 | """ 41 | 42 | def __init__(self, value: a) -> None: 43 | self.value = value # pragma: no cover 44 | 45 | @staticmethod 46 | @abstractmethod 47 | def e() -> typing.Any: 48 | """Identity element""" 49 | ... 50 | 51 | @abstractmethod 52 | def __add__(self, other: typing.Any): 53 | """Binary operation""" 54 | ... 55 | 56 | 57 | class CommutativeMonoid(Monoid[a]): 58 | """A CommutativeMonoid is just a Monoid 59 | where the binary operation is commutative, 60 | i.e :math:`a + b = b + a` 61 | """ 62 | 63 | pass 64 | 65 | 66 | class Functor(ABC, typing.Generic[a], Atomic): 67 | """A Functor is a mapping between categories. 68 | 69 | In programming sense we typically think of a Functor 70 | as a container, parameterized by a type `a`. 71 | 72 | Examples for Functors are Lists and Queues 73 | which are structures that we typically would think of as 74 | containers, but other examples of Functors are 75 | Maybe, Either and Promise which are less container like. 76 | The key features is that the structure contains something, and 77 | given a function, we can apply it to the thing inside our structure. 78 | Note that the thing inside the structure has type `a`. 79 | 80 | Example 81 | ------- 82 | For example `a` could be string and the functor could 83 | be `List`, in which case we have a list of strings. 84 | """ 85 | 86 | @abstractmethod 87 | def map(self, func: typing.Callable[[a], b]) -> "Functor[b]": 88 | r"""Take a function and apply it to each element in the structure 89 | 90 | Given a function 91 | 92 | .. math:: 93 | 94 | f : a \mapsto b 95 | 96 | apply :math:`f` to all elements in the structure. 97 | 98 | """ 99 | ... 100 | 101 | 102 | class Applicative(Functor[a]): 103 | """An applicative functor is a functor that 104 | 105 | Parameters 106 | ---------- 107 | Functor : [type] 108 | [description] 109 | """ 110 | 111 | @staticmethod 112 | @abstractmethod 113 | def pure(value: a) -> "Applicative[a]": ... 114 | 115 | @abstractmethod 116 | def apply( 117 | self, 118 | func: "Applicative[typing.Callable[[a], b]]", 119 | ) -> "Applicative[b]": ... 120 | -------------------------------------------------------------------------------- /src/category_theory/functor.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from .core import Functor 4 | from .operations import is_nothing 5 | 6 | a = typing.TypeVar("a") 7 | b = typing.TypeVar("b") 8 | 9 | 10 | class List(Functor[a]): 11 | def __init__(self, value: typing.List[a]) -> None: 12 | self.value = value 13 | 14 | def map(self, func: typing.Callable[[a], b]) -> "List[b]": 15 | r""" 16 | Lets suppose the functor is a `List` and `a` is `int`, so that 17 | we have a list of integers. Then one possible :math:`f` could be 18 | 19 | .. math:: 20 | 21 | f : \text{int} \mapsto \text{bool} 22 | f(x) = \begin{cases} 23 | \text{True, if } x | 2, \\ 24 | \text{False, if } x \nmid 2. 25 | \end{cases} 26 | 27 | In other :math:`f` is the function better known as *is_even*. 28 | If The list is given by 29 | 30 | .. code:: 31 | 32 | >> F = List([1, 2, 3, 3]) 33 | 34 | then 35 | 36 | .. code:: 37 | 38 | >> f = lambda x : x % 2 == 0 39 | >> F.map(f) 40 | List([False, True, False, True]) 41 | 42 | Returns 43 | ------- 44 | List[b] 45 | A new list after applying the map 46 | """ 47 | return List(list(map(func, self.value))) 48 | 49 | 50 | class Maybe(Functor[a]): 51 | """About maybe""" 52 | 53 | 54 | class Just(Maybe[a]): 55 | def __init__(self, value: a) -> None: 56 | super().__init__(value) 57 | 58 | def map(self, func: typing.Callable[[a], b]) -> "Just[b]": 59 | return Just(func(self.value)) 60 | 61 | 62 | class _Nothing(Maybe[a]): 63 | def __init__(self, value: None = None) -> None: 64 | super().__init__(value) 65 | 66 | def map(self, func: typing.Callable[[a], b]) -> "Maybe[b]": 67 | return Nothing 68 | 69 | 70 | Nothing: Maybe[typing.Any] = _Nothing() 71 | 72 | 73 | def maybe(value: a) -> Maybe[a]: 74 | if is_nothing(value): 75 | return Nothing 76 | return Just(value) 77 | -------------------------------------------------------------------------------- /src/category_theory/monoid.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from .core import CommutativeMonoid 4 | from .core import Monoid 5 | 6 | 7 | class String(Monoid[str]): 8 | """Monoid whose values are strings. 9 | Binary operation is string concatenation 10 | and identity element being the empty string 11 | """ 12 | 13 | @staticmethod 14 | def e() -> "String": 15 | return String("") 16 | 17 | def __add__(self, other: "String") -> "String": 18 | return String(self.value + other.value) 19 | 20 | 21 | class IntPlus(CommutativeMonoid[int]): 22 | """Monoid whose values are integers. 23 | Binary operation is the plus operation 24 | and identity element is 0 25 | """ 26 | 27 | @staticmethod 28 | def e() -> "IntPlus": 29 | return IntPlus(0) 30 | 31 | def __add__(self, other: "IntPlus") -> "IntPlus": 32 | return IntPlus(self.value + other.value) 33 | 34 | 35 | class IntProd(CommutativeMonoid[int]): 36 | """Monoid whose values are integers. 37 | Binary operation is the multiplication operation 38 | and identity element is 1 39 | """ 40 | 41 | @staticmethod 42 | def e() -> "IntProd": 43 | return IntProd(1) 44 | 45 | def __add__(self, other: "IntProd") -> "IntProd": 46 | return IntProd(self.value * other.value) 47 | 48 | 49 | class MaybeIntPlus(CommutativeMonoid[typing.Optional[int]]): 50 | """Monoid whose values are maybe integers. 51 | This means that the value can be int or None. 52 | Binary operation is the plus operation is the 53 | value is of type int and returns None otherwise. 54 | Identity element is MaybeIntPlus(0) 55 | """ 56 | 57 | @staticmethod 58 | def e() -> "MaybeIntPlus": 59 | return MaybeIntPlus(0) 60 | 61 | def __add__(self, other: "MaybeIntPlus") -> "MaybeIntPlus": 62 | if self.value is None: 63 | return MaybeIntPlus(None) 64 | if other.value is None: 65 | return MaybeIntPlus(None) 66 | return MaybeIntPlus(self.value + other.value) 67 | 68 | 69 | class MaybeIntProd(CommutativeMonoid[typing.Optional[int]]): 70 | """Monoid whose values are maybe integers. 71 | This means that the value can be int or None. 72 | Binary operation is the multiplication operation is the 73 | value is of type int and returns None otherwise. 74 | Identity element is MaybeIntProd(1) 75 | """ 76 | 77 | @staticmethod 78 | def e() -> "MaybeIntProd": 79 | return MaybeIntProd(1) 80 | 81 | def __add__(self, other: "MaybeIntProd") -> "MaybeIntProd": 82 | if self.value is None: 83 | return MaybeIntProd(None) 84 | if other.value is None: 85 | return MaybeIntProd(None) 86 | return MaybeIntProd(self.value * other.value) 87 | 88 | 89 | class Any(CommutativeMonoid[bool]): 90 | @staticmethod 91 | def e() -> "Any": 92 | return Any(False) 93 | 94 | def __add__(self, other: "Any") -> "Any": 95 | return Any(self.value or other.value) 96 | 97 | 98 | class All(CommutativeMonoid[bool]): 99 | @staticmethod 100 | def e() -> "All": 101 | return All(True) 102 | 103 | def __add__(self, other: "All") -> "All": 104 | return All(self.value and other.value) 105 | -------------------------------------------------------------------------------- /src/category_theory/operations.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import math 3 | import typing 4 | 5 | from .core import Monoid 6 | 7 | a = typing.TypeVar("a") 8 | b = typing.TypeVar("b") 9 | c = typing.TypeVar("c") 10 | 11 | 12 | def identity(x: a) -> a: 13 | r"""Identity function 14 | 15 | .. math:: 16 | 17 | f : a \mapsto a \\ 18 | f(x) = x 19 | 20 | Parameters 21 | ---------- 22 | x : a 23 | Input variable 24 | 25 | Returns 26 | ------- 27 | a 28 | Output variable :math:`f(x)` 29 | """ 30 | return x 31 | 32 | 33 | def is_nothing(x: typing.Optional[a]) -> typing.TypeGuard[a]: 34 | """Check if a variable should be declared as Nothing. 35 | 36 | Parameters 37 | ---------- 38 | x : typing.Optional[a] 39 | The variable 40 | 41 | Returns 42 | ------- 43 | typing.TypeGuard[a] 44 | Returns true if the variable is nothing and false otherwise. 45 | Note that er make use of TypGuard here which tell the type 46 | checker than if the thing we get in is None, then this 47 | function will return True 48 | """ 49 | if x is None: 50 | return True 51 | if isinstance(x, float): 52 | return math.isinf(x) or math.isnan(x) 53 | return False 54 | 55 | 56 | def compose( 57 | g: typing.Callable[[b], c], 58 | f: typing.Callable[[a], b], 59 | ) -> typing.Callable[[a], c]: 60 | r"""Compose two functions :math:`g` and :math:`f`, :math:`g \circ f`. 61 | 62 | We have the following two functions 63 | 64 | .. math:: 65 | 66 | g: b \mapsto c \\ 67 | f: a \mapsto b 68 | 69 | and through this function we create the composition 70 | 71 | .. math:: 72 | 73 | g \circ f: a \mapsto c 74 | 75 | Parameters 76 | ---------- 77 | g : typing.Callable[[b], c] 78 | Second function to be applied 79 | f : typing.Callable[[a], b] 80 | First function to be applied 81 | 82 | Returns 83 | ------- 84 | typing.Callable[[a], c] 85 | The composition of `f` and `g` 86 | """ 87 | return lambda x: g(f(x)) 88 | 89 | 90 | def fold(lst: typing.Iterable[Monoid], cls: typing.Type) -> typing.Any: 91 | """Fold an iterable of Monoids together using the identity 92 | element as initial. 93 | 94 | Parameters 95 | ---------- 96 | lst : typing.Iterable[Monoid] 97 | An iterable of monoids that should be squashed 98 | cls : typing.Type 99 | The type of the monoid 100 | 101 | Returns 102 | ------- 103 | typing.Any 104 | The reduction of the iterable. 105 | """ 106 | 107 | return functools.reduce(lambda x, y: x + y, lst, cls.e()) 108 | 109 | 110 | def foldr(lst: typing.Iterable[Monoid], cls: typing.Type) -> typing.Any: 111 | """Same as `fold`, but from the right 112 | 113 | Parameters 114 | ---------- 115 | lst : typing.Iterable[Monoid] 116 | An iterable of monoids that should be squashed 117 | cls : typing.Type 118 | The type of the monoid 119 | 120 | Returns 121 | ------- 122 | typing.Any 123 | The reduction of the iterable. 124 | """ 125 | return functools.reduce(lambda x, y: y + x, reversed(tuple(lst)), cls.e()) 126 | -------------------------------------------------------------------------------- /src/category_theory/par_operations.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import typing 3 | from itertools import zip_longest 4 | 5 | import dask 6 | 7 | from .core import Monoid 8 | 9 | 10 | def chunkify( 11 | chunk_size: int, 12 | iterable: typing.Iterable[typing.Any], 13 | fillvalue: typing.Any = None, 14 | ) -> typing.Iterable[typing.Iterable[typing.Any]]: 15 | """Split iterable into chunks of size `chunk_size`. 16 | If all chunks does not add up, it will use the 17 | `fillvalue` in the remaining spots 18 | 19 | Parameters 20 | ---------- 21 | chunk_size : int 22 | Number of elements in each chunk 23 | iterable : typing.Iterable[typing.Any] 24 | The iterable that should be chunkified 25 | fillvalue : typing.Any, optional 26 | A value to put in those places when the 27 | chunk size does not add up, by default None 28 | 29 | Returns 30 | ------- 31 | typing.Iterable[typing.Iterable[typing.Any]] 32 | A list of new iterables, each being of size `chunk_size`. 33 | 34 | Example 35 | ------- 36 | 37 | .. code:: python 38 | 39 | >> iterable = (1, 2, 3, 4, 5) 40 | >> chunkify(3, iterable, fillvalue=None) 41 | ((1, 2, 3), (4, 5, None)) 42 | 43 | """ 44 | args = [iter(iterable)] * chunk_size 45 | return zip_longest(fillvalue=fillvalue, *args) 46 | 47 | 48 | def fold( 49 | iterable: typing.Iterable[Monoid], 50 | cls: typing.Type, 51 | chunk_size=1000, 52 | ) -> typing.Any: 53 | """Fold an iterable of Monoids together using the identity 54 | element as initial. 55 | 56 | Parameters 57 | ---------- 58 | iterable : typing.Iterable[Monoid] 59 | An iterable of monoids that should be squashed 60 | cls : typing.Type 61 | The type of the monoid 62 | 63 | Returns 64 | ------- 65 | typing.Any 66 | The reduction of the iterable. 67 | """ 68 | output = [] 69 | for chunk in chunkify(chunk_size, iterable, fillvalue=cls.e()): 70 | future = dask.delayed(functools.reduce)(lambda x, y: x + y, chunk, cls.e()) 71 | output.append(future) 72 | 73 | return dask.delayed(functools.reduce)(lambda x, y: x + y, output, cls.e()) 74 | -------------------------------------------------------------------------------- /tests/test_applicative.py: -------------------------------------------------------------------------------- 1 | from category_theory import applicative 2 | 3 | 4 | def test_maybe_just_just(): 5 | maybe = applicative.Maybe.pure(3) 6 | func = applicative.Maybe.pure(lambda x: x + 1) 7 | new_maybe = maybe.apply(func) 8 | assert new_maybe == applicative.Just(4) 9 | 10 | 11 | def test_maybe_just_nothing(): 12 | maybe = applicative.Maybe.pure(3) 13 | func = applicative.Maybe.pure(None) 14 | new_maybe = maybe.apply(func) 15 | assert new_maybe == applicative.Nothing 16 | 17 | 18 | def test_maybe_nothing_just(): 19 | maybe = applicative.Maybe.pure(None) 20 | func = applicative.Maybe.pure(lambda x: x + 1) 21 | new_maybe = maybe.apply(func) 22 | assert new_maybe == applicative.Nothing 23 | -------------------------------------------------------------------------------- /tests/test_functor.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from category_theory import functor 4 | from category_theory.operations import compose 5 | from category_theory.operations import identity 6 | from hypothesis import given 7 | from hypothesis import strategies as st 8 | 9 | a = typing.Union[str, int, float, bool] 10 | b = typing.Union[str, int, float, bool] 11 | 12 | monoids = (st.integers(), st.booleans(), st.characters(), st.floats(allow_nan=False)) 13 | 14 | all_lists = st.lists(st.one_of(*monoids)) 15 | 16 | 17 | def test_List(): 18 | lst = functor.List([1, 2, 3, 4]) 19 | is_even = lambda x: x % 2 == 0 20 | new_lst = lst.map(is_even) 21 | assert new_lst.value == [False, True, False, True] 22 | 23 | 24 | @given(all_lists) 25 | def test_List_identity(x): 26 | lst = functor.List(x) 27 | assert identity(lst) == lst.map(identity) 28 | 29 | 30 | @st.composite 31 | def chained_functions(draw, elements=st.integers()): 32 | def f(x: int) -> float: # type: ignore 33 | ... 34 | 35 | def g(y: float) -> str: # type: ignore 36 | ... 37 | 38 | func1 = draw(st.functions(like=f, pure=True, returns=st.floats())) 39 | func2 = draw(st.functions(like=g, pure=True, returns=st.characters())) 40 | return (func1, func2) 41 | 42 | 43 | @given(chained_functions(), st.lists(st.integers())) 44 | def test_List_composition(fg, x): 45 | f, g = fg 46 | lst = functor.List(x) 47 | assert lst.map(f).map(g) == lst.map(compose(g, f)) 48 | 49 | 50 | @given(st.one_of(*monoids)) 51 | def test_Maybe_identity(x): 52 | maybe = functor.maybe(x) 53 | assert identity(maybe) == maybe.map(identity) 54 | 55 | 56 | @given(chained_functions(), st.one_of(st.integers(), st.none())) 57 | def test_Maybe_composition(fg, x): 58 | f, g = fg 59 | maybe = functor.maybe(x) 60 | lhs = maybe.map(f).map(g) 61 | rhs = maybe.map(compose(g, f)) 62 | assert lhs == rhs, f"lhs = {lhs.value}, rhs = {rhs.value}" 63 | 64 | 65 | def test_Maybe_int_just(): 66 | maybe = functor.maybe(2) 67 | is_even = lambda x: x % 2 == 0 68 | new_maybe = maybe.map(is_even) 69 | assert new_maybe == functor.maybe(True) 70 | 71 | 72 | def test_Maybe_int_Nothing(): 73 | maybe = functor.maybe(None) 74 | is_even = lambda x: x % 2 == 0 75 | new_maybe = maybe.map(is_even) 76 | assert new_maybe == functor.Nothing 77 | 78 | 79 | def test_Maybe_repr(): 80 | assert repr(functor.maybe(1)) == "Just()" 81 | 82 | 83 | def test_Maybe_comparison_with_different_types(): 84 | assert functor.maybe(1) != functor.maybe("1") 85 | 86 | 87 | def test_comparison_with_different_types(): 88 | assert functor.maybe(1) != functor.List([1]) 89 | -------------------------------------------------------------------------------- /tests/test_monoid.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import itertools 3 | import operator 4 | 5 | import pytest 6 | from category_theory import monoid 7 | from category_theory import operations as op 8 | from category_theory import par_operations as parop 9 | from hypothesis import given 10 | from hypothesis import strategies as st 11 | 12 | 13 | @pytest.mark.parametrize("cls", [monoid.IntPlus, monoid.IntProd, monoid.MaybeIntPlus]) 14 | @given(st.integers(), st.integers(), st.integers()) 15 | def test_integer_Monoid_is_associative(cls, a_, b_, c_): 16 | a = cls(a_) 17 | b = cls(b_) 18 | c = cls(c_) 19 | assert a + (b + c) == (a + b) + c == a + b + c 20 | 21 | 22 | @pytest.mark.parametrize("cls", [monoid.All, monoid.Any]) 23 | @given(st.booleans(), st.booleans(), st.booleans()) 24 | def test_boolean_Monoid_is_associative(cls, a_, b_, c_): 25 | a = cls(a_) 26 | b = cls(b_) 27 | c = cls(c_) 28 | assert a + (b + c) == (a + b) + c == a + b + c 29 | 30 | 31 | @given(st.text(), st.text(), st.text()) 32 | def test_String_is_associative(a_, b_, c_): 33 | a = monoid.String(a_) 34 | b = monoid.String(b_) 35 | c = monoid.String(c_) 36 | assert a + (b + c) == (a + b) + c == a + b + c 37 | 38 | 39 | @pytest.mark.parametrize( 40 | "cls", 41 | [monoid.IntPlus, monoid.IntProd, monoid.MaybeIntPlus, monoid.MaybeIntProd], 42 | ) 43 | @given(st.integers()) 44 | def test_integer_Monoid_identity(cls, a_): 45 | a = cls(a_) 46 | e = cls.e() 47 | assert a + e == e + a == a 48 | 49 | 50 | @pytest.mark.parametrize("cls", [monoid.All, monoid.Any]) 51 | @given(st.booleans()) 52 | def test_boolean_Monoid_identity(cls, a_): 53 | a = cls(a_) 54 | e = cls.e() 55 | assert a + e == e + a == a 56 | 57 | 58 | @given(st.text()) 59 | def test_String_identity(a_): 60 | a = monoid.String(a_) 61 | e = monoid.String.e() 62 | assert a + e == e + a == a 63 | 64 | 65 | @pytest.mark.parametrize("func", [op.fold, op.foldr]) 66 | @given(st.iterables(st.integers())) 67 | def test_IntPlus_fold(func, values): 68 | values, values_copy = itertools.tee(values) 69 | value = func((monoid.IntPlus(v) for v in values), monoid.IntPlus) 70 | assert value == monoid.IntPlus(sum(values_copy)) 71 | 72 | 73 | @st.composite 74 | def maybeint(draw, elements=st.integers()): 75 | values = draw(st.lists(elements)) 76 | index = draw(st.integers(min_value=0, max_value=max(len(values) - 1, 0))) 77 | maybe = draw(st.one_of(st.integers(), st.none())) 78 | if len(values) > 0: 79 | values[index] = maybe 80 | return values 81 | 82 | 83 | @pytest.mark.parametrize("func", [op.fold, op.foldr]) 84 | @given(maybeint()) 85 | def test_MaybeIntPlus_fold(func, values): 86 | value = func((monoid.MaybeIntPlus(v) for v in values), monoid.MaybeIntPlus) 87 | if None in values: 88 | true_value = None 89 | else: 90 | true_value = sum(values) 91 | assert value == monoid.MaybeIntPlus(true_value) 92 | 93 | 94 | @pytest.mark.parametrize("func", [op.fold, op.foldr]) 95 | @given(maybeint()) 96 | def test_MaybeIntProd_fold(func, values): 97 | value = func((monoid.MaybeIntProd(v) for v in values), monoid.MaybeIntProd) 98 | if None in values: 99 | true_value = None 100 | else: 101 | true_value = functools.reduce( 102 | operator.mul, 103 | values, 104 | monoid.MaybeIntProd.e().value, 105 | ) 106 | assert value == monoid.MaybeIntProd(true_value) 107 | 108 | 109 | def test_IntPlus_par(): 110 | values = list(range(100 * 5)) 111 | value = parop.fold( 112 | (monoid.IntPlus(v) for v in values), 113 | monoid.IntPlus, 114 | chunk_size=99, 115 | ).compute() 116 | assert value == monoid.IntPlus(sum(values)) 117 | 118 | 119 | @pytest.mark.parametrize( 120 | "func", 121 | (op.fold, op.foldr), 122 | ) 123 | @given(st.iterables(st.integers())) 124 | def test_IntProd_fold(func, values): 125 | values, values_copy = itertools.tee(values) 126 | value = func((monoid.IntProd(v) for v in values), monoid.IntProd) 127 | assert value == monoid.IntProd( 128 | functools.reduce(operator.mul, values_copy, monoid.IntProd.e().value), 129 | ) 130 | 131 | 132 | def test_MaybeIntPlus(): 133 | values = [1, None, 3] 134 | value = op.fold((monoid.MaybeIntPlus(v) for v in values), monoid.MaybeIntPlus) 135 | assert value == monoid.MaybeIntPlus(None) 136 | 137 | 138 | @given(st.iterables(st.booleans())) 139 | def test_All_fold(values): 140 | values, values_copy = itertools.tee(values) 141 | value = op.fold((monoid.All(v) for v in values), monoid.All) 142 | assert value == monoid.All(all(values_copy)) 143 | 144 | 145 | @given(st.iterables(st.booleans())) 146 | def test_Any_fold(values): 147 | values, values_copy = itertools.tee(values) 148 | value = op.fold((monoid.Any(v) for v in values), monoid.Any) 149 | assert value == monoid.Any(any(values_copy)) 150 | 151 | 152 | @pytest.mark.parametrize( 153 | "func", 154 | (op.fold, op.foldr), 155 | ) 156 | @given(st.iterables(st.text())) 157 | def test_String_fold(func, values): 158 | values, values_copy = itertools.tee(values) 159 | value = func((monoid.String(v) for v in values), monoid.String) 160 | assert value == monoid.String("".join(values_copy)) 161 | 162 | 163 | @st.composite 164 | def string_chunk_size(draw, elements=st.text()): 165 | values = draw(st.lists(elements)) 166 | chunk_size = draw(st.integers(min_value=1, max_value=max(len(values), 1))) 167 | return (values, chunk_size) 168 | 169 | 170 | @given(string_chunk_size()) 171 | def test_String_par_fold(data): 172 | values, chunk_size = data 173 | value = parop.fold( 174 | (monoid.String(v) for v in values), 175 | monoid.String, 176 | chunk_size=chunk_size, 177 | ).compute() 178 | assert value == monoid.String("".join(values)) 179 | -------------------------------------------------------------------------------- /tests/test_operations.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from category_theory import operations as op 3 | from hypothesis import given 4 | from hypothesis import strategies as st 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "f, g, h", 9 | [ 10 | (lambda x: x + 2, lambda x: x - 4, lambda x: x - 2), 11 | ( 12 | lambda x: (x + 1) ** 2, 13 | lambda x: 2 * (x + 1), 14 | lambda x: 2 * ((x + 1) ** 2 + 1), 15 | ), 16 | ], 17 | ) 18 | @given(st.integers()) 19 | def test_compose(f, g, h, value): 20 | gf = op.compose(g, f) 21 | assert gf(value) == h(value) 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "x, expected", 26 | [ 27 | (None, True), 28 | (float("nan"), True), 29 | (float("inf"), True), 30 | (1, False), 31 | (0, False), 32 | ("", False), 33 | (False, False), 34 | (True, False), 35 | ], 36 | ) 37 | def test_is_nothing(x, expected): 38 | assert op.is_nothing(x) is expected 39 | -------------------------------------------------------------------------------- /tests/test_par_operations.py: -------------------------------------------------------------------------------- 1 | from category_theory import par_operations as parop 2 | 3 | 4 | def test_chunkify(): 5 | iterable = (1, 2, 3, 4, 5) 6 | chunks = parop.chunkify(3, iterable, fillvalue=None) 7 | assert tuple(chunks) == ((1, 2, 3), (4, 5, None)) 8 | --------------------------------------------------------------------------------