├── .flake8 ├── .gitattributes ├── .github └── workflows │ ├── python-package.yml │ └── python-publish.yml ├── .gitignore ├── .pylintrc ├── LICENSE ├── MANIFEST.in ├── README.md ├── examples ├── hello.py ├── hello2.py ├── hello3.py └── issue.py ├── notebooks ├── README.md └── Reader.ipynb ├── oslash ├── __init__.py ├── _version.py ├── cont.py ├── do.py ├── either.py ├── identity.py ├── ioaction.py ├── list.py ├── maybe.py ├── monadic.py ├── observable.py ├── reader.py ├── state.py ├── typing │ ├── __init__.py │ ├── applicative.py │ ├── functor.py │ ├── monad.py │ └── monoid.py ├── util │ ├── __init__.py │ ├── basic.py │ ├── fn.py │ └── numerals.py └── writer.py ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── test_cont.py ├── test_do.py ├── test_either.py ├── test_identity.py ├── test_ioaction.py ├── test_list.py ├── test_maybe.py ├── test_monad.py ├── test_numerals.py ├── test_observable.py ├── test_reader.py ├── test_state.py ├── test_util.py └── test_writer.py └── versioneer.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E731 # Do not assign a lambda expression, use a def 3 | max-line-length = 120 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | oslash/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: [3.8] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install flake8 pytest 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | - name: Lint with flake8 32 | run: | 33 | # stop the build if there are Python syntax errors or undefined names 34 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 35 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 36 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 37 | - name: Test with pytest 38 | run: | 39 | pytest 40 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | dist 3 | build 4 | .zedstate 5 | .idea 6 | *.suo 7 | *.egg-info 8 | *.pyc 9 | *.user 10 | .ipynb_checkpoints 11 | 12 | .coverage 13 | .cache/ 14 | .mypy_cache/ 15 | .vscode/ 16 | 17 | 18 | coverage.xml 19 | .eggs 20 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [BASIC] 2 | 3 | good-names=n,x,xs,fn,T_in,T_out 4 | 5 | [MESSAGES CONTROL] 6 | 7 | disable=C0111 8 | 9 | [FORMAT] 10 | 11 | # Maximum number of characters on a single line. 12 | max-line-length=120 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Dag Brattli 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include versioneer.py 2 | include oslash/_version.py 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Functors, Applicatives, And Monads in Python 2 | 3 | ![Python package](https://github.com/dbrattli/OSlash/workflows/Python%20package/badge.svg) 4 | 5 | OSlash (Ø) is a library for playing with functional programming in Python 3.8+. It's an attempt to re-implement some of 6 | the code from [Learn You a Haskell for Great Good!](http://learnyouahaskell.com/) in Python 3.8. OSlash unifies 7 | functional and object oriented paradigms by grouping related functions within classes. Objects are however never used 8 | for storing values or mutable data, and data only lives within function closures. 9 | 10 | OSlash is intended to be a tutorial. For practical functional programming in Python in production environments you 11 | should use [Expression](https://github.com/dbrattli/Expression) instead. 12 | 13 | ## Install 14 | 15 | ```bash 16 | > pip3 install oslash 17 | ``` 18 | 19 | The project currently contains implementations for: 20 | 21 | ## Abstract Base Classes 22 | 23 | - **[Functor](https://github.com/dbrattli/OSlash/wiki/Functors,-Applicatives,-And-Monads-In-Pictures#functors)**, for stuff that can be mapped 24 | - **[Applicative](https://github.com/dbrattli/OSlash/wiki/Functors,-Applicatives,-And-Monads-In-Pictures#applicatives)**, for callable stuff 25 | - **Monoid**, for associative stuff 26 | - **[Monad](https://github.com/dbrattli/OSlash/wiki/Functors,-Applicatives,-And-Monads-In-Pictures#monads)**, for monadic stuff 27 | 28 | ## And Some Monads 29 | 30 | - **Identity**, boxed stuff in its simplest form 31 | - **[Maybe (Just | Nothing)](https://github.com/dbrattli/oslash/wiki/Functors,-Applicatives,-And-Monads-In-Pictures)**, for optional stuff 32 | - **Either (Right | Left)**, for possible failures 33 | - **List**, purely functional list of stuff 34 | - **[IO Action](https://github.com/dbrattli/OSlash/wiki/Functors,-Applicatives,-And-Monads-In-Pictures#io-monad)**, for impure stuff 35 | - **[Writer](https://github.com/dbrattli/OSlash/wiki/Three-Useful-Monads#the-writer-monad)**, for logging stuff 36 | - **[Reader](https://github.com/dbrattli/OSlash/wiki/Three-Useful-Monads#the-reader-monad)**, for callable stuff 37 | - **State**, for stateful computations of stuff 38 | - **Cont**, for continuation of stuff 39 | 40 | ## Monadic functions 41 | 42 | - **>>**, for sequencing monadic actions 43 | - **lift**, for mapping a function over monadic values 44 | - **join**, for removing one level of monadic structure 45 | - **compose**, for composing monadic functions 46 | 47 | ## Utility functions 48 | 49 | - **compose**, for composing 0 to n functions 50 | 51 | ## But why? 52 | 53 | Yes, I know there are other projects out there like [PyMonad](https://bitbucket.org/jason_delaat/pymonad/), 54 | [fn.py](https://github.com/kachayev/fn.py). I'm simply doing this in order to better understand the 55 | [book](http://learnyouahaskell.com/). It's so much easier to learn when you implement things yourself. The code may be 56 | similar to PyMonad in structure, but is quite different in implementation. 57 | 58 | Why is the project called OSlash? OSlash is the Norwegian character called [Oslash](http://en.wikipedia.org/wiki/Ø). 59 | Initially I wanted to create a project that used Ø and ø (unicode) for the project name and modules. It didn't work out 60 | well, so I renamed it to OSlash. 61 | 62 | ## Examples 63 | 64 | Haskell: 65 | 66 | ```haskell 67 | > fmap (+3) (Just 2) 68 | Just 5 69 | 70 | > (+3) <$> (Just 2) 71 | Just 5 72 | ``` 73 | 74 | Python: 75 | 76 | ```python 77 | >>> Just(2).map(lambda x: x+3) 78 | Just 5 79 | 80 | >>> (lambda x: x+3) % Just(2) 81 | Just 5 82 | 83 | ``` 84 | 85 | IO Actions: 86 | 87 | ```python 88 | from oslash import put_line, get_line 89 | 90 | main = put_line("What is your name?") | (lambda _: 91 | get_line() | (lambda name: 92 | put_line("What is your age?") | (lambda _: 93 | get_line() | (lambda age: 94 | put_line("Hello " + name + "!") | (lambda _: 95 | put_line("You are " + age + " years old")))))) 96 | 97 | if __name__ == "__main__": 98 | main() 99 | ``` 100 | 101 | ## Tutorials 102 | 103 | - [Functors, Applicatives, And Monads In Pictures](https://github.com/dbrattli/oslash/wiki/Functors,-Applicatives,-And-Monads-In-Pictures) in Python. 104 | - [Three Useful Monads](https://github.com/dbrattli/OSlash/wiki/Three-Useful-Monads) _(in progress)_ 105 | - [Using Either monad in Python](https://medium.com/@rnesytov/using-either-monad-in-python-b6eac698dff5) 106 | -------------------------------------------------------------------------------- /examples/hello.py: -------------------------------------------------------------------------------- 1 | """Hello example using bind.""" 2 | from oslash import put_line, get_line 3 | 4 | main = ( 5 | put_line("What is your name?") 6 | | (lambda _: get_line() 7 | | (lambda name: put_line("What is your age?") 8 | | (lambda _: get_line() 9 | | (lambda age: put_line("Hello " + name + "!") 10 | | (lambda _: put_line("You are " + age + " years old") 11 | )))))) 12 | 13 | if __name__ == "__main__": 14 | #print(main) 15 | main() 16 | -------------------------------------------------------------------------------- /examples/hello2.py: -------------------------------------------------------------------------------- 1 | """Hello example as expression tree.""" 2 | from oslash import Put, Get, Return, Unit 3 | 4 | main = Put("What is your name?", 5 | Get(lambda name: 6 | Put("What is your age?", 7 | Get(lambda age: 8 | Put("Hello " + name + "!", 9 | Put("You are " + age + " years old", 10 | Return(Unit) 11 | ) 12 | ) 13 | ) 14 | ) 15 | ) 16 | ) 17 | 18 | if __name__ == "__main__": 19 | print(main) 20 | -------------------------------------------------------------------------------- /examples/hello3.py: -------------------------------------------------------------------------------- 1 | """Hello example using do-notation.""" 2 | from oslash import put_line, get_line, do, let 3 | 4 | main = do( 5 | put_line("What is your name?"), 6 | let(name=get_line()), 7 | put_line("What is your age?"), 8 | let(age=get_line()), 9 | lambda e: put_line("Hello " + e.name + "!"), 10 | lambda e: put_line("You are " + e.age + " years old") 11 | ) 12 | 13 | if __name__ == "__main__": 14 | #print(main) 15 | main() 16 | -------------------------------------------------------------------------------- /examples/issue.py: -------------------------------------------------------------------------------- 1 | from oslash import List 2 | 3 | IntList = List[int] 4 | 5 | 6 | def f() -> IntList: 7 | """Return list..""" 8 | return List.unit(0) 9 | -------------------------------------------------------------------------------- /notebooks/README.md: -------------------------------------------------------------------------------- 1 | Here are some [IPython](http://ipython.org) notebooks. You can view them 2 | using [NBViewer](http://nbviewer.ipython.org): 3 | 4 | * [The Reader Monad](http://nbviewer.ipython.org/github/dbrattli/OSlash/blob/master/notebooks/Reader.ipynb) 5 | -------------------------------------------------------------------------------- /notebooks/Reader.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# The Reader Monad\n", 8 | "\n", 9 | "The Reader monad pass the state you want to share between functions. Functions may read that state, but can't change it. The reader monad lets us access shared immutable state within a monadic context. In the Reader monad this shared state is called the environment.\n", 10 | "\n", 11 | "The Reader is just a fancy name for a wrapped function, so this monad could also be called the Function monad, or perhaps the Callable monad. Reader is all about composing wrapped functions.\n", 12 | "\n", 13 | "This [IPython](http://ipython.org) notebook uses the [OSlash](https://github.com/dbrattli/OSlash) library for Python 3.4, aka Ø. You can install Ø using:\n", 14 | "\n", 15 | "```bash\n", 16 | "> pip3 install oslash\n", 17 | "```" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": 106, 23 | "metadata": { 24 | "collapsed": true 25 | }, 26 | "outputs": [], 27 | "source": [ 28 | "from oslash import Reader\n", 29 | "unit = Reader.unit" 30 | ] 31 | }, 32 | { 33 | "cell_type": "markdown", 34 | "metadata": {}, 35 | "source": [ 36 | "A Reader wraps a function, so it takes a callable:" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": 107, 42 | "metadata": { 43 | "collapsed": false 44 | }, 45 | "outputs": [], 46 | "source": [ 47 | "r = Reader(lambda name: \"Hi %s!\" % name)" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "In Python you can call this wrapped function as any other callable:" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 108, 60 | "metadata": { 61 | "collapsed": false 62 | }, 63 | "outputs": [ 64 | { 65 | "data": { 66 | "text/plain": [ 67 | "'Hi Dag!'" 68 | ] 69 | }, 70 | "execution_count": 108, 71 | "metadata": {}, 72 | "output_type": "execute_result" 73 | } 74 | ], 75 | "source": [ 76 | "r(\"Dag\")" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "metadata": {}, 82 | "source": [ 83 | "## Unit\n", 84 | "\n", 85 | "Unit is a constructor that takes a value and returns a Reader that ignores the environment. That is it ignores any value that is passed to the Reader when it's called:" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": 109, 91 | "metadata": { 92 | "collapsed": false 93 | }, 94 | "outputs": [ 95 | { 96 | "data": { 97 | "text/plain": [ 98 | "42" 99 | ] 100 | }, 101 | "execution_count": 109, 102 | "metadata": {}, 103 | "output_type": "execute_result" 104 | } 105 | ], 106 | "source": [ 107 | "r = unit(42)\n", 108 | "r(\"Ignored\")" 109 | ] 110 | }, 111 | { 112 | "cell_type": "markdown", 113 | "metadata": {}, 114 | "source": [ 115 | "## Bind\n", 116 | "\n", 117 | "You can bind a Reader to a monadic function using the pipe `|` operator (The bind operator is called `>>=` in Haskell). A monadic function is a function that takes a value and returns a monad, and in this case it returns a new Reader monad:" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": 117, 123 | "metadata": { 124 | "collapsed": false 125 | }, 126 | "outputs": [ 127 | { 128 | "data": { 129 | "text/plain": [ 130 | "'Hello Dag!'" 131 | ] 132 | }, 133 | "execution_count": 117, 134 | "metadata": {}, 135 | "output_type": "execute_result" 136 | } 137 | ], 138 | "source": [ 139 | "r = Reader(lambda name: \"Hi %s!\" % name)\n", 140 | "\n", 141 | "b = r | (lambda x: unit(x.replace(\"Hi\", \"Hello\")))\n", 142 | "b(\"Dag\")" 143 | ] 144 | }, 145 | { 146 | "cell_type": "markdown", 147 | "metadata": {}, 148 | "source": [ 149 | "## Applicative\n", 150 | "\n", 151 | "Apply (`*`) is a beefed up `map`. It takes a Reader that has a function in it and another Reader, and extracts that function from the first Reader and then maps it over the second one (basically composes the two functions)." 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": 122, 157 | "metadata": { 158 | "collapsed": false 159 | }, 160 | "outputs": [ 161 | { 162 | "data": { 163 | "text/plain": [ 164 | "'Hi Dag!!!!'" 165 | ] 166 | }, 167 | "execution_count": 122, 168 | "metadata": {}, 169 | "output_type": "execute_result" 170 | } 171 | ], 172 | "source": [ 173 | "r = Reader(lambda name: \"Hi %s!\" % name)\n", 174 | "\n", 175 | "a = Reader.pure(lambda x: x + \"!!!\") * r\n", 176 | "a(\"Dag\")" 177 | ] 178 | }, 179 | { 180 | "cell_type": "markdown", 181 | "metadata": {}, 182 | "source": [ 183 | "# MonadReader\n", 184 | "\n", 185 | "The MonadReader class provides a number of convenience functions that are very useful when working with a Reader monad." 186 | ] 187 | }, 188 | { 189 | "cell_type": "code", 190 | "execution_count": 112, 191 | "metadata": { 192 | "collapsed": true 193 | }, 194 | "outputs": [], 195 | "source": [ 196 | "from oslash import MonadReader\n", 197 | "asks = MonadReader.asks\n", 198 | "ask = MonadReader.ask" 199 | ] 200 | }, 201 | { 202 | "cell_type": "markdown", 203 | "metadata": {}, 204 | "source": [ 205 | "## Ask\n", 206 | "\n", 207 | "Provides a way to easily access the environment. Ask lets us read the environment and then play with it:" 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": 113, 213 | "metadata": { 214 | "collapsed": false 215 | }, 216 | "outputs": [ 217 | { 218 | "data": { 219 | "text/plain": [ 220 | "'Hi Dag!'" 221 | ] 222 | }, 223 | "execution_count": 113, 224 | "metadata": {}, 225 | "output_type": "execute_result" 226 | } 227 | ], 228 | "source": [ 229 | "r = ask() | (lambda x: unit(\"Hi %s!\" % x))\n", 230 | "r(\"Dag\")" 231 | ] 232 | }, 233 | { 234 | "cell_type": "markdown", 235 | "metadata": {}, 236 | "source": [ 237 | "## Asks\n", 238 | "\n", 239 | "Given a function it returns a Reader which evaluates that function and returns the result." 240 | ] 241 | }, 242 | { 243 | "cell_type": "code", 244 | "execution_count": 114, 245 | "metadata": { 246 | "collapsed": false 247 | }, 248 | "outputs": [ 249 | { 250 | "data": { 251 | "text/plain": [ 252 | "6" 253 | ] 254 | }, 255 | "execution_count": 114, 256 | "metadata": {}, 257 | "output_type": "execute_result" 258 | } 259 | ], 260 | "source": [ 261 | "r = asks(len)\n", 262 | "r(\"banana\")" 263 | ] 264 | }, 265 | { 266 | "cell_type": "markdown", 267 | "metadata": {}, 268 | "source": [ 269 | "## A Longer Example\n", 270 | "\n", 271 | "This example has been translated to Python from https://gist.github.com/egonSchiele/5752172." 272 | ] 273 | }, 274 | { 275 | "cell_type": "code", 276 | "execution_count": 115, 277 | "metadata": { 278 | "collapsed": false 279 | }, 280 | "outputs": [ 281 | { 282 | "name": "stdout", 283 | "output_type": "stream", 284 | "text": [ 285 | "Hello, dag! Bye, dag!\n" 286 | ] 287 | } 288 | ], 289 | "source": [ 290 | "from oslash import Reader, MonadReader\n", 291 | "ask = MonadReader.ask\n", 292 | " \n", 293 | "def hello():\n", 294 | " return ask() | (lambda name: \n", 295 | " unit(\"Hello, \" + name + \"!\"))\n", 296 | " \n", 297 | "def bye():\n", 298 | " return ask() | (lambda name: \n", 299 | " unit(\"Bye, \" + name + \"!\"))\n", 300 | " \n", 301 | "def convo():\n", 302 | " return hello() | (lambda c1: \n", 303 | " bye() | (lambda c2: \n", 304 | " unit(\"%s %s\" % (c1, c2))))\n", 305 | "\n", 306 | "r = convo()\n", 307 | "print(r(\"dag\"))" 308 | ] 309 | }, 310 | { 311 | "cell_type": "markdown", 312 | "metadata": {}, 313 | "source": [ 314 | "_That is it, that's the Reader monad for you in Python and Ø!_" 315 | ] 316 | }, 317 | { 318 | "cell_type": "code", 319 | "execution_count": null, 320 | "metadata": { 321 | "collapsed": true 322 | }, 323 | "outputs": [], 324 | "source": [] 325 | } 326 | ], 327 | "metadata": { 328 | "kernelspec": { 329 | "display_name": "Python 3", 330 | "language": "python", 331 | "name": "python3" 332 | }, 333 | "language_info": { 334 | "codemirror_mode": { 335 | "name": "ipython", 336 | "version": 3 337 | }, 338 | "file_extension": ".py", 339 | "mimetype": "text/x-python", 340 | "name": "python", 341 | "nbconvert_exporter": "python", 342 | "pygments_lexer": "ipython3", 343 | "version": "3.4.3" 344 | } 345 | }, 346 | "nbformat": 4, 347 | "nbformat_minor": 0 348 | } 349 | -------------------------------------------------------------------------------- /oslash/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from .typing import Functor, Applicative, Monoid, Monad 3 | from .cont import Cont 4 | from .maybe import Maybe, Just, Nothing 5 | from .either import Either, Right, Left 6 | from .list import List 7 | from .ioaction import IO, Put, Get, Return, ReadFile, put_line, get_line, read_file 8 | from .writer import Writer, MonadWriter, StringWriter 9 | from .reader import Reader, MonadReader 10 | from .identity import Identity 11 | from .state import State 12 | from .do import do, let, guard 13 | 14 | from .monadic import * 15 | from .util import fn, Unit 16 | 17 | from ._version import get_versions 18 | __version__ = get_versions()['version'] 19 | del get_versions 20 | -------------------------------------------------------------------------------- /oslash/_version.py: -------------------------------------------------------------------------------- 1 | 2 | # This file helps to compute a version number in source trees obtained from 3 | # git-archive tarball (such as those provided by githubs download-from-tag 4 | # feature). Distribution tarballs (built by setup.py sdist) and build 5 | # directories (produced by setup.py build) will contain a much shorter file 6 | # that just contains the computed version number. 7 | 8 | # This file is released into the public domain. Generated by 9 | # versioneer-0.18 (https://github.com/warner/python-versioneer) 10 | 11 | """Git implementation of _version.py.""" 12 | 13 | import errno 14 | import os 15 | import re 16 | import subprocess 17 | import sys 18 | 19 | 20 | def get_keywords(): 21 | """Get the keywords needed to look up the version information.""" 22 | # these strings will be replaced by git during git-archive. 23 | # setup.py/versioneer.py will grep for the variable names, so they must 24 | # each be defined on a line of their own. _version.py will just call 25 | # get_keywords(). 26 | git_refnames = " (HEAD -> master)" 27 | git_full = "c271c7633daf9d72393b419cfc9229e427e6a42a" 28 | git_date = "2020-11-07 17:04:51 +0100" 29 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 30 | return keywords 31 | 32 | 33 | class VersioneerConfig: 34 | """Container for Versioneer configuration parameters.""" 35 | 36 | 37 | def get_config(): 38 | """Create, populate and return the VersioneerConfig() object.""" 39 | # these strings are filled in when 'setup.py versioneer' creates 40 | # _version.py 41 | cfg = VersioneerConfig() 42 | cfg.VCS = "git" 43 | cfg.style = "pep440" 44 | cfg.tag_prefix = "v" 45 | cfg.parentdir_prefix = "oslash" 46 | cfg.versionfile_source = "oslash/_version.py" 47 | cfg.verbose = False 48 | return cfg 49 | 50 | 51 | class NotThisMethod(Exception): 52 | """Exception raised if a method is not valid for the current scenario.""" 53 | 54 | 55 | LONG_VERSION_PY = {} 56 | HANDLERS = {} 57 | 58 | 59 | def register_vcs_handler(vcs, method): # decorator 60 | """Decorator to mark a method as the handler for a particular VCS.""" 61 | def decorate(f): 62 | """Store f in HANDLERS[vcs][method].""" 63 | if vcs not in HANDLERS: 64 | HANDLERS[vcs] = {} 65 | HANDLERS[vcs][method] = f 66 | return f 67 | return decorate 68 | 69 | 70 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 71 | env=None): 72 | """Call the given command(s).""" 73 | assert isinstance(commands, list) 74 | p = None 75 | for c in commands: 76 | try: 77 | dispcmd = str([c] + args) 78 | # remember shell=False, so use git.cmd on windows, not just git 79 | p = subprocess.Popen([c] + args, cwd=cwd, env=env, 80 | stdout=subprocess.PIPE, 81 | stderr=(subprocess.PIPE if hide_stderr 82 | else None)) 83 | break 84 | except EnvironmentError: 85 | e = sys.exc_info()[1] 86 | if e.errno == errno.ENOENT: 87 | continue 88 | if verbose: 89 | print("unable to run %s" % dispcmd) 90 | print(e) 91 | return None, None 92 | else: 93 | if verbose: 94 | print("unable to find command, tried %s" % (commands,)) 95 | return None, None 96 | stdout = p.communicate()[0].strip() 97 | if sys.version_info[0] >= 3: 98 | stdout = stdout.decode() 99 | if p.returncode != 0: 100 | if verbose: 101 | print("unable to run %s (error)" % dispcmd) 102 | print("stdout was %s" % stdout) 103 | return None, p.returncode 104 | return stdout, p.returncode 105 | 106 | 107 | def versions_from_parentdir(parentdir_prefix, root, verbose): 108 | """Try to determine the version from the parent directory name. 109 | 110 | Source tarballs conventionally unpack into a directory that includes both 111 | the project name and a version string. We will also support searching up 112 | two directory levels for an appropriately named parent directory 113 | """ 114 | rootdirs = [] 115 | 116 | for i in range(3): 117 | dirname = os.path.basename(root) 118 | if dirname.startswith(parentdir_prefix): 119 | return {"version": dirname[len(parentdir_prefix):], 120 | "full-revisionid": None, 121 | "dirty": False, "error": None, "date": None} 122 | else: 123 | rootdirs.append(root) 124 | root = os.path.dirname(root) # up a level 125 | 126 | if verbose: 127 | print("Tried directories %s but none started with prefix %s" % 128 | (str(rootdirs), parentdir_prefix)) 129 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 130 | 131 | 132 | @register_vcs_handler("git", "get_keywords") 133 | def git_get_keywords(versionfile_abs): 134 | """Extract version information from the given file.""" 135 | # the code embedded in _version.py can just fetch the value of these 136 | # keywords. When used from setup.py, we don't want to import _version.py, 137 | # so we do it with a regexp instead. This function is not used from 138 | # _version.py. 139 | keywords = {} 140 | try: 141 | f = open(versionfile_abs, "r") 142 | for line in f.readlines(): 143 | if line.strip().startswith("git_refnames ="): 144 | mo = re.search(r'=\s*"(.*)"', line) 145 | if mo: 146 | keywords["refnames"] = mo.group(1) 147 | if line.strip().startswith("git_full ="): 148 | mo = re.search(r'=\s*"(.*)"', line) 149 | if mo: 150 | keywords["full"] = mo.group(1) 151 | if line.strip().startswith("git_date ="): 152 | mo = re.search(r'=\s*"(.*)"', line) 153 | if mo: 154 | keywords["date"] = mo.group(1) 155 | f.close() 156 | except EnvironmentError: 157 | pass 158 | return keywords 159 | 160 | 161 | @register_vcs_handler("git", "keywords") 162 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 163 | """Get version information from git keywords.""" 164 | if not keywords: 165 | raise NotThisMethod("no keywords at all, weird") 166 | date = keywords.get("date") 167 | if date is not None: 168 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 169 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 170 | # -like" string, which we must then edit to make compliant), because 171 | # it's been around since git-1.5.3, and it's too difficult to 172 | # discover which version we're using, or to work around using an 173 | # older one. 174 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 175 | refnames = keywords["refnames"].strip() 176 | if refnames.startswith("$Format"): 177 | if verbose: 178 | print("keywords are unexpanded, not using") 179 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 180 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 181 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 182 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 183 | TAG = "tag: " 184 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 185 | if not tags: 186 | # Either we're using git < 1.8.3, or there really are no tags. We use 187 | # a heuristic: assume all version tags have a digit. The old git %d 188 | # expansion behaves like git log --decorate=short and strips out the 189 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 190 | # between branches and tags. By ignoring refnames without digits, we 191 | # filter out many common branch names like "release" and 192 | # "stabilization", as well as "HEAD" and "master". 193 | tags = set([r for r in refs if re.search(r'\d', r)]) 194 | if verbose: 195 | print("discarding '%s', no digits" % ",".join(refs - tags)) 196 | if verbose: 197 | print("likely tags: %s" % ",".join(sorted(tags))) 198 | for ref in sorted(tags): 199 | # sorting will prefer e.g. "2.0" over "2.0rc1" 200 | if ref.startswith(tag_prefix): 201 | r = ref[len(tag_prefix):] 202 | if verbose: 203 | print("picking %s" % r) 204 | return {"version": r, 205 | "full-revisionid": keywords["full"].strip(), 206 | "dirty": False, "error": None, 207 | "date": date} 208 | # no suitable tags, so version is "0+unknown", but full hex is still there 209 | if verbose: 210 | print("no suitable tags, using unknown + full revision id") 211 | return {"version": "0+unknown", 212 | "full-revisionid": keywords["full"].strip(), 213 | "dirty": False, "error": "no suitable tags", "date": None} 214 | 215 | 216 | @register_vcs_handler("git", "pieces_from_vcs") 217 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 218 | """Get version from 'git describe' in the root of the source tree. 219 | 220 | This only gets called if the git-archive 'subst' keywords were *not* 221 | expanded, and _version.py hasn't already been rewritten with a short 222 | version string, meaning we're inside a checked out source tree. 223 | """ 224 | GITS = ["git"] 225 | if sys.platform == "win32": 226 | GITS = ["git.cmd", "git.exe"] 227 | 228 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 229 | hide_stderr=True) 230 | if rc != 0: 231 | if verbose: 232 | print("Directory %s not under git control" % root) 233 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 234 | 235 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 236 | # if there isn't one, this yields HEX[-dirty] (no NUM) 237 | describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 238 | "--always", "--long", 239 | "--match", "%s*" % tag_prefix], 240 | cwd=root) 241 | # --long was added in git-1.5.5 242 | if describe_out is None: 243 | raise NotThisMethod("'git describe' failed") 244 | describe_out = describe_out.strip() 245 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 246 | if full_out is None: 247 | raise NotThisMethod("'git rev-parse' failed") 248 | full_out = full_out.strip() 249 | 250 | pieces = {} 251 | pieces["long"] = full_out 252 | pieces["short"] = full_out[:7] # maybe improved later 253 | pieces["error"] = None 254 | 255 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 256 | # TAG might have hyphens. 257 | git_describe = describe_out 258 | 259 | # look for -dirty suffix 260 | dirty = git_describe.endswith("-dirty") 261 | pieces["dirty"] = dirty 262 | if dirty: 263 | git_describe = git_describe[:git_describe.rindex("-dirty")] 264 | 265 | # now we have TAG-NUM-gHEX or HEX 266 | 267 | if "-" in git_describe: 268 | # TAG-NUM-gHEX 269 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 270 | if not mo: 271 | # unparseable. Maybe git-describe is misbehaving? 272 | pieces["error"] = ("unable to parse git-describe output: '%s'" 273 | % describe_out) 274 | return pieces 275 | 276 | # tag 277 | full_tag = mo.group(1) 278 | if not full_tag.startswith(tag_prefix): 279 | if verbose: 280 | fmt = "tag '%s' doesn't start with prefix '%s'" 281 | print(fmt % (full_tag, tag_prefix)) 282 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 283 | % (full_tag, tag_prefix)) 284 | return pieces 285 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 286 | 287 | # distance: number of commits since tag 288 | pieces["distance"] = int(mo.group(2)) 289 | 290 | # commit: short hex revision ID 291 | pieces["short"] = mo.group(3) 292 | 293 | else: 294 | # HEX: no tags 295 | pieces["closest-tag"] = None 296 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 297 | cwd=root) 298 | pieces["distance"] = int(count_out) # total number of commits 299 | 300 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 301 | date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], 302 | cwd=root)[0].strip() 303 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 304 | 305 | return pieces 306 | 307 | 308 | def plus_or_dot(pieces): 309 | """Return a + if we don't already have one, else return a .""" 310 | if "+" in pieces.get("closest-tag", ""): 311 | return "." 312 | return "+" 313 | 314 | 315 | def render_pep440(pieces): 316 | """Build up version string, with post-release "local version identifier". 317 | 318 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 319 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 320 | 321 | Exceptions: 322 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 323 | """ 324 | if pieces["closest-tag"]: 325 | rendered = pieces["closest-tag"] 326 | if pieces["distance"] or pieces["dirty"]: 327 | rendered += plus_or_dot(pieces) 328 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 329 | if pieces["dirty"]: 330 | rendered += ".dirty" 331 | else: 332 | # exception #1 333 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 334 | pieces["short"]) 335 | if pieces["dirty"]: 336 | rendered += ".dirty" 337 | return rendered 338 | 339 | 340 | def render_pep440_pre(pieces): 341 | """TAG[.post.devDISTANCE] -- No -dirty. 342 | 343 | Exceptions: 344 | 1: no tags. 0.post.devDISTANCE 345 | """ 346 | if pieces["closest-tag"]: 347 | rendered = pieces["closest-tag"] 348 | if pieces["distance"]: 349 | rendered += ".post.dev%d" % pieces["distance"] 350 | else: 351 | # exception #1 352 | rendered = "0.post.dev%d" % pieces["distance"] 353 | return rendered 354 | 355 | 356 | def render_pep440_post(pieces): 357 | """TAG[.postDISTANCE[.dev0]+gHEX] . 358 | 359 | The ".dev0" means dirty. Note that .dev0 sorts backwards 360 | (a dirty tree will appear "older" than the corresponding clean one), 361 | but you shouldn't be releasing software with -dirty anyways. 362 | 363 | Exceptions: 364 | 1: no tags. 0.postDISTANCE[.dev0] 365 | """ 366 | if pieces["closest-tag"]: 367 | rendered = pieces["closest-tag"] 368 | if pieces["distance"] or pieces["dirty"]: 369 | rendered += ".post%d" % pieces["distance"] 370 | if pieces["dirty"]: 371 | rendered += ".dev0" 372 | rendered += plus_or_dot(pieces) 373 | rendered += "g%s" % pieces["short"] 374 | else: 375 | # exception #1 376 | rendered = "0.post%d" % pieces["distance"] 377 | if pieces["dirty"]: 378 | rendered += ".dev0" 379 | rendered += "+g%s" % pieces["short"] 380 | return rendered 381 | 382 | 383 | def render_pep440_old(pieces): 384 | """TAG[.postDISTANCE[.dev0]] . 385 | 386 | The ".dev0" means dirty. 387 | 388 | Eexceptions: 389 | 1: no tags. 0.postDISTANCE[.dev0] 390 | """ 391 | if pieces["closest-tag"]: 392 | rendered = pieces["closest-tag"] 393 | if pieces["distance"] or pieces["dirty"]: 394 | rendered += ".post%d" % pieces["distance"] 395 | if pieces["dirty"]: 396 | rendered += ".dev0" 397 | else: 398 | # exception #1 399 | rendered = "0.post%d" % pieces["distance"] 400 | if pieces["dirty"]: 401 | rendered += ".dev0" 402 | return rendered 403 | 404 | 405 | def render_git_describe(pieces): 406 | """TAG[-DISTANCE-gHEX][-dirty]. 407 | 408 | Like 'git describe --tags --dirty --always'. 409 | 410 | Exceptions: 411 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 412 | """ 413 | if pieces["closest-tag"]: 414 | rendered = pieces["closest-tag"] 415 | if pieces["distance"]: 416 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 417 | else: 418 | # exception #1 419 | rendered = pieces["short"] 420 | if pieces["dirty"]: 421 | rendered += "-dirty" 422 | return rendered 423 | 424 | 425 | def render_git_describe_long(pieces): 426 | """TAG-DISTANCE-gHEX[-dirty]. 427 | 428 | Like 'git describe --tags --dirty --always -long'. 429 | The distance/hash is unconditional. 430 | 431 | Exceptions: 432 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 433 | """ 434 | if pieces["closest-tag"]: 435 | rendered = pieces["closest-tag"] 436 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 437 | else: 438 | # exception #1 439 | rendered = pieces["short"] 440 | if pieces["dirty"]: 441 | rendered += "-dirty" 442 | return rendered 443 | 444 | 445 | def render(pieces, style): 446 | """Render the given version pieces into the requested style.""" 447 | if pieces["error"]: 448 | return {"version": "unknown", 449 | "full-revisionid": pieces.get("long"), 450 | "dirty": None, 451 | "error": pieces["error"], 452 | "date": None} 453 | 454 | if not style or style == "default": 455 | style = "pep440" # the default 456 | 457 | if style == "pep440": 458 | rendered = render_pep440(pieces) 459 | elif style == "pep440-pre": 460 | rendered = render_pep440_pre(pieces) 461 | elif style == "pep440-post": 462 | rendered = render_pep440_post(pieces) 463 | elif style == "pep440-old": 464 | rendered = render_pep440_old(pieces) 465 | elif style == "git-describe": 466 | rendered = render_git_describe(pieces) 467 | elif style == "git-describe-long": 468 | rendered = render_git_describe_long(pieces) 469 | else: 470 | raise ValueError("unknown style '%s'" % style) 471 | 472 | return {"version": rendered, "full-revisionid": pieces["long"], 473 | "dirty": pieces["dirty"], "error": None, 474 | "date": pieces.get("date")} 475 | 476 | 477 | def get_versions(): 478 | """Get version information or return default if unable to do so.""" 479 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 480 | # __file__, we can work backwards from there to the root. Some 481 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 482 | # case we can only use expanded keywords. 483 | 484 | cfg = get_config() 485 | verbose = cfg.verbose 486 | 487 | try: 488 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 489 | verbose) 490 | except NotThisMethod: 491 | pass 492 | 493 | try: 494 | root = os.path.realpath(__file__) 495 | # versionfile_source is the relative path from the top of the source 496 | # tree (where the .git directory might live) to this file. Invert 497 | # this to find the root from __file__. 498 | for i in cfg.versionfile_source.split('/'): 499 | root = os.path.dirname(root) 500 | except NameError: 501 | return {"version": "0+unknown", "full-revisionid": None, 502 | "dirty": None, 503 | "error": "unable to find root of source tree", 504 | "date": None} 505 | 506 | try: 507 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 508 | return render(pieces, cfg.style) 509 | except NotThisMethod: 510 | pass 511 | 512 | try: 513 | if cfg.parentdir_prefix: 514 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 515 | except NotThisMethod: 516 | pass 517 | 518 | return {"version": "0+unknown", "full-revisionid": None, 519 | "dirty": None, 520 | "error": "unable to compute version", "date": None} 521 | -------------------------------------------------------------------------------- /oslash/cont.py: -------------------------------------------------------------------------------- 1 | """ The Continuation Monad 2 | 3 | * https://wiki.haskell.org/MonadCont_under_the_hood 4 | * http://blog.sigfpe.com/2008/12/mother-of-all-monads.html 5 | * http://www.haskellforall.com/2012/12/the-continuation-monad.html 6 | 7 | """ 8 | 9 | from typing import Callable, Generic, TypeVar 10 | 11 | 12 | from .util import identity, compose 13 | from .typing import Monad, Functor 14 | 15 | T = TypeVar('T') 16 | T2 = TypeVar('T2') 17 | TResult = TypeVar('TResult') 18 | 19 | TCont = Callable[[T], TResult] 20 | 21 | 22 | class Cont(Generic[T, TResult]): 23 | """The Continuation Monad. 24 | 25 | The Continuation monad represents suspended computations in continuation- 26 | passing style (CPS). 27 | """ 28 | 29 | def __init__(self, comp: Callable[[TCont], TResult]) -> None: 30 | """Cont constructor. 31 | 32 | Keyword arguments: 33 | cont -- A callable 34 | """ 35 | self._comp = comp 36 | 37 | @classmethod 38 | def unit(cls, value: T) -> 'Cont[T, TResult]': 39 | """Create new continuation. 40 | 41 | Haskell: a -> Cont a 42 | """ 43 | fn: Callable[[TCont], TResult] = lambda cont: cont(value) 44 | return Cont(fn) 45 | 46 | def map(self, fn: Callable[[T], T2]) -> 'Cont[T2, TResult]': 47 | r"""Map a function over a continuation. 48 | 49 | Haskell: fmap f m = Cont $ \c -> runCont m (c . f) 50 | """ 51 | def comp(cont: Callable[[T2], TResult]) -> TResult: 52 | return self.run(compose(cont, fn)) 53 | return Cont(comp) 54 | 55 | def bind(self, fn: Callable[[T], 'Cont[T2, TResult]']) -> 'Cont[T2, TResult]': 56 | r"""Chain continuation passing functions. 57 | 58 | Haskell: m >>= k = Cont $ \c -> runCont m $ \a -> runCont (k a) c 59 | """ 60 | return Cont(lambda cont: self.run(lambda a: fn(a).run(cont))) 61 | 62 | @staticmethod 63 | def call_cc(fn: Callable) -> 'Cont': 64 | r"""call-with-current-continuation. 65 | 66 | Haskell: callCC f = Cont $ \c -> runCont (f (\a -> Cont $ \_ -> c a )) c 67 | """ 68 | return Cont(lambda c: fn(lambda a: Cont(lambda _: c(a))).run(c)) 69 | 70 | def run(self, cont: Callable[[T], TResult]) -> TResult: 71 | return self._comp(cont) 72 | 73 | def __or__(self, func): 74 | """Use | as operator for bind. 75 | 76 | Provide the | operator instead of the Haskell >>= operator 77 | """ 78 | return self.bind(func) 79 | 80 | def __call__(self, comp: Callable[[T], TResult]) -> TResult: 81 | return self.run(comp) 82 | 83 | def __eq__(self, other) -> bool: 84 | return self(identity) == other(identity) 85 | 86 | 87 | assert isinstance(Cont, Functor) 88 | assert isinstance(Cont, Monad) 89 | -------------------------------------------------------------------------------- /oslash/do.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Monadic do notation for Python.""" 4 | 5 | from collections import namedtuple 6 | 7 | from .typing import Monad 8 | from .util import Unit 9 | 10 | # This would be most natural to implement as a syntactic macro. 11 | # But to stay within Python's builtin capabilities, this is a code generator. 12 | # 13 | # The price is the sprinkling of "lambda e: ..."s to feed in the environment, 14 | # and manually simulated lexical scoping for env attrs instead of just 15 | # borrowing Python's for run-of-the-mill names. 16 | 17 | __all__ = ["do", "let", "guard"] 18 | 19 | 20 | # TODO: guard belongs where in OSlash? 21 | def guard(M, test): 22 | """Monadic guard. 23 | 24 | What it does:: 25 | 26 | return M.pure(Unit) if test else M.empty() 27 | 28 | https://en.wikibooks.org/wiki/Haskell/Alternative_and_MonadPlus#guard 29 | """ 30 | return M.pure(Unit) if test else M.empty() 31 | 32 | 33 | # The kwargs syntax forces name to be a valid Python identifier. 34 | MonadicLet = namedtuple("MonadicLet", "name value") 35 | 36 | 37 | def let(**binding): 38 | """``<-`` for Python. 39 | 40 | Haskell:: 41 | 42 | a <- [1, 2, 3] 43 | 44 | Python:: 45 | 46 | let(a=List.from_iterable((1, 2, 3))) 47 | """ 48 | if len(binding) != 1: 49 | raise ValueError("Expected exactly one binding, got {:d} with values {}".format(len(binding), binding)) 50 | for k, v in binding.items(): 51 | return MonadicLet(k, v) 52 | 53 | 54 | def do(*lines): 55 | """Do-notation. 56 | 57 | Syntax:: 58 | 59 | do(line, 60 | ...) 61 | 62 | where each ``line`` is one of:: 63 | 64 | let(name=body) # Haskell: name <- expr (see below on expr) 65 | body # Haskell: expr 66 | 67 | where ``name`` is a Python identifier. 68 | 69 | - Use ``let(name=body)`` when you want to bind a name to the extracted value, 70 | for use on any following lines. 71 | 72 | - Use only ``body`` when you just want to sequence operations, 73 | and on the last line. 74 | 75 | Here ``body`` is one of: 76 | 77 | - An expression ``expr`` that evaluates to a Monad instance. 78 | 79 | - A one-argument function which takes in the environment, such as 80 | ``lambda e: expr``, and when called (with the environment as its 81 | only argument), returns a Monad instance. This allows accessing 82 | the ``let`` bindings in the environment. 83 | 84 | **Examples**. This Haskell:: 85 | 86 | do 87 | a <- [3, 10, 6] 88 | b <- [100, 200] 89 | return a + b 90 | 91 | pythonifies as:: 92 | 93 | l = lambda *items: List.from_iterable(items) 94 | do(let(a=l(3, 10, 6)), # e.a <- ... 95 | let(b=l(100, 200)), # e.b <- ... 96 | lambda e: List.unit(e.a + e.b)) # access the env via lambda e: ... 97 | 98 | which has the same effect as:: 99 | 100 | l(3, 10, 6) | (lambda a: 101 | l(100, 200) | (lambda b: 102 | List.unit(a + b))) 103 | 104 | *Pythagorean triples*. (A classic test case for McCarthy's *amb* operator.) 105 | 106 | Denote ``z`` = hypotenuse, ``x`` = shorter leg, ``y`` = longer leg, 107 | so their lengths ``z >= y >= x``. Define:: 108 | 109 | def r(low, high): 110 | return List.from_iterable(range(low, high)) 111 | 112 | Now:: 113 | 114 | pt = do(let(z=r(1, 21)), 115 | let(x=lambda e: r(1, e.z+1)), # needs the env to access "z" 116 | let(y=lambda e: r(e.x, e.z+1)), 117 | lambda e: guard(List, e.x*e.x + e.y*e.y == e.z*e.z), 118 | lambda e: List.unit((e.x, e.y, e.z))) 119 | 120 | which has the same effect as:: 121 | 122 | pt = r(1, 21) | (lambda z: 123 | r(1, z+1) | (lambda x: 124 | r(x, z+1) | (lambda y: 125 | guard(List, x*x + y*y == z*z) >> 126 | List.unit((x,y,z))))) 127 | """ 128 | # The monadic bind and sequence operators, with any relevant whitespace. 129 | bind = " | " 130 | seq = " >> " 131 | 132 | class env: 133 | def __init__(self): 134 | self.names = set() 135 | 136 | def assign(self, k, v): 137 | self.names.add(k) 138 | setattr(self, k, v) 139 | 140 | # simulate lexical closure property for env attrs 141 | # - freevars: set of names that "fall in" from a surrounding lexical scope 142 | def close_over(self, freevars): 143 | names_to_clear = {k for k in self.names if k not in freevars} 144 | for k in names_to_clear: 145 | delattr(self, k) 146 | self.names = freevars.copy() 147 | 148 | # stuff used inside the eval 149 | e = env() 150 | 151 | def begin(*exprs): # args eagerly evaluated by Python 152 | # begin(e1, e2, ..., en): 153 | # perform side effects e1, e2, ..., e[n-1], return the value of en. 154 | return exprs[-1] 155 | 156 | allcode = "" 157 | names = set() # names seen so far (working line by line, so textually!) 158 | bodys = [] 159 | begin_is_open = False 160 | for j, item in enumerate(lines): 161 | is_first = j == 0 162 | is_last = j == len(lines) - 1 163 | 164 | if isinstance(item, MonadicLet): 165 | name, body = item 166 | else: 167 | name, body = None, item 168 | bodys.append(body) 169 | 170 | freevars = names.copy() # names from the surrounding scopes 171 | if name: 172 | names.add(name) 173 | 174 | if isinstance(body, Monad): # doesn't need the environment 175 | code = "bodys[{j:d}]".format(j=j) 176 | elif callable(body): # lambda e: ... 177 | # TODO: check arity (see unpythonic.arity.arity_includes) 178 | code = "bodys[{j:d}](e)".format(j=j) 179 | else: 180 | raise TypeError("Unexpected body type '{}' with value '{}'".format(type(body), body)) 181 | 182 | if begin_is_open: 183 | code += ")" 184 | begin_is_open = False 185 | 186 | # monadic-bind or sequence to the next item, leaving only the appropriate 187 | # names defined in the env (so that we get proper lexical scoping 188 | # even though we use an imperative stateful object to implement it) 189 | if not is_last: 190 | if name: 191 | code += "{bind:s}(lambda {n:s}:\nbegin(e.close_over({fvs}), e.assign('{n:s}', {n:s}), ".format( 192 | bind=bind, n=name, fvs=freevars 193 | ) 194 | begin_is_open = True 195 | else: 196 | if is_first: 197 | code += "{bind:s}(lambda _:\nbegin(e.close_over(set()), ".format(bind=bind) 198 | begin_is_open = True 199 | else: 200 | code += "{seq:s}(\n".format(seq=seq) 201 | 202 | allcode += code 203 | allcode += ")" * (len(lines) - 1) 204 | 205 | # The eval'd code doesn't close over the current lexical scope, 206 | # so provide the necessary names as its globals. 207 | return eval(allcode, {"e": e, "bodys": bodys, "begin": begin}) 208 | -------------------------------------------------------------------------------- /oslash/either.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from functools import partial 3 | 4 | from typing import Callable, TypeVar, Generic 5 | 6 | from oslash.typing import Applicative 7 | from oslash.typing import Functor 8 | from oslash.typing import Monad 9 | 10 | TSource = TypeVar("TSource") 11 | TResult = TypeVar("TResult") 12 | TError = TypeVar("TError") 13 | 14 | 15 | class Either(Generic[TSource, TError]): 16 | 17 | """The Either Monad. 18 | 19 | Represents either a successful computation, or a computation that 20 | has failed. 21 | """ 22 | 23 | @abstractmethod 24 | def map(self, _: Callable[[TSource], TResult]) -> "Either[TResult, TError]": 25 | raise NotImplementedError 26 | 27 | @classmethod 28 | @abstractmethod 29 | def pure(cls, value: Callable[[TSource], TResult]) -> "Either[Callable[[TSource], TResult], TError]": 30 | raise NotImplementedError 31 | 32 | @abstractmethod 33 | def apply( 34 | self: "Either[Callable[[TSource], TResult], TError]", something: "Either[TSource, TError]" 35 | ) -> "Either[TResult, TError]": 36 | raise NotImplementedError 37 | 38 | @classmethod 39 | @abstractmethod 40 | def unit(cls, value: TSource) -> "Right[TSource, TError]": 41 | raise NotImplementedError 42 | 43 | @abstractmethod 44 | def bind(self, func: Callable[[TSource], "Either[TResult, TError]"]) -> "Either[TResult, TError]": 45 | raise NotImplementedError 46 | 47 | @abstractmethod 48 | def __eq__(self, other) -> bool: 49 | raise NotImplementedError 50 | 51 | 52 | class Right(Either[TSource, TError]): 53 | 54 | """Represents a successful computation.""" 55 | 56 | def __init__(self, value: TSource) -> None: 57 | self._value = value 58 | 59 | # Functor Section 60 | # =============== 61 | 62 | def map(self, mapper: Callable[[TSource], TResult]) -> Either[TResult, TError]: 63 | result = mapper(self._value) 64 | return Right(result) 65 | 66 | # Applicative Section 67 | # =================== 68 | 69 | @classmethod 70 | def pure(cls, value: Callable[[TSource], TResult]) -> "Right[Callable[[TSource], TResult], TError]": 71 | return Right(value) 72 | 73 | def apply( 74 | self: "Right[Callable[[TSource], TResult], TError]", something: "Either[TSource, TError]" 75 | ) -> "Either[TResult, TError]": 76 | def mapper(other_value): 77 | try: 78 | return self._value(other_value) 79 | except TypeError: 80 | return partial(self._value, other_value) 81 | 82 | return something.map(mapper) 83 | 84 | # Monad Section 85 | # ============= 86 | 87 | @classmethod 88 | def unit(cls, value: TSource) -> "Right[TSource, TError]": 89 | return Right(value) 90 | 91 | def bind(self, func: Callable[[TSource], Either[TResult, TError]]) -> Either[TResult, TError]: 92 | return func(self._value) 93 | 94 | # Operator Overloads 95 | # ================== 96 | 97 | def __eq__(self, other) -> bool: 98 | return isinstance(other, Right) and self._value == other._value 99 | 100 | def __str__(self) -> str: 101 | return "Right %s" % self._value 102 | 103 | 104 | class Left(Either[TSource, TError]): 105 | 106 | """Represents a computation that has failed.""" 107 | 108 | def __init__(self, error: TError) -> None: 109 | self._error = error 110 | 111 | @classmethod 112 | def pure(cls, value: Callable[[TSource], TResult]) -> Either[Callable[[TSource], TResult], TError]: 113 | return Right(value) 114 | 115 | def apply(self, something: Either) -> Either: 116 | return Left(self._error) 117 | 118 | def map(self, mapper: Callable[[TSource], TResult]) -> Either[TResult, TError]: 119 | return Left(self._error) 120 | 121 | @classmethod 122 | def unit(cls, value: TSource): 123 | return Right(value) 124 | 125 | def bind(self, func: Callable[[TSource], Either[TResult, TError]]) -> Either[TResult, TError]: 126 | return Left(self._error) 127 | 128 | def __eq__(self, other) -> bool: 129 | return isinstance(other, Left) and self._error == other._error 130 | 131 | def __str__(self) -> str: 132 | return "Left: %s" % self._error 133 | 134 | 135 | assert(isinstance(Either, Functor)) 136 | assert(isinstance(Either, Applicative)) 137 | assert(isinstance(Either, Monad)) 138 | 139 | assert(isinstance(Right, Functor)) 140 | assert(isinstance(Right, Applicative)) 141 | assert(isinstance(Right, Monad)) 142 | 143 | assert(isinstance(Left, Functor)) 144 | assert(isinstance(Left, Applicative)) 145 | assert(isinstance(Left, Monad)) 146 | -------------------------------------------------------------------------------- /oslash/identity.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from typing import TypeVar, Generic, Callable 3 | 4 | from .typing import Functor, Monad, Applicative 5 | 6 | 7 | TSource = TypeVar('TSource') 8 | TResult = TypeVar('TResult') 9 | 10 | 11 | class Identity(Generic[TSource]): 12 | """Identity monad. 13 | 14 | The Identity monad is the simplest monad, which attaches no 15 | information to values. 16 | """ 17 | def __init__(self, value: TSource) -> None: 18 | self._value = value 19 | 20 | @classmethod 21 | def unit(cls, value: TSource) -> 'Identity[TSource]': 22 | """Initialize a new identity.""" 23 | return Identity(value) 24 | 25 | def map(self, mapper: Callable[[TSource], TResult]) -> 'Identity[TResult]': 26 | """Map a function over wrapped values.""" 27 | result = mapper(self._value) 28 | return Identity(result) 29 | 30 | def bind(self, func: Callable[[TSource], 'Identity[TResult]']) -> 'Identity[TResult]': 31 | return func(self._value) 32 | 33 | @classmethod 34 | def pure(cls, value: TSource): 35 | return Identity(value) 36 | 37 | def apply(self: 'Identity[Callable[[TSource], TResult]]', something: 'Identity[TSource]') -> 'Identity[TResult]': 38 | def mapper(other_value): 39 | try: 40 | return self._value(other_value) 41 | except TypeError: 42 | return partial(self._value, other_value) 43 | return something.map(mapper) 44 | 45 | def run(self) -> TSource: 46 | return self._value 47 | 48 | def __call__(self) -> TSource: 49 | return self.run() 50 | 51 | def __eq__(self, other) -> bool: 52 | return self._value == other() 53 | 54 | def __str__(self) -> str: 55 | return "Identity(%s)" % self._value 56 | 57 | def __repr__(self) -> str: 58 | return str(self) 59 | 60 | 61 | assert isinstance(Identity, Functor) 62 | assert isinstance(Identity, Applicative) 63 | assert isinstance(Identity, Monad) 64 | -------------------------------------------------------------------------------- /oslash/ioaction.py: -------------------------------------------------------------------------------- 1 | """Implementation of IO Actions.""" 2 | 3 | from abc import abstractmethod 4 | from typing import Any, Callable, Generic, TypeVar, Tuple 5 | 6 | from .typing import Functor 7 | from .typing import Monad 8 | from .util import indent as ind, Unit 9 | 10 | TSource = TypeVar("TSource") 11 | TResult = TypeVar("TResult") 12 | 13 | 14 | class IO(Generic[TSource]): 15 | """A container for a world remaking function. 16 | 17 | IO Actions specify something that can be done. They are not active 18 | in and of themselves. They need to be "run" to make something 19 | happen. Simply having an action lying around doesn't make anything 20 | happen. 21 | """ 22 | 23 | @classmethod 24 | def unit(cls, value: TSource): 25 | return Return(value) 26 | 27 | @abstractmethod 28 | def bind(self, func: Callable[[TSource], "IO[TResult]"]) -> "IO[TResult]": 29 | """IO a -> (a -> IO b) -> IO b.""" 30 | 31 | raise NotImplementedError 32 | 33 | @abstractmethod 34 | def map(self, func: Callable[[TSource], TResult]) -> "IO[TResult]": 35 | raise NotImplementedError 36 | 37 | @abstractmethod 38 | def run(self, world: int) -> TSource: 39 | """Run IO action.""" 40 | raise NotImplementedError 41 | 42 | def __or__(self, func): 43 | """Use | as operator for bind. 44 | 45 | Provide the | operator instead of the Haskell >>= operator 46 | """ 47 | return self.bind(func) 48 | 49 | def __rshift__(self, next: 'IO[TResult]') -> 'IO[TResult]': 50 | """The "Then" operator. 51 | Sequentially compose two monadic actions, discarding any value 52 | produced by the first, like sequencing operators (such as the 53 | semicolon) in imperative languages. 54 | Haskell: (>>) :: m a -> m b -> m b 55 | """ 56 | return self.bind(lambda _: next) 57 | 58 | def __call__(self, world: int = 0) -> Any: 59 | """Run io action.""" 60 | return self.run(world) 61 | 62 | @abstractmethod 63 | def __str__(self, m: int = 0, n: int = 0) -> str: 64 | raise NotImplementedError 65 | 66 | def __repr__(self) -> str: 67 | return self.__str__() 68 | 69 | 70 | class Return(IO[TSource]): 71 | def __init__(self, value: TSource) -> None: 72 | """Create IO Action.""" 73 | 74 | self._value = value 75 | 76 | def map(self, func: Callable[[TSource], TResult]) -> "IO[TResult]": 77 | return Return(func(self._value)) 78 | 79 | def bind(self, func: Callable[[TSource], "IO[TResult]"]) -> "IO[TResult]": 80 | """IO a -> (a -> IO b) -> IO b.""" 81 | 82 | return func(self._value) 83 | 84 | def run(self, world: int) -> TSource: 85 | """Run IO action.""" 86 | return self._value 87 | 88 | def __str__(self, m: int = 0, n: int = 0) -> str: 89 | a = self._value 90 | return f"{ind(m)}Return {a}" 91 | 92 | 93 | class Put(IO[TSource]): 94 | """The Put action. 95 | 96 | A container holding a string to be printed to stdout, followed by 97 | another IO Action. 98 | """ 99 | 100 | def __init__(self, text: str, io: IO) -> None: 101 | self._value = text, io 102 | 103 | def bind(self, func: Callable[[TSource], IO[TResult]]) -> 'IO[TResult]': 104 | """IO a -> (a -> IO b) -> IO b""" 105 | 106 | text, io = self._value 107 | return Put(text, io.bind(func)) 108 | 109 | def map(self, func: Callable[[TSource], TResult]) -> "IO[TResult]": 110 | # Put s (fmap f io) 111 | assert self._value is not None 112 | text, action = self._value 113 | return Put(text, action.map(func)) 114 | 115 | def run(self, world: int) -> TSource: 116 | """Run IO action""" 117 | 118 | assert self._value is not None 119 | text, action = self._value 120 | new_world = pure_print(world, text) 121 | return action(world=new_world) 122 | 123 | def __call__(self, world: int = 0) -> TSource: 124 | return self.run(world) 125 | 126 | def __str__(self, m: int = 0, n: int = 0) -> str: 127 | s, io = self._value 128 | a = io.__str__(m + 1, n) 129 | return '%sPut ("%s",\n%s\n%s)' % (ind(m), s, a, ind(m)) 130 | 131 | 132 | class Get(IO[TSource]): 133 | """A container holding a function from string -> IO[TSource], which can 134 | be applied to whatever string is read from stdin. 135 | """ 136 | 137 | def __init__(self, fn: Callable[[str], IO[TSource]]) -> None: 138 | self._fn = fn 139 | 140 | def bind(self, func: Callable[[TSource], IO[TResult]]) -> IO[TResult]: 141 | """IO a -> (a -> IO b) -> IO b""" 142 | 143 | g = self._fn 144 | return Get(lambda text: g(text).bind(func)) 145 | 146 | def map(self, func: Callable[[TSource], TResult]) -> IO[TResult]: 147 | # Get (\s -> fmap f (g s)) 148 | g = self._fn 149 | return Get(lambda s: g(s).map(func)) 150 | 151 | def run(self, world: int) -> TSource: 152 | """Run IO Action""" 153 | 154 | func = self._fn 155 | new_world, text = pure_input(world) 156 | action = func(text) 157 | return action(world=new_world) 158 | 159 | def __call__(self, world: int = 0) -> TSource: 160 | return self.run(world) 161 | 162 | def __str__(self, m: int = 0, n: int = 0) -> str: 163 | g = self._fn 164 | i = "x%s" % n 165 | a = g(i).__str__(m + 1, n + 1) 166 | return "%sGet (%s => \n%s\n%s)" % (ind(m), i, a, ind(m)) 167 | 168 | 169 | class ReadFile(IO[str]): 170 | """A container holding a filename and a function from string -> IO[str], 171 | which can be applied to whatever string is read from the file. 172 | """ 173 | 174 | def __init__(self, filename: str, func: Callable[[str], IO]) -> None: 175 | self.open_func = open 176 | self._value = filename, func 177 | 178 | def bind(self, func: Callable[[Any], IO]) -> IO: 179 | """IO a -> (a -> IO b) -> IO b""" 180 | 181 | filename, g = self._value 182 | return ReadFile(filename, lambda s: g(s).bind(func)) 183 | 184 | def map(self, func: Callable[[Any], Any]) -> IO: 185 | # Get (\s -> fmap f (g s)) 186 | filename, g = self._value 187 | return Get(lambda s: g(s).map(func)) 188 | 189 | def run(self, world: int) -> str: 190 | """Run IO Action""" 191 | 192 | filename, func = self._value 193 | f = self.open_func(filename) 194 | action = func(f.read()) 195 | return action(world=world + 1) 196 | 197 | def __call__(self, world: int = 0) -> str: 198 | return self.run(world) 199 | 200 | def __str__(self, m: int = 0, n: int = 0) -> str: 201 | filename, g = self._value 202 | i = "x%s" % n 203 | a = g(i).__str__(m + 2, n + 1) 204 | return '%sReadFile ("%s",%s => \n%s\n%s)' % (ind(m), filename, i, a, ind(m)) 205 | 206 | 207 | def get_line() -> IO[str]: 208 | return Get(Return) 209 | 210 | 211 | def put_line(text: str) -> IO: 212 | return Put(text, Return(Unit)) 213 | 214 | 215 | def read_file(filename: str) -> IO: 216 | return ReadFile(filename, Return) 217 | 218 | 219 | def pure_print(world: int, text: str) -> int: 220 | print(text) # Impure. NOTE: If you see this line you need to wash your hands 221 | return world + 1 222 | 223 | 224 | def pure_input(world: int) -> Tuple[int, str]: 225 | text = input() # Impure. NOTE: If you see this line you need to wash your hands 226 | return (world + 1, text) 227 | 228 | 229 | assert isinstance(IO, Functor) 230 | assert isinstance(IO, Monad) 231 | 232 | assert isinstance(Put, Functor) 233 | assert isinstance(Put, Monad) 234 | 235 | assert isinstance(Get, Functor) 236 | assert isinstance(Get, Monad) 237 | 238 | assert isinstance(ReadFile, Functor) 239 | assert isinstance(ReadFile, Monad) 240 | -------------------------------------------------------------------------------- /oslash/list.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from functools import partial, reduce 3 | 4 | from typing import Callable, Iterator, TypeVar, Iterable, Sized, Any, cast, Union 5 | 6 | from .typing import Applicative 7 | from .typing import Functor 8 | from .typing import Monoid 9 | from .typing import Monad 10 | 11 | TSource = TypeVar("TSource") 12 | TResult = TypeVar("TResult") 13 | TSelector = Callable[[TSource, 'List[TSource]'], Union[TSource, 'List[TSource]']] 14 | 15 | 16 | class List(Iterable[TSource], Sized): 17 | """The list monad. 18 | 19 | Wraps an immutable list built from lambda expressions. 20 | """ 21 | 22 | @classmethod 23 | def unit(cls, value: Any) -> 'List': 24 | """Wrap a value within the singleton list.""" 25 | return List.empty().cons(value) 26 | 27 | pure = unit 28 | 29 | @classmethod 30 | def empty(cls) -> 'List[TSource]': 31 | """Create an empty list.""" 32 | return Nil() 33 | 34 | @classmethod 35 | def from_iterable(cls, iterable: Iterable) -> 'List': 36 | """Create list from iterable.""" 37 | 38 | iterator = iter(iterable) 39 | 40 | def recurse() -> List: 41 | try: 42 | value = next(iterator) 43 | except StopIteration: 44 | 45 | return List.empty() 46 | return List.unit(value).append(recurse()) 47 | return List.empty().append(recurse()) 48 | 49 | @classmethod 50 | def concat(cls, xs): 51 | """mconcat :: [m] -> m 52 | 53 | Fold a list using the monoid. For most types, the default 54 | definition for mconcat will be used, but the function is 55 | included in the class definition so that an optimized version 56 | can be provided for specific types. 57 | """ 58 | 59 | def reducer(a, b): 60 | return a + b 61 | 62 | return reduce(reducer, xs, Nil()) 63 | 64 | @abstractmethod 65 | def head(self) -> TSource: 66 | raise NotImplementedError 67 | 68 | @abstractmethod 69 | def tail(self) -> 'List[TSource]': 70 | """Return tail of List.""" 71 | raise NotImplementedError 72 | 73 | @abstractmethod 74 | def apply(self, something: 'List') -> 'List': 75 | raise NotImplementedError 76 | 77 | @abstractmethod 78 | def map(self, mapper: Callable[[TSource], TResult]) -> 'List[TResult]': 79 | raise NotImplementedError 80 | 81 | @abstractmethod 82 | def bind(self, fn: Callable[[TSource], 'List[TResult]']) -> 'List[TResult]': 83 | raise NotImplementedError 84 | 85 | @abstractmethod 86 | def cons(self, element: TSource) -> 'List[TSource]': 87 | raise NotImplementedError 88 | 89 | @abstractmethod 90 | def append(self, other: 'List[TSource]') -> 'List[TSource]': 91 | raise NotImplementedError 92 | 93 | @abstractmethod 94 | def null(self) -> bool: 95 | """Return True if List is empty.""" 96 | raise NotImplementedError 97 | 98 | @abstractmethod 99 | def __add__(self, other): 100 | raise NotImplementedError 101 | 102 | 103 | class Cons(List[TSource]): 104 | """The list cons case monad.""" 105 | 106 | def __init__(self, run: Callable[[TSelector], Union[TSource, List[TSource]]]) -> None: 107 | """Initialize List.""" 108 | 109 | self._list = run 110 | 111 | def cons(self, element: TSource) -> List[TSource]: 112 | """Add element to front of List.""" 113 | 114 | return Cons(lambda sel: sel(element, self)) 115 | 116 | def head(self) -> TSource: 117 | """Retrive first element in List.""" 118 | 119 | run = self._list 120 | return cast(TSource, run(lambda head, _: head)) 121 | 122 | def tail(self) -> 'List[TSource]': 123 | """Return tail of List.""" 124 | 125 | run = self._list 126 | return cast(List[TSource], run(lambda _, tail: tail)) 127 | 128 | def null(self) -> bool: 129 | """Return True if List is empty.""" 130 | return False 131 | 132 | def map(self, mapper: Callable[[TSource], TResult]) -> List[TResult]: 133 | """Map a function over a List.""" 134 | return (self.tail().map(mapper)).cons(mapper(self.head())) 135 | 136 | def apply(self, something: 'List') -> 'List': 137 | # fs <*> xs = [f x | f <- fs, x <- xs] 138 | try: 139 | xs = [f(x) for f in self for x in something] 140 | except TypeError: 141 | xs = [partial(f, x) for f in self for x in something] 142 | 143 | return List.from_iterable(xs) 144 | 145 | def append(self, other: List[TSource]) -> List[TSource]: 146 | """Append other list to this list.""" 147 | 148 | return (self.tail().append(other)).cons(self.head()) 149 | 150 | def bind(self, fn: Callable[[TSource], List[TResult]]) -> List[TResult]: 151 | """Flatten and map the List. 152 | 153 | Haskell: xs >>= f = concat (map f xs) 154 | """ 155 | return List.concat(self.map(fn)) 156 | 157 | def __iter__(self) -> Iterator: 158 | """Return iterator for List.""" 159 | 160 | yield self.head() 161 | yield from self.tail() 162 | 163 | def __or__(self, func): 164 | """Use | as operator for bind. 165 | 166 | Provide the | operator instead of the Haskell >>= operator 167 | """ 168 | return self.bind(func) 169 | 170 | def __rshift__(self, next): 171 | """The "Then" operator. 172 | 173 | Sequentially compose two monadic actions, discarding any value 174 | produced by the first, like sequencing operators (such as the 175 | semicolon) in imperative languages. 176 | 177 | Haskell: (>>) :: m a -> m b -> m b 178 | """ 179 | return self.bind(lambda _: next) 180 | 181 | def __add__(self, other): 182 | return self.append(other) 183 | 184 | def __len__(self) -> int: 185 | """Return length of List.""" 186 | 187 | return 1 + len(self.tail()) 188 | 189 | def __str__(self) -> str: 190 | """Return string representation of List.""" 191 | 192 | return "[%s]" % ", ".join([str(x) for x in self]) 193 | 194 | def __repr__(self) -> str: 195 | """Return string representation of List.""" 196 | 197 | return str(self) 198 | 199 | def __eq__(self, other) -> bool: 200 | """Compare if List is equal to other List.""" 201 | 202 | if isinstance(other, Nil): 203 | return False 204 | return self.head() == other.head() and self.tail() == other.tail() 205 | 206 | 207 | class Nil(List[TSource]): 208 | def __init__(self) -> None: 209 | """Initialize List.""" 210 | pass 211 | 212 | def cons(self, element: TSource) -> 'List[TSource]': 213 | """Add element to front of List.""" 214 | 215 | return Cons(lambda sel: sel(element, Nil())) 216 | 217 | def head(self) -> TSource: 218 | """Retrive first element in List.""" 219 | 220 | raise IndexError("List is empty") 221 | 222 | def tail(self) -> 'List[TSource]': 223 | """Return tail of List.""" 224 | 225 | raise IndexError("List is empty") 226 | 227 | def null(self) -> bool: 228 | """Return True if List is empty.""" 229 | return True 230 | 231 | def map(self, mapper: Callable[[TSource], TResult]) -> List[TResult]: 232 | """Map a function over a List.""" 233 | return Nil() 234 | 235 | def apply(self, something: 'List') -> 'List': 236 | # fs <*> xs = [f x | f <- fs, x <- xs] 237 | try: 238 | xs = [f(x) for f in self for x in something] 239 | except TypeError: 240 | xs = [partial(f, x) for f in self for x in something] 241 | 242 | return List.from_iterable(xs) 243 | 244 | def append(self, other: List[TSource]) -> List[TSource]: 245 | """Append other list to this list.""" 246 | 247 | if self.null(): 248 | return other 249 | return (self.tail().append(other)).cons(self.head()) 250 | 251 | def bind(self, fn: Callable[[TSource], List[TResult]]) -> List[TResult]: 252 | """Flatten and map the List. 253 | 254 | Haskell: xs >>= f = concat (map f xs) 255 | """ 256 | return List.concat(self.map(fn)) 257 | 258 | def __iter__(self) -> Iterator: 259 | """Return iterator for List.""" 260 | 261 | while False: 262 | yield 263 | 264 | def __or__(self, func): 265 | """Use | as operator for bind. 266 | 267 | Provide the | operator instead of the Haskell >>= operator 268 | """ 269 | return self.bind(func) 270 | 271 | def __rshift__(self, next): 272 | """The "Then" operator. 273 | 274 | Sequentially compose two monadic actions, discarding any value 275 | produced by the first, like sequencing operators (such as the 276 | semicolon) in imperative languages. 277 | 278 | Haskell: (>>) :: m a -> m b -> m b 279 | """ 280 | return self.bind(lambda _: next) 281 | 282 | def __add__(self, other): 283 | return self.append(other) 284 | 285 | def __len__(self) -> int: 286 | """Return length of List.""" 287 | 288 | return 0 289 | 290 | def __str__(self) -> str: 291 | """Return string representation of List.""" 292 | 293 | return "[%s]" % ", ".join([str(x) for x in self]) 294 | 295 | def __repr__(self) -> str: 296 | """Return string representation of List.""" 297 | 298 | return str(self) 299 | 300 | def __eq__(self, other) -> bool: 301 | """Compare if List is equal to other List.""" 302 | 303 | return True if isinstance(other, Nil) else False 304 | 305 | 306 | assert(isinstance(List, Monoid)) 307 | assert(isinstance(List, Functor)) 308 | assert(isinstance(List, Applicative)) 309 | assert(isinstance(List, Monad)) 310 | 311 | assert(isinstance(Cons, Monoid)) 312 | assert(isinstance(Cons, Functor)) 313 | assert(isinstance(Cons, Applicative)) 314 | assert(isinstance(Cons, Monad)) 315 | 316 | assert(isinstance(Nil, Monoid)) 317 | assert(isinstance(Nil, Functor)) 318 | assert(isinstance(Nil, Applicative)) 319 | assert(isinstance(Nil, Monad)) 320 | -------------------------------------------------------------------------------- /oslash/maybe.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from functools import reduce, partial 3 | 4 | from typing import Callable, Any, Generic, TypeVar, cast 5 | 6 | from .typing import Applicative 7 | from .typing import Functor 8 | from .typing import Monoid 9 | from .typing import Monad 10 | 11 | TSource = TypeVar("TSource") 12 | TResult = TypeVar("TResult") 13 | 14 | 15 | class Maybe(Generic[TSource]): 16 | """Encapsulates an optional value. 17 | 18 | The Maybe type encapsulates an optional value. A value of type 19 | Maybe a either contains a value of (represented as Just a), or it is 20 | empty (represented as Nothing). Using Maybe is a good way to deal 21 | with errors or exceptional cases without resorting to drastic 22 | measures such as error. 23 | """ 24 | 25 | @classmethod 26 | def empty(cls) -> "Maybe[TSource]": 27 | return Nothing() 28 | 29 | @abstractmethod 30 | def __add__(self, other: "Maybe[TSource]") -> "Maybe[TSource]": 31 | raise NotImplementedError 32 | 33 | @abstractmethod 34 | def map(self, mapper: Callable[[TSource], TResult]) -> "Maybe[TResult]": 35 | raise NotImplementedError 36 | 37 | @classmethod 38 | def pure(cls, value: Callable[[TSource], TResult]) -> "Maybe[Callable[[TSource], TResult]]": 39 | raise NotImplementedError 40 | 41 | @abstractmethod 42 | def apply(self: "Maybe[Callable[[TSource], TResult]]", something: "Maybe[TSource]") -> "Maybe[TResult]": 43 | raise NotImplementedError 44 | 45 | @classmethod 46 | @abstractmethod 47 | def unit(cls, a: TSource) -> "Maybe[TSource]": 48 | raise NotImplementedError 49 | 50 | @abstractmethod 51 | def bind(self, fn: Callable[[TSource], "Maybe[TResult]"]) -> "Maybe[TResult]": 52 | raise NotImplementedError 53 | 54 | @classmethod 55 | def concat(cls, xs): 56 | """mconcat :: [m] -> m 57 | 58 | Fold a list using the monoid. For most types, the default 59 | definition for mconcat will be used, but the function is 60 | included in the class definition so that an optimized version 61 | can be provided for specific types. 62 | """ 63 | 64 | def reducer(a, b): 65 | return a + b 66 | 67 | return reduce(reducer, xs, cls.empty()) 68 | 69 | def __rmod__(self, fn): 70 | """Infix version of map. 71 | 72 | Haskell: <$> 73 | 74 | Example: 75 | >>> (lambda x: x+2) % Just(40) 76 | 42 77 | 78 | Returns a new Functor. 79 | """ 80 | return self.map(fn) 81 | 82 | 83 | class Just(Maybe[TSource]): 84 | """A Maybe that contains a value. 85 | 86 | Represents a Maybe that contains a value (represented as Just a). 87 | """ 88 | 89 | def __init__(self, value: TSource) -> None: 90 | self._value = value 91 | 92 | # Monoid Section 93 | # ============== 94 | 95 | def __add__(self, other: Maybe[TSource]) -> Maybe[TSource]: 96 | # m `append` Nothing = m 97 | if isinstance(other, Nothing): 98 | return self 99 | 100 | # Just m1 `append` Just m2 = Just (m1 `append` m2) 101 | return other.map( 102 | lambda other_value: cast(Any, self._value) + other_value if hasattr(self._value, "__add__") else Nothing() 103 | ) 104 | 105 | # Functor Section 106 | # =============== 107 | 108 | def map(self, mapper: Callable[[TSource], TResult]) -> Maybe[TResult]: 109 | # fmap f (Just x) = Just (f x) 110 | 111 | result = mapper(self._value) 112 | 113 | return Just(result) 114 | 115 | # Applicative Section 116 | # =================== 117 | 118 | @classmethod 119 | def pure(cls, value: Callable[[TSource], TResult]) -> "Just[Callable[[TSource], TResult]]": 120 | return Just(value) 121 | 122 | def apply(self: "Just[Callable[[TSource], TResult]]", something: Maybe[TSource]) -> Maybe[TResult]: 123 | def mapper(other_value): 124 | try: 125 | return self._value(other_value) 126 | except TypeError: 127 | return partial(self._value, other_value) 128 | 129 | return something.map(mapper) 130 | 131 | # Monad Section 132 | # ============= 133 | 134 | @classmethod 135 | def unit(cls, value: TSource) -> Maybe[TSource]: 136 | return Just(value) 137 | 138 | def bind(self, func: Callable[[TSource], Maybe[TResult]]) -> Maybe[TResult]: 139 | """Just x >>= f = f x.""" 140 | 141 | value = self._value 142 | return func(value) 143 | 144 | # Utilities Section 145 | # ================= 146 | 147 | def is_just(self) -> bool: 148 | return True 149 | 150 | def is_nothing(self) -> bool: 151 | return False 152 | 153 | # Operator Overloads Section 154 | # ========================== 155 | 156 | def __bool__(self) -> bool: 157 | """Convert Just to bool.""" 158 | return bool(self._value) 159 | 160 | def __eq__(self, other) -> bool: 161 | """Return self == other.""" 162 | 163 | if isinstance(other, Nothing): 164 | return False 165 | 166 | return bool(other.map(lambda other_value: other_value == self._value)) 167 | 168 | def __str__(self) -> str: 169 | return "Just %s" % self._value 170 | 171 | def __repr__(self) -> str: 172 | return str(self) 173 | 174 | 175 | class Nothing(Maybe[TSource]): 176 | 177 | """Represents an empty Maybe. 178 | 179 | Represents an empty Maybe that holds nothing (in which case it has 180 | the value of Nothing). 181 | """ 182 | 183 | # Monoid Section 184 | # ============== 185 | 186 | def __add__(self, other: Maybe) -> Maybe: 187 | # m `append` Nothing = m 188 | return other 189 | 190 | # Functor Section 191 | # =============== 192 | 193 | def map(self, mapper: Callable[[TSource], TResult]) -> Maybe[TResult]: 194 | return Nothing() 195 | 196 | # Applicative Section 197 | # =================== 198 | 199 | @classmethod 200 | def pure(cls, value: Callable[[TSource], TResult]) -> Maybe[Callable[[TSource], TResult]]: 201 | return Nothing() 202 | 203 | def apply(self: "Nothing[Callable[[TSource], TResult]]", something: Maybe[TSource]) -> Maybe[TResult]: 204 | return Nothing() 205 | 206 | # Monad Section 207 | # ============= 208 | 209 | @classmethod 210 | def unit(cls, value: TSource) -> Maybe[TSource]: 211 | return cls() 212 | 213 | def bind(self, func: Callable[[TSource], Maybe[TResult]]) -> Maybe[TResult]: 214 | """Nothing >>= f = Nothing 215 | 216 | Nothing in, Nothing out. 217 | """ 218 | 219 | return Nothing() 220 | 221 | # Utilities Section 222 | # ================= 223 | 224 | def is_pure(self) -> bool: 225 | return False 226 | 227 | def is_nothing(self) -> bool: 228 | return True 229 | 230 | # Operator Overloads Section 231 | # ========================== 232 | 233 | def __eq__(self, other) -> bool: 234 | return isinstance(other, Nothing) 235 | 236 | def __str__(self) -> str: 237 | return "Nothing" 238 | 239 | def __repr__(self) -> str: 240 | return str(self) 241 | 242 | 243 | assert issubclass(Just, Maybe) 244 | assert issubclass(Nothing, Maybe) 245 | 246 | assert isinstance(Maybe, Monoid) 247 | assert isinstance(Maybe, Functor) 248 | assert isinstance(Maybe, Applicative) 249 | assert isinstance(Maybe, Monad) 250 | 251 | assert isinstance(Just, Monoid) 252 | assert isinstance(Just, Functor) 253 | assert isinstance(Just, Applicative) 254 | assert isinstance(Just, Monad) 255 | 256 | assert isinstance(Nothing, Monoid) 257 | assert isinstance(Nothing, Functor) 258 | assert isinstance(Nothing, Applicative) 259 | assert isinstance(Nothing, Monad) 260 | -------------------------------------------------------------------------------- /oslash/monadic.py: -------------------------------------------------------------------------------- 1 | """Some useful Monadic functions. 2 | 3 | This module contains some useful Monadic functions. Most functions 4 | are extension methods to the Monad class, making them available to 5 | subclasses that inherit from Monad. 6 | """ 7 | from typing import Callable, Any 8 | 9 | from .typing import Monad 10 | 11 | 12 | def compose(f: Callable[[Any], Monad], g: Callable[[Any], Monad]) -> Callable[[Any], Monad]: 13 | r"""Monadic compose function. 14 | 15 | Right-to-left Kleisli composition of two monadic functions. 16 | 17 | (<=<) :: Monad m => (b -> m c) -> (a -> m b) -> a -> m c 18 | f <=< g = \x -> g x >>= f 19 | """ 20 | return lambda x: g(x).bind(f) 21 | -------------------------------------------------------------------------------- /oslash/observable.py: -------------------------------------------------------------------------------- 1 | """ The Observable Monad 2 | 3 | * https://www.youtube.com/watch?v=looJcaeboBY 4 | * https://wiki.haskell.org/MonadCont_under_the_hood 5 | * http://blog.sigfpe.com/2008/12/mother-of-all-monads.html 6 | * http://www.haskellforall.com/2012/12/the-continuation-monad.html 7 | 8 | """ 9 | 10 | from typing import Any, Callable, TypeVar, Generic 11 | 12 | from .util import identity, compose 13 | from .typing import Monad, Functor 14 | 15 | TSource = TypeVar("TSource") 16 | TResult = TypeVar("TResult") 17 | 18 | 19 | class Observable(Generic[TSource]): 20 | 21 | """The Rx Observable Monad. 22 | 23 | The Rx Observable monad is based on the Continuation monad 24 | representing suspended computations in continuation-passing style 25 | (CPS). 26 | """ 27 | 28 | def __init__(self, subscribe: Callable[[Callable], Any]) -> None: 29 | """Observable constructor. 30 | 31 | Keyword arguments: 32 | subscribe -- A callable that takes a callable (on_next) 33 | """ 34 | self._get_value = lambda: subscribe 35 | 36 | @classmethod 37 | def unit(cls, x: TSource) -> 'Observable[TSource]': 38 | """x -> Observable x""" 39 | return cls(lambda on_next: on_next(x)) 40 | just = unit 41 | 42 | def map(self, mapper: Callable[[TSource], TResult]) -> 'Observable[TResult]': 43 | r"""Map a function over an observable. 44 | 45 | Haskell: fmap f m = Cont $ \c -> runCont m (c . f) 46 | """ 47 | source = self 48 | return Observable(lambda on_next: source.subscribe(compose(on_next, mapper))) 49 | 50 | def bind(self, fn: Callable[[TSource], 'Observable[TResult]']) -> 'Observable[TResult]': 51 | r"""Chain continuation passing functions. 52 | 53 | Haskell: m >>= k = Cont $ \c -> runCont m $ \a -> runCont (k a) c 54 | """ 55 | source = self 56 | return Observable(lambda on_next: source.subscribe(lambda a: fn(a).subscribe(on_next))) 57 | flat_map = bind 58 | 59 | def filter(self, predicate: Callable[[TSource], bool]) -> 'Observable[TSource]': 60 | """Filter the on_next continuation functions""" 61 | source = self 62 | 63 | def subscribe(on_next): 64 | def _next(x): 65 | if predicate(x): 66 | on_next(x) 67 | 68 | return source.subscribe(_next) 69 | return Observable(subscribe) 70 | 71 | @staticmethod 72 | def call_cc(fn: Callable) -> 'Observable': 73 | r"""call-with-current-continuation. 74 | 75 | Haskell: callCC f = Cont $ \c -> runCont (f (\a -> Cont $ \_ -> c a )) c 76 | """ 77 | def subscribe(on_next): 78 | return fn(lambda a: Observable(lambda _: on_next(a))).subscribe(on_next) 79 | 80 | return Observable(subscribe) 81 | 82 | def subscribe(self, on_next: Callable[[TSource], None]) -> Any: 83 | return self._get_value()(on_next) 84 | 85 | def __or__(self, func): 86 | """Use | as operator for bind. 87 | 88 | Provide the | operator instead of the Haskell >>= operator 89 | """ 90 | return self.bind(func) 91 | 92 | def __eq__(self, other) -> bool: 93 | return self.subscribe(identity) == other.subscribe(identity) 94 | 95 | 96 | assert(isinstance(Observable, Functor)) 97 | assert(isinstance(Observable, Monad)) 98 | -------------------------------------------------------------------------------- /oslash/reader.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from typing import Callable, Any, TypeVar, Generic 4 | 5 | from .typing import Functor 6 | from .typing import Monad 7 | from .typing import Applicative 8 | 9 | TEnv = TypeVar("TEnv") 10 | TSource = TypeVar("TSource") 11 | TResult = TypeVar("TResult") 12 | 13 | 14 | class Reader(Generic[TEnv, TSource]): 15 | """The Reader monad. 16 | 17 | The Reader monad pass the state you want to share between functions. 18 | Functions may read that state, but can't change it. The reader monad 19 | lets us access shared immutable state within a monadic context. 20 | 21 | The Reader is just a fancy name for a wrapped function, so this 22 | monad could also be called the Function monad, or perhaps the 23 | Callable monad. Reader is all about composing wrapped functions. 24 | """ 25 | 26 | def __init__(self, fn: Callable[[TEnv], TSource]) -> None: 27 | """Initialize a new reader.""" 28 | 29 | self.fn = fn 30 | 31 | @classmethod 32 | def unit(cls, value: TSource) -> "Reader[TEnv, TSource]": 33 | r"""The return function creates a Reader that ignores the 34 | environment and produces the given value. 35 | 36 | return a = Reader $ \_ -> a 37 | """ 38 | return cls(lambda _: value) 39 | 40 | def map(self, fn: Callable[[TSource], TResult]) -> "Reader[TEnv, TResult]": 41 | r"""Map a function over the Reader. 42 | 43 | Haskell: 44 | fmap f m = Reader $ \r -> f (runReader m r). 45 | fmap f g = (\x -> f (g x)) 46 | """ 47 | 48 | def _compose(x: Any) -> Any: 49 | return fn(self.run(x)) 50 | 51 | return Reader(_compose) 52 | 53 | def bind(self, fn: "Callable[[TSource], Reader[TEnv, TResult]]") -> "Reader[TEnv, TResult]": 54 | r"""Bind a monadic function to the Reader. 55 | 56 | Haskell: 57 | Reader: m >>= k = Reader $ \r -> runReader (k (runReader m r)) r 58 | Function: h >>= f = \w -> f (h w) w 59 | """ 60 | return Reader(lambda x: fn(self.run(x)).run(x)) 61 | 62 | @classmethod 63 | def pure(cls, fn: Callable[[TSource], TResult]) -> "Reader[TEnv, Callable[[TSource], TResult]]": 64 | return Reader.unit(fn) 65 | 66 | def apply( 67 | self: "Reader[TEnv, Callable[[TSource], TResult]]", something: "Reader[TEnv, TSource]" 68 | ) -> "Reader[TEnv, TResult]": 69 | r"""(<*>) :: f (a -> b) -> f a -> f b. 70 | 71 | Haskell: f <*> g = \x -> f x (g x) 72 | 73 | Apply (<*>) is a beefed up map. It takes a Reader that 74 | has a function in it and another Reader, and extracts that 75 | function from the first Reader and then maps it over the second 76 | one (composes the two functions). 77 | """ 78 | 79 | def comp(env: TEnv): 80 | fn: Callable[[TSource], TResult] = self.run(env) 81 | 82 | value: TSource = something.run(env) 83 | try: 84 | return fn(value) 85 | except TypeError: 86 | return partial(fn, value) 87 | 88 | return Reader(comp) 89 | 90 | def run(self, env: TEnv) -> TSource: 91 | """Run reader in given environment. 92 | 93 | Haskell: runReader :: Reader r a -> r -> a 94 | 95 | Applies given environment on wrapped function. 96 | """ 97 | return self.fn(env) 98 | 99 | def __call__(self, env: TEnv) -> TSource: 100 | """Call the wrapped function.""" 101 | 102 | return self.run(env) 103 | 104 | def __str__(self) -> str: 105 | return "Reader(%s)" % repr(self.fn) 106 | 107 | def __repr__(self) -> str: 108 | return str(self) 109 | 110 | 111 | class MonadReader(Reader[TEnv, TSource]): 112 | 113 | """The MonadReader class. 114 | 115 | The MonadReader class provides a number of convenience functions 116 | that are very useful when working with a Reader monad. 117 | """ 118 | 119 | @classmethod 120 | def ask(cls) -> Reader[TEnv, TEnv]: 121 | r"""Reader $ \x -> x 122 | 123 | Provides a way to easily access the environment. 124 | ask lets us read the environment and then play with it 125 | """ 126 | return Reader(lambda x: x) 127 | 128 | @classmethod 129 | def asks(cls, fn: Callable[[TEnv], TSource]) -> Reader[TEnv, TSource]: 130 | """ 131 | Given a function it returns a Reader which evaluates that 132 | function and returns the result. 133 | 134 | asks :: (e -> a) -> R e a 135 | asks f = do 136 | e <- ask 137 | return $ f e 138 | 139 | asks sel = ask >>= return . sel 140 | """ 141 | 142 | return cls.ask().bind(lambda env: cls.unit(fn(env))) 143 | 144 | def local(self, fn: Callable[[TEnv], TEnv]) -> Reader[TEnv, TSource]: 145 | r"""local transforms the environment a Reader sees. 146 | 147 | local f c = Reader $ \e -> runReader c (f e) 148 | """ 149 | return Reader(lambda env: self.run(fn(env))) 150 | 151 | 152 | assert isinstance(Reader, Functor) 153 | assert isinstance(Reader, Applicative) 154 | assert isinstance(Reader, Monad) 155 | -------------------------------------------------------------------------------- /oslash/state.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Tuple, Any, TypeVar, Generic 2 | 3 | from .util import Unit 4 | from .typing import Functor 5 | from .typing import Monad 6 | 7 | TState = TypeVar("TState") 8 | TSource = TypeVar("TSource") 9 | TResult = TypeVar("TResult") 10 | 11 | 12 | class State(Generic[TSource, TState]): 13 | """The state monad. 14 | 15 | Wraps stateful computations. A stateful computation is a function 16 | that takes a state and returns a result and new state: 17 | 18 | state -> (result, state') 19 | """ 20 | 21 | def __init__(self, fn: Callable[[TState], Tuple[TSource, TState]]) -> None: 22 | """Initialize a new state. 23 | 24 | Keyword arguments: 25 | fn -- State processor. 26 | """ 27 | 28 | self._fn = fn 29 | 30 | @classmethod 31 | def unit(cls, value: TSource) -> "State[TSource, TState]": 32 | r"""Create new State. 33 | 34 | The unit function creates a new State object wrapping a stateful 35 | computation. 36 | 37 | State $ \s -> (x, s) 38 | """ 39 | return cls(lambda state: (value, state)) 40 | 41 | def map(self, mapper: Callable[[TSource], TResult]) -> "State[TResult, TState]": 42 | def _(a: Any, state: Any) -> Tuple[Any, Any]: 43 | return mapper(a), state 44 | 45 | return State(lambda state: _(*self.run(state))) 46 | 47 | def bind(self, fn: Callable[[TSource], "State[TState, TResult]"]) -> "State[TResult, TState]": 48 | r"""m >>= k = State $ \s -> let (a, s') = runState m s 49 | in runState (k a) s' 50 | """ 51 | 52 | def _(result: Any, state: Any) -> Tuple[Any, Any]: 53 | return fn(result).run(state) 54 | 55 | return State(lambda state: _(*self.run(state))) 56 | 57 | @classmethod 58 | def get(cls) -> "State[TState, TState]": 59 | r"""get = state $ \s -> (s, s)""" 60 | return State(lambda state: (state, state)) 61 | 62 | @classmethod 63 | def put(cls, new_state: TState) -> "State[Tuple, TState]": 64 | r"""put newState = state $ \s -> ((), newState)""" 65 | return State(lambda state: (Unit, new_state)) 66 | 67 | def run(self, state: TState) -> Tuple[TSource, TState]: 68 | """Return wrapped state computation. 69 | 70 | This is the inverse of unit and returns the wrapped function. 71 | """ 72 | return self._fn(state) 73 | 74 | def __call__(self, state: Any) -> Tuple: 75 | return self.run(state) 76 | 77 | 78 | assert issubclass(State, Functor) 79 | assert issubclass(State, Monad) 80 | -------------------------------------------------------------------------------- /oslash/typing/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from .applicative import Applicative 3 | from .functor import Functor 4 | from .monoid import Monoid 5 | from .monad import Monad 6 | -------------------------------------------------------------------------------- /oslash/typing/applicative.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | from typing import Callable, TypeVar, Protocol 4 | from typing_extensions import runtime_checkable 5 | 6 | TSource = TypeVar('TSource') 7 | TResult = TypeVar('TResult') 8 | 9 | 10 | @runtime_checkable 11 | class Applicative(Protocol[TSource, TResult]): 12 | """Applicative. 13 | 14 | Applicative functors are functors with some extra properties. 15 | Most importantly, they allow you to apply functions inside the 16 | functor (hence the name). 17 | 18 | To learn more about Applicative functors: 19 | * http://www.davesquared.net/2012/05/fp-newbie-learns-applicatives.html 20 | """ 21 | 22 | @abstractmethod 23 | def apply(self, something): 24 | """Apply wrapped callable. 25 | 26 | Python: apply(self: Applicative, something: Applicative[Callable[[A], B]]) -> Applicative 27 | Haskell: (<*>) :: f (a -> b) -> f a -> f b. 28 | 29 | Apply (<*>) is a beefed up fmap. It takes a functor value that 30 | has a function in it and another functor, and extracts that 31 | function from the first functor and then maps it over the second 32 | one. 33 | """ 34 | raise NotImplementedError 35 | 36 | #def __mul__(self, something): 37 | # """(<*>) :: f (a -> b) -> f a -> f b. 38 | 39 | # Provide the * as an infix version of apply() since we cannot 40 | # represent the Haskell's <*> operator in Python. 41 | # """ 42 | # return self.apply(something) 43 | 44 | #def lift_a2(self, func, b): 45 | # """liftA2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c.""" 46 | 47 | # return func % self * b 48 | 49 | @classmethod 50 | @abstractmethod 51 | def pure(cls, fn: Callable[[TSource], TResult]) -> 'Applicative[TSource, TResult]': 52 | """Applicative functor constructor. 53 | 54 | Use pure if you're dealing with values in an applicative context 55 | (using them with <*>); otherwise, stick to the default class 56 | constructor. 57 | """ 58 | raise NotImplementedError 59 | -------------------------------------------------------------------------------- /oslash/typing/functor.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | from typing import TypeVar, Protocol, Callable 4 | from typing_extensions import runtime_checkable 5 | 6 | TSource = TypeVar('TSource', covariant=True) 7 | TResult = TypeVar('TResult') 8 | 9 | @runtime_checkable 10 | class Functor(Protocol[TSource]): 11 | """The Functor class is used for types that can be mapped over. 12 | 13 | Instances of Functor should satisfy the following laws: 14 | 15 | Haskell: 16 | fmap id == id 17 | fmap (f . g) == fmap f . fmap g 18 | 19 | Python: 20 | x.map(id) == id(x) 21 | x.map(compose(f, g)) == x.map(g).map(f) 22 | 23 | The instances of Functor for lists, Maybe and IO satisfy these laws. 24 | """ 25 | 26 | @abstractmethod 27 | def map(self, fn: Callable[[TSource], TResult]) -> 'Functor[TResult]': 28 | """Map a function over wrapped values. 29 | 30 | Map knows how to apply functions to values that are wrapped in 31 | a context. 32 | """ 33 | raise NotImplementedError 34 | 35 | # def __rmod__(self, fn): 36 | # """Infix version of map. 37 | 38 | # Haskell: <$> 39 | 40 | # Example: 41 | # >>> (lambda x: x+2) % Just(40) 42 | # 42 43 | 44 | # Returns a new Functor. 45 | # """ 46 | # return self.map(fn) 47 | -------------------------------------------------------------------------------- /oslash/typing/monad.py: -------------------------------------------------------------------------------- 1 | r"""The Monad abstract base class. 2 | 3 | All instances of the Monad typeclass should obey the three monad laws: 4 | 5 | 1) Left identity: return a >>= f = f a 6 | 2) Right identity: m >>= return = m 7 | 3) Associativity: (m >>= f) >>= g = m >>= (\x -> f x >>= g) 8 | """ 9 | 10 | from abc import abstractmethod 11 | from typing import TypeVar, Protocol, Callable 12 | from typing_extensions import runtime_checkable 13 | 14 | TSource = TypeVar('TSource') 15 | TResult = TypeVar('TResult') 16 | 17 | 18 | @runtime_checkable 19 | class Monad(Protocol[TSource]): 20 | """Monad protocol""" 21 | 22 | @abstractmethod 23 | def bind(self, fn: Callable[[TSource], 'Monad[TResult]']) -> 'Monad[TResult]': 24 | """Monad bind method. 25 | 26 | Python: bind(self: Monad[A], func: Callable[[A], Monad[B]]) -> Monad[B] 27 | Haskell: (>>=) :: m a -> (a -> m b) -> m b 28 | 29 | This is the mother of all methods. It's hard to describe what it 30 | does, because it can be used for pretty much anything: 31 | 32 | * Transformation, for projecting Monadic values and functions. 33 | * Composition, for composing monadic functions. 34 | * Chaining, for chaining of functions as a monadic value. 35 | * Combining, for combining monadic values. 36 | * Sequencing, of Monadic functions. 37 | * Flattening, of nested Monadic values. 38 | * Variable substitution, assign values to variables. 39 | 40 | The Monad doesn’t specify what is happening, only that whatever 41 | is happening satisfies the laws of associativity and identity. 42 | 43 | Returns a new Monad. 44 | """ 45 | raise NotImplementedError 46 | 47 | @classmethod 48 | @abstractmethod 49 | def unit(value: TSource) -> 'Monad[TSource]': 50 | """Wrap a value in a default context. 51 | 52 | Haskell: return :: a -> m a . 53 | 54 | Inject a value into the monadic type. Since return is a reserved 55 | word in Python, we align with Scala and use the name unit 56 | instead. 57 | """ 58 | raise NotImplementedError 59 | 60 | #def lift(self, func: Callable) -> Monad[B]: 61 | # """Map function over monadic value. 62 | # 63 | # Takes a function and a monadic value and maps the function over the 64 | # monadic value 65 | # 66 | # Haskell: liftM :: (Monad m) => (a -> b) -> m a -> m b 67 | # 68 | # This is really the same function as Functor.fmap, but is instead 69 | # implemented using bind, and does not rely on us inheriting from 70 | # Functor. 71 | # """ 72 | # 73 | # return self.bind(lambda x: Monad.unit(func(x))) 74 | 75 | 76 | #def join(self): 77 | # """join :: Monad m => m (m a) -> m a 78 | # 79 | # The join function is the conventional monad join operator. It is 80 | # used to remove one level of monadic structure, projecting its 81 | # bound argument into the outer level.""" 82 | 83 | # return self.bind(identity) 84 | -------------------------------------------------------------------------------- /oslash/typing/monoid.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import Protocol, TypeVar 3 | from typing_extensions import runtime_checkable 4 | 5 | TSource = TypeVar('TSource') 6 | 7 | 8 | @runtime_checkable 9 | class Monoid(Protocol[TSource]): 10 | """The Monoid abstract base class. 11 | 12 | The class of monoids (types with an associative binary operation that 13 | has an identity). Instances should satisfy the following laws: 14 | 15 | mappend mempty x = x 16 | mappend x mempty = x 17 | mappend x (mappend y z) = mappend (mappend x y) z 18 | mconcat = foldr mappend mempty 19 | """ 20 | 21 | @classmethod 22 | @abstractmethod 23 | def empty(cls) -> 'Monoid[TSource]': 24 | """Create empty monoid. 25 | 26 | Haskell: mempty :: m 27 | 28 | The empty element and identity of append. 29 | """ 30 | 31 | raise NotImplementedError 32 | 33 | def __add__(self, other): 34 | """Append other monoid to this monoid. 35 | 36 | Haskell: mappend :: m -> m -> m 37 | 38 | An associative operation 39 | """ 40 | raise NotImplementedError 41 | -------------------------------------------------------------------------------- /oslash/util/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from .basic import Unit, indent 3 | from .fn import compose, identity, fmap 4 | -------------------------------------------------------------------------------- /oslash/util/basic.py: -------------------------------------------------------------------------------- 1 | Unit = () 2 | 3 | 4 | def indent(level, size=2): 5 | """Return indentation.""" 6 | return ' ' * level * size 7 | -------------------------------------------------------------------------------- /oslash/util/fn.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | 3 | from typing import Tuple, Callable, Any, TypeVar, overload # noqa 4 | 5 | A = TypeVar("A") 6 | B = TypeVar("B") 7 | C = TypeVar("C") 8 | D = TypeVar("D") 9 | E = TypeVar("E") 10 | F = TypeVar("F") 11 | G = TypeVar("G") 12 | 13 | 14 | @overload 15 | def compose() -> Callable[[A], A]: # pylint: disable=function-redefined 16 | ... # pylint: disable=pointless-statement 17 | 18 | 19 | @overload 20 | def compose(op1: Callable[[A], B]) -> Callable[[A], B]: # pylint: disable=function-redefined 21 | ... # pylint: disable=pointless-statement 22 | 23 | 24 | @overload 25 | def compose(op2: Callable[[B], C], op1: Callable[[A], B]) -> Callable[[A], C]: # pylint: disable=function-redefined 26 | ... # pylint: disable=pointless-statement 27 | 28 | 29 | @overload 30 | def compose( 31 | op3: Callable[[C], D], op2: Callable[[B], C], op1: Callable[[A], B] # pylint: disable=function-redefined 32 | ) -> Callable[[A], D]: 33 | ... # pylint: disable=pointless-statement 34 | 35 | 36 | @overload 37 | def compose( 38 | op4: Callable[[D], E], # pylint: disable=function-redefined 39 | op3: Callable[[C], D], 40 | op2: Callable[[B], C], 41 | op1: Callable[[A], B], 42 | ) -> Callable[[A], E]: 43 | ... # pylint: disable=pointless-statement 44 | 45 | 46 | @overload 47 | def compose( 48 | op5: Callable[[E], F], # pylint: disable=function-redefined 49 | op4: Callable[[D], E], 50 | op3: Callable[[C], D], 51 | op2: Callable[[B], C], 52 | op1: Callable[[A], B], 53 | ) -> Callable[[A], F]: 54 | ... # pylint: disable=pointless-statement 55 | 56 | 57 | @overload 58 | def compose( 59 | op1: Callable[[A], B], # pylint: disable=function-redefined,too-many-arguments 60 | op2: Callable[[B], C], 61 | op3: Callable[[C], D], 62 | op4: Callable[[D], E], 63 | op5: Callable[[E], F], 64 | op6: Callable[[F], G], 65 | ) -> Callable[[A], G]: 66 | ... # pylint: disable=pointless-statement 67 | 68 | 69 | def compose(*funcs: Callable) -> Callable: # type: ignore 70 | """Compose multiple functions right to left. 71 | 72 | Composes zero or more functions into a functional composition. The 73 | functions are composed right to left. A composition of zero 74 | functions gives back the identity function. 75 | 76 | compose()(x) == x 77 | compose(f)(x) == f(x) 78 | compose(g, f)(x) == g(f(x)) 79 | compose(h, g, f)(x) == h(g(f(x))) 80 | ... 81 | 82 | Returns the composed function. 83 | """ 84 | 85 | def _compose(source: Any) -> Any: 86 | return reduce(lambda acc, f: f(acc), funcs[::-1], source) 87 | 88 | return _compose 89 | 90 | 91 | fmap = lambda f, g: compose(f, g) # To force partial application 92 | 93 | identity = compose() # type: Callable 94 | -------------------------------------------------------------------------------- /oslash/util/numerals.py: -------------------------------------------------------------------------------- 1 | """ 2 | Lambda calculus. 3 | 4 | * http://en.wikipedia.org/wiki/Church_encoding 5 | * http://www.cs.bham.ac.uk/~axj/pub/papers/lambda-calculus.pdf 6 | * http://vanderwijk.info/blog/pure-lambda-calculus-python/ 7 | * http://eflorenzano.com/blog/2008/11/20/lambda-calculus/ 8 | 9 | Just for the fun of it. 10 | """ 11 | 12 | 13 | identity = lambda x: x 14 | self_apply = lambda s: s(s) 15 | 16 | select_first = lambda first: lambda second: first 17 | select_second = lambda first: lambda second: second 18 | 19 | make_pair = lambda first: lambda second: lambda func: func(first)(second) 20 | 21 | apply = lambda func: lambda arg: func(arg) 22 | 23 | cond = lambda e1: lambda e2: lambda c: c(e1)(e2) 24 | 25 | true = select_first 26 | false = select_second 27 | 28 | iff = lambda c, a, b: c(a)(b) 29 | 30 | not_ = lambda x: cond(false)(true)(x) 31 | and_ = lambda x: lambda y: x(y)(false) 32 | or_ = lambda x: lambda y: x(true)(y) 33 | 34 | # succ = λn.λf.λx.f (n f x) 35 | succ = lambda n: lambda f: lambda x: f(n(f)(x)) 36 | 37 | zero = lambda f: identity 38 | one = lambda f: lambda x: f(x) 39 | 40 | two = succ(one) 41 | three = succ(two) 42 | 43 | iszero = lambda n: n(select_first) 44 | 45 | to_int = lambda n: n(lambda x: x + 1)(0) 46 | printl = lambda n: n(lambda x: x + 1)(0) 47 | -------------------------------------------------------------------------------- /oslash/writer.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Tuple, Any, TypeVar, Generic, Union, cast 2 | 3 | from .typing import Functor 4 | from .typing import Monad 5 | from .typing import Monoid 6 | 7 | TLog = TypeVar("TLog", str, Monoid) 8 | TSource = TypeVar("TSource") 9 | TResult = TypeVar("TResult") 10 | 11 | 12 | class Writer(Generic[TSource, TLog]): 13 | """The writer monad.""" 14 | 15 | def __init__(self, value: TSource, log: TLog) -> None: 16 | """Initialize a new writer. 17 | 18 | value Value to 19 | """ 20 | 21 | self._value: Tuple[TSource, TLog] = (value, log) 22 | 23 | def map(self, func: Callable[[Tuple[TSource, TLog]], Tuple[TResult, TLog]]) -> 'Writer[TResult, TLog]': 24 | """Map a function func over the Writer value. 25 | 26 | Haskell: 27 | fmap f m = Writer $ let (a, w) = runWriter m in (f a, w) 28 | 29 | Keyword arguments: 30 | func -- Mapper function: 31 | """ 32 | a, w = self.run() 33 | b, _w = func((a, w)) 34 | return Writer(b, _w) 35 | 36 | def bind(self, func: Callable[[TSource], 'Writer[TResult, TLog]']) -> 'Writer[TResult, TLog]': 37 | """Flat is better than nested. 38 | 39 | Haskell: 40 | (Writer (x, v)) >>= f = let 41 | (Writer (y, v')) = f x in Writer (y, v `append` v') 42 | """ 43 | a, w = self.run() 44 | b, w_ = func(a).run() 45 | 46 | w__ = w + w_ 47 | 48 | return Writer(b, w__) 49 | 50 | @classmethod 51 | def unit(cls, value: TSource) -> 'Writer[TSource, TLog]': 52 | """Wrap a single value in a Writer. 53 | 54 | Use the factory method to create *Writer classes that 55 | uses a different monoid than str, or use the constructor 56 | directly. 57 | """ 58 | return Writer(value, log=cast(TLog, "")) 59 | 60 | def run(self) -> Tuple[TSource, TLog]: 61 | """Extract value from Writer. 62 | 63 | This is the inverse function of the constructor and converts the 64 | Writer to s simple tuple. 65 | """ 66 | return self._value 67 | 68 | @staticmethod 69 | def apply_log(a: tuple, func: Callable[[Any], Tuple[TSource, TLog]]) -> Tuple[TSource, TLog]: 70 | """Apply a function to a value with a log. 71 | 72 | Helper function to apply a function to a value with a log tuple. 73 | """ 74 | value, log = a 75 | new, entry = func(value) 76 | return new, log + entry 77 | 78 | @classmethod 79 | def create(cls, class_name: str, monoid_type=Union[Monoid, str]): 80 | """Create Writer subclass using specified monoid type. lets us 81 | create a Writer that uses a different monoid than str for the 82 | log. 83 | 84 | Usage: 85 | StringWriter = Writer.create("StringWriter", str) 86 | IntWriter = Writer.create("IntWriter", int) 87 | ... 88 | """ 89 | 90 | def unit(cls, value): 91 | if hasattr(monoid_type, "empty"): 92 | log = monoid_type.empty() 93 | else: 94 | log = monoid_type() 95 | 96 | return cls(value, log) 97 | 98 | return type(class_name, (Writer, ), dict(unit=classmethod(unit))) 99 | 100 | def __eq__(self, other) -> bool: 101 | return self.run() == other.run() 102 | 103 | def __str__(self) -> str: 104 | return "%s :: %s" % self.run() 105 | 106 | def __repr__(self) -> str: 107 | return str(self) 108 | 109 | 110 | class MonadWriter(Writer[Any, TLog]): 111 | 112 | @classmethod 113 | def tell(cls, log: TLog) -> 'MonadWriter': 114 | return cls(None, log) 115 | 116 | 117 | StringWriter = Writer.create("StringWriter", str) 118 | 119 | 120 | assert(isinstance(Writer, Functor)) 121 | assert(isinstance(Writer, Monad)) 122 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "OSlash" 3 | version = "0.6.0" 4 | description = "A functional library for playing with Functors, Applicatives, and Monads in Python" 5 | authors = ["Dag Brattli "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "*" 9 | 10 | [tool.poetry.dev-dependencies] 11 | pytest = "^3.4" 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | typing_extensions -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | 2 | # See the docstring in versioneer.py for instructions. Note that you must 3 | # re-run 'versioneer.py setup' after changing this section, and commit the 4 | # resulting files. 5 | 6 | [versioneer] 7 | VCS = git 8 | style = pep440 9 | versionfile_source = oslash/_version.py 10 | versionfile_build = oslash/_version.py 11 | tag_prefix = v 12 | parentdir_prefix = oslash 13 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from setuptools import setup 3 | import versioneer 4 | 5 | # read the contents of your README file 6 | from os import path 7 | this_directory = path.abspath(path.dirname(__file__)) 8 | with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: 9 | long_description = f.read() 10 | 11 | setup( 12 | name='OSlash', 13 | version=versioneer.get_version(), 14 | cmdclass=versioneer.get_cmdclass(), 15 | description="OSlash (Ø) for Python 3.8+", 16 | long_description=long_description, 17 | long_description_content_type='text/markdown', 18 | author='Dag Brattli', 19 | author_email='dag@brattli.net', 20 | license='MIT License', 21 | url='https://github.com/dbrattli/oslash', 22 | download_url='https://github.com/dbrattli/oslash', 23 | zip_safe=True, 24 | # https://pypi.python.org/pypi?%3Aaction=list_classifiers 25 | classifiers=[ 26 | # 'Development Status :: 3 - Alpha', 27 | 'Development Status :: 4 - Beta', 28 | # 'Development Status :: 5 - Production/Stable', 29 | 'Environment :: Other Environment', 30 | 'Intended Audience :: Developers', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Operating System :: OS Independent', 33 | 'Programming Language :: Python :: 3.8', 34 | 'Programming Language :: Python :: 3.9', 35 | 'Topic :: Software Development :: Libraries :: Python Modules', 36 | ], 37 | install_requires=['typing_extensions'], 38 | setup_requires=['pytest-runner'], 39 | tests_require=['pytest'], 40 | 41 | packages=['oslash', 'oslash.util', 'oslash.typing'], 42 | package_dir={'oslash': 'oslash'} 43 | ) 44 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbrattli/OSlash/c271c7633daf9d72393b419cfc9229e427e6a42a/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_cont.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from oslash.cont import Cont 4 | from oslash.util import identity, compose 5 | 6 | # pure = Cont.pure 7 | unit = Cont.unit 8 | call_cc = Cont.call_cc 9 | 10 | 11 | class TestCont(unittest.TestCase): 12 | def test_cont_pythagoras(self): 13 | add = lambda x, y: unit(x + y) 14 | square = lambda x: unit(x * x) 15 | 16 | pythagoras = lambda x, y: square(x) | ( 17 | lambda xx: (square(y) | ( 18 | lambda yy: add(xx, yy)))) 19 | 20 | self.assertEqual(32, pythagoras(4, 4)(identity)) 21 | 22 | def test_cont_basic(self): 23 | add = lambda x, y: x+y 24 | add_cont = unit(add) 25 | 26 | self.assertEqual( 27 | 42, 28 | add_cont(identity)(40, 2) 29 | ) 30 | 31 | def test_cont_simple(self): 32 | f = lambda x: unit(x*3) 33 | g = lambda x: unit(x-2) 34 | 35 | h = lambda x: f(x) if x == 5 else g(x) 36 | 37 | do_c = unit(5) | h 38 | final_c = lambda x: "Done: %s" % x 39 | 40 | self.assertEqual( 41 | "Done: 15", 42 | do_c.run(final_c) 43 | ) 44 | 45 | def test_cont_simpler(self): 46 | f = lambda x: unit(x*3) 47 | g = lambda x: unit(x-2) 48 | 49 | h = lambda x: f(x) if x == 5 else g(x) 50 | 51 | do_c = unit(4) | h 52 | final_c = lambda x: "Done: %s" % x 53 | 54 | self.assertEqual( 55 | "Done: 2", 56 | do_c.run(final_c) 57 | ) 58 | 59 | def test_cont_call_cc(self): 60 | f = lambda x: unit(x*3) 61 | g = lambda x: unit(x-2) 62 | 63 | h = lambda x, abort: f(x) if x == 5 else abort(-1) 64 | 65 | do_c = lambda n: unit(n) | ( 66 | lambda x: call_cc(lambda abort: h(x, abort))) | ( 67 | lambda y: g(y)) 68 | final_c = lambda x: "Done: %s" % x 69 | 70 | self.assertEqual( 71 | "Done: 13", 72 | do_c(5).run(final_c) 73 | ) 74 | 75 | def test_cont_call_cc_abort(self): 76 | f = lambda x: unit(x*3) 77 | g = lambda x: unit(x-2) 78 | 79 | h = lambda x, abort: f(x) if x == 5 else abort(-1) 80 | 81 | do_c = lambda n: unit(n) | ( 82 | lambda x: call_cc(lambda abort: h(x, abort))) | ( 83 | lambda y: g(y)) 84 | final_c = lambda x: "Done: %s" % x 85 | 86 | self.assertEqual( 87 | "Done: -3", 88 | do_c(4).run(final_c) 89 | ) 90 | 91 | def test_cont_call_cc_abort_2(self): 92 | f = lambda x: unit(x*3) 93 | g = lambda x: unit(x-2) 94 | h = lambda x, abort: f(x) if x == 5 else abort(-1) 95 | 96 | do_c = lambda n: unit(n) | ( 97 | lambda x: call_cc(lambda abort: h(x, abort) | ( 98 | lambda y: g(y)))) 99 | 100 | final_c = lambda x: "Done: %s" % x 101 | 102 | self.assertEqual( 103 | "Done: -1", 104 | do_c(4).run(final_c) 105 | ) 106 | 107 | 108 | class TestContFunctor(unittest.TestCase): 109 | 110 | def test_cont_functor_map(self): 111 | x = unit(42) 112 | f = lambda x: x * 10 113 | 114 | self.assertEqual( 115 | x.map(f), 116 | unit(420) 117 | ) 118 | 119 | def test_cont_functor_law_1(self): 120 | # fmap id = id 121 | x = unit(42) 122 | 123 | self.assertEqual( 124 | x.map(identity), 125 | x 126 | ) 127 | 128 | def test_cont_functor_law2(self): 129 | # fmap (f . g) x = fmap f (fmap g x) 130 | def f(x): 131 | return x+10 132 | 133 | def g(x): 134 | return x*10 135 | 136 | x = unit(42) 137 | 138 | self.assertEqual( 139 | x.map(compose(f, g)), 140 | x.map(g).map(f) 141 | ) 142 | 143 | 144 | class TestContMonad(unittest.TestCase): 145 | 146 | def test_cont_monad_bind(self): 147 | m = unit(42) 148 | f = lambda x: unit(x*10) 149 | 150 | self.assertEqual( 151 | m.bind(f), 152 | unit(420) 153 | ) 154 | 155 | def test_cont_monad_law_left_identity(self): 156 | # return x >>= f is the same thing as f x 157 | 158 | f = lambda x: unit(x+100000) 159 | x = 3 160 | 161 | self.assertEqual( 162 | unit(x).bind(f), 163 | f(x) 164 | ) 165 | 166 | def test_cont_monad_law_right_identity(self): 167 | # m >>= return is no different than just m. 168 | 169 | m = unit("move on up") 170 | 171 | self.assertEqual( 172 | m.bind(unit), 173 | m 174 | ) 175 | 176 | def test_cont_monad_law_associativity(self): 177 | # (m >>= f) >>= g is just like doing m >>= (\x -> f x >>= g) 178 | m = unit(42) 179 | f = lambda x: unit(x+1000) 180 | g = lambda y: unit(y*42) 181 | 182 | self.assertEqual( 183 | m.bind(f).bind(g), 184 | m.bind(lambda x: f(x).bind(g)) 185 | ) 186 | -------------------------------------------------------------------------------- /tests/test_do.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import unittest 4 | 5 | from oslash.list import List 6 | from oslash.do import do, let, guard 7 | 8 | 9 | class TestDo(unittest.TestCase): 10 | def test_do_list_basic(self): 11 | l = lambda *items: List.from_iterable(items) 12 | out1 = do(let(a=l(3, 10, 6)), 13 | let(b=l(100, 200)), 14 | lambda e: List.unit(e.a + e.b)) 15 | out2 = l(3, 10, 6) | (lambda a: 16 | l(100, 200) | (lambda b: 17 | List.unit(a + b))) 18 | self.assertEqual(out1, out2) 19 | 20 | def test_do_list_pythag(self): 21 | r = lambda low, high: List.from_iterable(range(low, high)) 22 | out1 = do(let(z=r(1, 21)), 23 | let(x=lambda e: r(1, e.z+1)), 24 | let(y=lambda e: r(e.x, e.z+1)), 25 | lambda e: guard(List, e.x*e.x + e.y*e.y == e.z*e.z), 26 | lambda e: List.unit((e.x, e.y, e.z))) 27 | out2 = r(1, 21) | (lambda z: 28 | r(1, z+1) | (lambda x: 29 | r(x, z+1) | (lambda y: 30 | guard(List, x*x + y*y == z*z) >> 31 | List.unit((x,y,z))))) 32 | self.assertEqual(out1, out2) 33 | 34 | # TODO: add more tests 35 | 36 | 37 | class TestDoErrors(unittest.TestCase): 38 | def test_do_invalid_input(self): 39 | try: 40 | do("some invalid input") 41 | except TypeError: 42 | pass 43 | else: 44 | assert False 45 | 46 | def test_do_scoping_correct(self): 47 | try: 48 | l = lambda *items: List.from_iterable(items) 49 | do(let(a=l(1, 2, 3)), 50 | let(b=lambda e: List.unit(e.a)), 51 | lambda e: List.unit(e.a * e.b)) 52 | except AttributeError: 53 | assert False 54 | 55 | def test_do_scoping_invalid(self): 56 | try: 57 | l = lambda *items: List.from_iterable(items) 58 | do(let(a=lambda e: List.unit(e.b)), 59 | let(b=l(1, 2, 3)), 60 | lambda e: List.unit(e.a * e.b)) 61 | except AttributeError as err: 62 | pass 63 | else: 64 | assert False 65 | -------------------------------------------------------------------------------- /tests/test_either.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from oslash.either import Right, Left 4 | 5 | 6 | class TestEither(unittest.TestCase): 7 | 8 | def test_either_right_map(self) -> None: 9 | a = Right(42).map(lambda x: x * 10) 10 | self.assertEqual(a, Right(420)) 11 | 12 | def test_either_left_map(self) -> None: 13 | a = Left(42).map(lambda x: x*10) 14 | self.assertEqual(a, Left(42)) 15 | 16 | def test_either_right_functor_law1(self) -> None: 17 | """fmap id = id""" 18 | 19 | self.assertEqual(Right(3).map(lambda x: x), Right(3)) 20 | 21 | def test_either_right_functor_law2(self) -> None: 22 | """fmap (f . g) x = fmap f (fmap g x)""" 23 | def f(x: int) -> int: 24 | return x + 10 25 | 26 | def g(x: int) -> int: 27 | return x * 10 28 | 29 | self.assertEqual( 30 | Right(42).map(f).map(g), 31 | Right(42).map(lambda x: g(f(x))) 32 | ) 33 | 34 | def test_either_left_functor_law1(self) -> None: 35 | """fmap id = id""" 36 | 37 | self.assertEqual(Left(3).map(lambda x: x), Left(3)) 38 | 39 | def test_either_left_functor_law2(self) -> None: 40 | """fmap (f . g) x = fmap f (fmap g x)""" 41 | def f(x): 42 | return x + 10 43 | 44 | def g(x): 45 | return x * 10 46 | 47 | self.assertEqual( 48 | Left(42).map(f).map(g), 49 | Left(42).map(lambda x: g(f(x))) 50 | ) 51 | 52 | def test_right_applicative_1(self) -> None: 53 | a = Right.pure(lambda x, y: x + y).apply(Right(2)).apply(Right(40)) 54 | self.assertNotEqual(a, Left(42)) 55 | self.assertEqual(a, Right(42)) 56 | 57 | def test_right_applicative_2(self) -> None: 58 | a = Right.pure(lambda x, y: x + y).apply(Left("error")).apply(Right(42)) 59 | self.assertEqual(a, Left("error")) 60 | 61 | def test_right_applicative_3(self) -> None: 62 | a = Right.pure(lambda x, y: x + y).apply(Right(42)).apply(Left("error")) 63 | self.assertEqual(a, Left("error")) 64 | 65 | def test_either_monad_right_bind_right(self) -> None: 66 | m = Right(42).bind(lambda x: Right(x * 10)) 67 | self.assertEqual(m, Right(420)) 68 | 69 | def test_either_monad_right_bind_left(self) -> None: 70 | """Nothing >>= \\x -> return (x*10)""" 71 | m = Left("error").bind(lambda x: Right(x * 10)) 72 | self.assertEqual(m, Left("error")) 73 | -------------------------------------------------------------------------------- /tests/test_identity.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the Identity Monad. 3 | 4 | Functor laws: 5 | * http://learnyouahaskell.com/functors-applicative-functors-and-monoids 6 | 7 | Applicative laws: 8 | * http://en.wikibooks.org/wiki/Haskell/Applicative_Functors 9 | 10 | Monad laws: 11 | * http://learnyouahaskell.com/a-fistful-of-monads#monad-laws 12 | * https://wiki.haskell.org/Monad_laws 13 | """ 14 | import unittest 15 | 16 | from oslash.identity import Identity 17 | from oslash.util import identity, compose, fmap 18 | 19 | pure = Identity.pure 20 | unit = Identity.unit 21 | 22 | 23 | class TestIdentityFunctor(unittest.TestCase): 24 | def test_identity_functor_map(self): 25 | x = unit(42) 26 | f = lambda x: x * 10 27 | 28 | self.assertEqual(x.map(f), unit(420)) 29 | 30 | def test_identity_functor_law_1(self): 31 | # fmap id = id 32 | x = unit(42) 33 | 34 | self.assertEqual(x.map(identity), x) 35 | 36 | def test_identity_functor_law2(self): 37 | # fmap (f . g) x = fmap f (fmap g x) 38 | def f(x): 39 | return x + 10 40 | 41 | def g(x): 42 | return x * 10 43 | 44 | x = unit(42) 45 | 46 | self.assertEqual(x.map(compose(f, g)), x.map(g).map(f)) 47 | 48 | 49 | class TestIdentityApplicative(unittest.TestCase): 50 | def test_identity_applicative_law_functor(self): 51 | # pure f <*> x = fmap f x 52 | x = unit(42) 53 | f = lambda x: x * 42 54 | 55 | self.assertEqual(pure(f).apply(x), x.map(f)) 56 | 57 | def test_identity_applicative_law_identity(self): 58 | # pure id <*> v = v 59 | v = unit(42) 60 | 61 | self.assertEqual(pure(identity).apply(v), v) 62 | 63 | def test_identity_applicative_law_composition(self): 64 | # pure (.) <*> u <*> v <*> w = u <*> (v <*> w) 65 | 66 | w = unit(42) 67 | u = pure(lambda x: x * 42) 68 | v = pure(lambda x: x + 42) 69 | 70 | self.assertEqual(pure(fmap).apply(u).apply(v).apply(w), u.apply(v.apply(w))) 71 | 72 | def test_identity_applicative_law_homomorphism(self): 73 | # pure f <*> pure x = pure (f x) 74 | x = 42 75 | f = lambda x: x * 42 76 | 77 | self.assertEqual(pure(f).apply(pure(x)), pure(f(x))) 78 | 79 | def test_identity_applicative_law_interchange(self): 80 | # u <*> pure y = pure ($ y) <*> u 81 | 82 | y = 43 83 | u = unit(lambda x: x * 42) 84 | 85 | self.assertEqual(u.apply(pure(y)), pure(lambda f: f(y)).apply(u)) 86 | 87 | 88 | class TestIdentityMonad(unittest.TestCase): 89 | def test_identity_monad_bind(self): 90 | m = unit(42) 91 | f = lambda x: unit(x * 10) 92 | 93 | self.assertEqual(m.bind(f), unit(420)) 94 | 95 | def test_identity_monad_law_left_identity(self): 96 | # return x >>= f is the same thing as f x 97 | 98 | f = lambda x: unit(x + 100000) 99 | x = 3 100 | 101 | self.assertEqual(unit(x).bind(f), f(x)) 102 | 103 | def test_identity_monad_law_right_identity(self): 104 | # m >>= return is no different than just m. 105 | 106 | m = unit("move on up") 107 | 108 | self.assertEqual(m.bind(unit), m) 109 | 110 | def test_identity_monad_law_associativity(self): 111 | # (m >>= f) >>= g is just like doing m >>= (\x -> f x >>= g) 112 | m = unit(42) 113 | f = lambda x: unit(x + 1000) 114 | g = lambda y: unit(y * 42) 115 | 116 | self.assertEqual(m.bind(f).bind(g), m.bind(lambda x: f(x).bind(g))) 117 | -------------------------------------------------------------------------------- /tests/test_ioaction.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from typing import Tuple, Optional 3 | 4 | import oslash.ioaction 5 | from oslash import Put, Return, put_line, get_line 6 | from oslash.util import Unit 7 | 8 | 9 | class MyMock: 10 | """Mock for testing side effects""" 11 | 12 | def __init__(self) -> None: 13 | self.value: Optional[str] = None 14 | oslash.ioaction.pure_input = self.pure_input 15 | oslash.ioaction.pure_print = self.pure_print 16 | 17 | def pure_print(self, world: int, text: str) -> int: 18 | self.value = text 19 | return world + 1 20 | 21 | def pure_input(self, world: int) -> Tuple[int, str]: 22 | text = self.value 23 | new_world = world + 1 24 | return new_world, text 25 | 26 | 27 | class TestPut(unittest.TestCase): 28 | 29 | def test_put_line(self) -> None: 30 | pm = MyMock() 31 | action = put_line("hello, world!") 32 | action() 33 | self.assertEqual(pm.value, "hello, world!") 34 | 35 | def test_put_return(self) -> None: 36 | pm = MyMock() 37 | action: Put = Put("hello, world!", Return(Unit)) 38 | action() 39 | self.assertEqual(pm.value, "hello, world!") 40 | -------------------------------------------------------------------------------- /tests/test_list.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import unittest 3 | 4 | from oslash.list import List 5 | from oslash.util import identity, compose, fmap 6 | 7 | pure = List.pure 8 | unit = List.unit 9 | empty = List.empty 10 | 11 | 12 | class TestList(unittest.TestCase): 13 | def test_list_null(self): 14 | xs = empty() 15 | assert xs.null() 16 | 17 | def test_list_not_null_after_cons_and_tail(self): 18 | xs = empty().cons(1).tail() 19 | assert xs.null() 20 | 21 | def test_list_not_null_after_cons(self): 22 | xs = empty().cons(1) 23 | assert not xs.null() 24 | 25 | def test_list_head(self): 26 | x = empty().cons(42).head() 27 | self.assertEqual(42, x) 28 | 29 | def test_list_tail_head(self): 30 | xs = empty().cons("b").cons("a") 31 | self.assertEqual("a", xs.head()) 32 | 33 | def test_list_tail_tail_null(self): 34 | xs = empty().cons("b").cons("a") 35 | assert xs.tail().tail().null() 36 | 37 | def test_list_list(self): 38 | xs = empty().cons(empty().cons(42)) 39 | self.assertEqual(42, xs.head().head()) 40 | 41 | def test_list_length_empty(self): 42 | xs = empty() 43 | self.assertEqual(0, len(xs)) 44 | 45 | def test_list_length_non_empty(self): 46 | xs = List.unit(42) 47 | self.assertEqual(1, len(xs)) 48 | 49 | def test_list_length_multiple(self): 50 | xs = List.from_iterable(range(42)) 51 | self.assertEqual(42, len(xs)) 52 | 53 | def test_list_append_empty(self): 54 | xs = empty() 55 | ys = List.unit(42) 56 | zs = xs.append(ys) 57 | self.assertEqual(ys, zs) 58 | 59 | def test_list_append_empty_other(self): 60 | xs = List.unit(42) 61 | ys = empty() 62 | zs = xs.append(ys) 63 | self.assertEqual(xs, zs) 64 | 65 | def test_list_append_non_empty(self): 66 | xs = List.from_iterable(range(5)) 67 | ys = List.from_iterable(range(5, 10)) 68 | zs = xs.append(ys) 69 | self.assertEqual(List.from_iterable(range(10)), zs) 70 | 71 | 72 | class TestListFunctor(unittest.TestCase): 73 | def test_list_functor_map(self): 74 | # fmap f (return v) = return (f v) 75 | x = unit(42) 76 | f = lambda x: x * 10 77 | 78 | self.assertEqual(x.map(f), unit(420)) 79 | 80 | y = List.from_iterable([1, 2, 3, 4]) 81 | g = lambda x: x * 10 82 | 83 | self.assertEqual(y.map(g), List.from_iterable([10, 20, 30, 40])) 84 | 85 | def test_list_functor_law_1(self): 86 | # fmap id = id 87 | 88 | # Singleton list using return 89 | x = unit(42) 90 | self.assertEqual(x.map(identity), x) 91 | 92 | # Empty list 93 | y = empty() 94 | self.assertEqual(y.map(identity), y) 95 | 96 | # Long list 97 | z = List.from_iterable(range(42)) 98 | self.assertEqual(z.map(identity), z) 99 | 100 | def test_list_functor_law2(self): 101 | # fmap (f . g) x = fmap f (fmap g x) 102 | def f(x): 103 | return x + 10 104 | 105 | def g(x): 106 | return x * 10 107 | 108 | # Singleton list 109 | x = unit(42) 110 | self.assertEqual(x.map(compose(f, g)), x.map(g).map(f)) 111 | 112 | # Empty list 113 | y = List.empty() 114 | self.assertEqual(y.map(compose(f, g)), y.map(g).map(f)) 115 | 116 | # Long list 117 | z = List.from_iterable(range(42)) 118 | self.assertEqual(z.map(compose(f, g)), z.map(g).map(f)) 119 | 120 | 121 | class TestListApplicative(unittest.TestCase): 122 | def test_list_applicative_law_functor(self): 123 | # pure f <*> x = fmap f x 124 | f = lambda x: x * 42 125 | 126 | x = unit(42) 127 | self.assertEqual(pure(f).apply(x), x.map(f)) 128 | 129 | # Empty list 130 | z = empty() 131 | self.assertEqual(pure(f).apply(z), z.map(f)) 132 | 133 | # Long list 134 | z = List.from_iterable(range(42)) 135 | self.assertEqual(pure(f).apply(z), z.map(f)) 136 | 137 | def test_list_applicative_law_identity(self): 138 | # pure id <*> v = v 139 | 140 | # Singleton list 141 | x = unit(42) 142 | self.assertEqual(pure(identity).apply(x), x) 143 | 144 | # Empty list 145 | y = List.empty() 146 | self.assertEqual(pure(identity).apply(y), y) 147 | 148 | # Log list 149 | y = List.from_iterable(range(42)) 150 | self.assertEqual(pure(identity).apply(y), y) 151 | 152 | def test_identity_applicative_law_composition(self): 153 | # pure (.) <*> u <*> v <*> w = u <*> (v <*> w) 154 | 155 | u = pure(lambda x: x * 42) 156 | v = pure(lambda x: x + 42) 157 | 158 | # Singleton list 159 | w = unit(42) 160 | self.assertEqual(pure(fmap).apply(u).apply(v).apply(w), u.apply(v.apply(w))) 161 | 162 | def test_identity_applicative_law_composition_empty(self): 163 | # pure (.) <*> u <*> v <*> w = u <*> (v <*> w) 164 | 165 | u = pure(lambda x: x * 42) 166 | v = pure(lambda x: x + 42) 167 | 168 | # Empty list 169 | w = empty() 170 | self.assertEqual(pure(fmap).apply(u).apply(v).apply(w), u.apply(v.apply(w))) 171 | 172 | def test_identity_applicative_law_composition_range(self): 173 | # pure (.) <*> u <*> v <*> w = u <*> (v <*> w) 174 | 175 | u = pure(lambda x: x * 42) 176 | v = pure(lambda x: x + 42) 177 | 178 | # Long list 179 | w = List.from_iterable(range(42)) 180 | self.assertEqual(pure(fmap).apply(u).apply(v).apply(w), u.apply(v.apply(w))) 181 | 182 | def test_list_applicative_binary_func_singleton(self): 183 | f = lambda x, y: x + y 184 | v = unit(2) 185 | w = unit(40) 186 | 187 | self.assertEqual(pure(f).apply(v).apply(w), unit(42)) 188 | 189 | def test_list_applicative_unary_func(self): 190 | f = lambda x: x * 2 191 | v = unit(21) 192 | 193 | self.assertEqual(pure(f).apply(v), unit(42)) 194 | 195 | def test_list_applicative_binary_func(self): 196 | f = lambda x, y: x + y 197 | v = List.from_iterable([1, 2]) 198 | w = List.from_iterable([4, 8]) 199 | 200 | self.assertEqual(pure(f).apply(v).apply(w), List.from_iterable([5, 9, 6, 10])) 201 | 202 | def test_list_applicative_empty_func(self): 203 | v = unit(42) 204 | w = List.from_iterable([1, 2, 3]) 205 | 206 | self.assertEqual(empty().apply(v).apply(w), empty()) 207 | 208 | def test_list_applicative_binary_func_empty_arg_1(self): 209 | f = lambda x, y: x + y 210 | v = unit(42) 211 | e = empty() 212 | 213 | self.assertEqual(pure(f).apply(e).apply(v), empty()) 214 | 215 | def test_list_applicative_binary_func_empty_arg_2(self): 216 | f = lambda x, y: x + y 217 | v = unit(42) 218 | e = empty() 219 | 220 | self.assertEqual(pure(f).apply(v).apply(e), empty()) 221 | 222 | 223 | class TestListMonad(unittest.TestCase): 224 | def test_list_monad_bind(self): 225 | m = unit(42) 226 | f = lambda x: unit(x * 10) 227 | 228 | self.assertEqual(m.bind(f), unit(420)) 229 | 230 | def test_list_monad_empty_bind(self): 231 | m = empty() 232 | f = lambda x: unit(x * 10) 233 | 234 | self.assertEqual(m.bind(f), empty()) 235 | 236 | def test_list_monad_law_left_identity(self): 237 | # return x >>= f is the same thing as f x 238 | 239 | f = lambda x: unit(x + 100000) 240 | v = 42 241 | 242 | self.assertEqual(unit(v).bind(f), f(v)) 243 | 244 | def test_list_monad_law_right_identity(self): 245 | # m >>= return is no different than just m. 246 | 247 | m = unit("move on up") 248 | 249 | self.assertEqual(m.bind(unit), m) 250 | 251 | def test_list_monad_law_associativity(self): 252 | # (m >>= f) >>= g is just like doing m >>= (\x -> f x >>= g) 253 | f = lambda x: unit(x + 1000) 254 | g = lambda y: unit(y * 42) 255 | 256 | m = unit(42) 257 | self.assertEqual(m.bind(f).bind(g), m.bind(lambda x: f(x).bind(g))) 258 | 259 | def test_list_monad_law_associativity_empty(self): 260 | # (m >>= f) >>= g is just like doing m >>= (\x -> f x >>= g) 261 | f = lambda x: unit(x + 1000) 262 | g = lambda y: unit(y * 42) 263 | 264 | # Empty list 265 | m = empty() 266 | self.assertEqual(m.bind(f).bind(g), m.bind(lambda x: f(x).bind(g))) 267 | 268 | def test_list_monad_law_associativity_range(self): 269 | # (m >>= f) >>= g is just like doing m >>= (\x -> f x >>= g) 270 | f = lambda x: unit(x + 1000) 271 | g = lambda y: unit(y * 42) 272 | 273 | # Long list 274 | m = List.from_iterable(range(42)) 275 | self.assertEqual(m.bind(f).bind(g), m.bind(lambda x: f(x).bind(g))) 276 | -------------------------------------------------------------------------------- /tests/test_maybe.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import unittest 3 | 4 | from oslash.maybe import Maybe, Just, Nothing 5 | from oslash.util import identity, compose, fmap 6 | 7 | pure = Just.pure 8 | unit = Just.unit 9 | 10 | 11 | class TestMaybeFunctor(unittest.TestCase): 12 | def test_just_functor_map(self): 13 | f = lambda x: x * 2 14 | x = Just(21) 15 | 16 | self.assertEqual( 17 | x.map(f), 18 | Just(42) 19 | ) 20 | 21 | def test_nothing_functor_map(self): 22 | f = lambda x: x + 2 23 | x = Nothing() 24 | 25 | self.assertEqual( 26 | x.map(f), 27 | x 28 | ) 29 | 30 | def test_nothing_functor_law1(self): 31 | # fmap id = id 32 | self.assertEqual( 33 | Nothing().map(identity), 34 | Nothing() 35 | ) 36 | 37 | def test_just_functor_law1(self): 38 | # fmap id = id 39 | x = Just(3) 40 | self.assertEqual( 41 | x.map(identity), 42 | x 43 | ) 44 | 45 | def test_just_functor_law2(self): 46 | # fmap (f . g) x = fmap f (fmap g x) 47 | def f(x): 48 | return x + 10 49 | 50 | def g(x): 51 | return x * 10 52 | 53 | x = Just(42) 54 | 55 | self.assertEqual( 56 | x.map(compose(f, g)), 57 | x.map(g).map(f) 58 | ) 59 | 60 | def test_nothing_functor_law2(self): 61 | # fmap (f . g) x = fmap f (fmap g x) 62 | def f(x): 63 | return x+10 64 | 65 | def g(x): 66 | return x*10 67 | 68 | x = Nothing() 69 | 70 | self.assertEqual( 71 | x.map(compose(f, g)), 72 | x.map(g).map(f) 73 | ) 74 | 75 | 76 | class TestMaybeApplicative(unittest.TestCase): 77 | 78 | def test_just_applicative_law_functor(self): 79 | # pure f <*> x = fmap f x 80 | x = unit(42) 81 | f = lambda x: x * 42 82 | 83 | self.assertEqual( 84 | pure(f).apply(x), 85 | x.map(f) 86 | ) 87 | 88 | def test_nothing_applicative_law_functor(self): 89 | # pure f <*> x = fmap f x 90 | x = Nothing() 91 | f = lambda x: x * 42 92 | 93 | self.assertEqual( 94 | pure(f).apply(x), 95 | x.map(f) 96 | ) 97 | 98 | def test_just_applicative_law_identity(self): 99 | # pure id <*> v = v 100 | v = unit(42) 101 | 102 | self.assertEqual( 103 | pure(identity).apply(v), 104 | v 105 | ) 106 | 107 | def test_nothing_applicative_law_identity(self): 108 | # pure id <*> v = v 109 | v = Nothing() 110 | 111 | self.assertEqual( 112 | pure(identity).apply(v), 113 | v 114 | ) 115 | 116 | def test_just_applicative_law_composition(self): 117 | # pure (.) <*> u <*> v <*> w = u <*> (v <*> w) 118 | 119 | w = unit(42) 120 | u = pure(lambda x: x * 42) 121 | v = pure(lambda x: x + 42) 122 | 123 | self.assertEqual( 124 | pure(fmap).apply(u).apply(v).apply(w), 125 | u.apply(v.apply(w)) 126 | ) 127 | 128 | def test_identity_applicative_law_composition(self): 129 | # pure (.) <*> u <*> v <*> w = u <*> (v <*> w) 130 | 131 | w = Nothing() 132 | u = pure(lambda x: x * 42) 133 | v = pure(lambda x: x + 42) 134 | 135 | self.assertEqual( 136 | pure(fmap).apply(u).apply(v).apply(w), 137 | u.apply(v.apply(w)) 138 | ) 139 | 140 | def test_just_applicative_law_homomorphism(self): 141 | # pure f <*> pure x = pure (f x) 142 | x = 42 143 | f = lambda x: x * 42 144 | 145 | self.assertEqual( 146 | pure(f).apply(pure(x)), 147 | pure(f(x)) 148 | ) 149 | 150 | def test_nothing_applicative_law_homomorphism(self): 151 | # pure f <*> pure x = pure (f x) 152 | f = lambda x: x * 42 153 | 154 | self.assertEqual( 155 | pure(f).apply(Nothing()), 156 | Nothing() 157 | ) 158 | 159 | def test_just_applicative_law_interchange(self): 160 | # u <*> pure y = pure ($ y) <*> u 161 | 162 | y = 43 163 | u = unit(lambda x: x*42) 164 | 165 | self.assertEqual( 166 | u.apply(pure(y)), 167 | pure(lambda f: f(y)).apply(u) 168 | ) 169 | 170 | def test_nothing_applicative_law_interchange(self): 171 | # u <*> pure y = pure ($ y) <*> u 172 | 173 | u = unit(lambda x: x*42) 174 | 175 | self.assertEqual( 176 | u.apply(Nothing()), 177 | Nothing().apply(u) 178 | ) 179 | 180 | def test_just_applicative_1(self): 181 | a = Just.pure(lambda x, y: x+y).apply(Just(2)).apply(Just(40)) 182 | self.assertNotEqual(a, Nothing()) 183 | self.assertEqual(a, Just(42)) 184 | 185 | def test_just_applicative_2(self): 186 | a = Just.pure(lambda x, y: x+y).apply(Nothing()).apply(Just(42)) 187 | self.assertEqual(a, Nothing()) 188 | 189 | def test_just_applicative_3(self): 190 | a = Just.pure(lambda x, y: x+y).apply(Just(42)).apply(Nothing()) 191 | self.assertEqual(a, Nothing()) 192 | 193 | 194 | class TestMaybeMonoid(unittest.TestCase): 195 | 196 | def test_maybe_monoid_nothing_append_just(self): 197 | m = Just("Python") 198 | 199 | self.assertEqual( 200 | Nothing() + m, 201 | m 202 | ) 203 | 204 | def test_maybe_monoid_just_append_nothing(self): 205 | m = Just("Python") 206 | 207 | self.assertEqual( 208 | m + Nothing(), 209 | m 210 | ) 211 | 212 | def test_maybe_monoid_just_append_just(self): 213 | m = Just("Python") 214 | n = Just(" rocks!") 215 | 216 | self.assertEqual( 217 | m + n, 218 | Just("Python rocks!") 219 | ) 220 | 221 | def test_maybe_monoid_concat(self): 222 | 223 | self.assertEqual( 224 | Maybe.concat([Just(2), Just(40)]), 225 | Just(42) 226 | ) 227 | 228 | 229 | class TestMaybeMonad(unittest.TestCase): 230 | 231 | def test_just_monad_bind(self): 232 | m = unit(42) 233 | f = lambda x: unit(x*10) 234 | 235 | self.assertEqual( 236 | m.bind(f), 237 | unit(420) 238 | ) 239 | 240 | def test_nothing_monad_bind(self): 241 | m = Nothing() 242 | f = lambda x: unit(x*10) 243 | 244 | self.assertEqual( 245 | m.bind(f), 246 | Nothing() 247 | ) 248 | 249 | def test_just_monad_law_left_identity(self): 250 | # return x >>= f is the same thing as f x 251 | 252 | f = lambda x: unit(x+100000) 253 | x = 3 254 | 255 | self.assertEqual( 256 | unit(x).bind(f), 257 | f(x) 258 | ) 259 | 260 | def test_nothing_monad_law_left_identity(self): 261 | # return x >>= f is the same thing as f x 262 | 263 | f = lambda x: unit(x+100000) 264 | 265 | self.assertEqual( 266 | Nothing().bind(f), 267 | Nothing() 268 | ) 269 | 270 | def test_just_monad_law_right_identity(self): 271 | # m >>= return is no different than just m. 272 | 273 | m = unit("move on up") 274 | 275 | self.assertEqual( 276 | m.bind(unit), 277 | m 278 | ) 279 | 280 | def test_nothing_monad_law_right_identity(self): 281 | # m >>= return is no different than just m. 282 | 283 | m = Nothing() 284 | 285 | self.assertEqual( 286 | m.bind(unit), 287 | m 288 | ) 289 | 290 | def test_just_monad_law_associativity(self): 291 | # (m >>= f) >>= g is just like doing m >>= (\x -> f x >>= g) 292 | m = unit(42) 293 | f = lambda x: unit(x+1000) 294 | g = lambda y: unit(y*42) 295 | 296 | self.assertEqual( 297 | m.bind(f).bind(g), 298 | m.bind(lambda x: f(x).bind(g)) 299 | ) 300 | 301 | def test_nothing_monad_law_associativity(self): 302 | # (m >>= f) >>= g is just like doing m >>= (\x -> f x >>= g) 303 | m = Nothing() 304 | f = lambda x: unit(x+1000) 305 | g = lambda y: unit(y*42) 306 | 307 | self.assertEqual( 308 | m.bind(f).bind(g), 309 | m.bind(lambda x: f(x).bind(g)) 310 | ) 311 | 312 | def test_combine_just_and_just_rule1(self): 313 | self.assertEqual( 314 | Just(5) and Just(6), 315 | Just(6) 316 | ) 317 | 318 | def test_combine_just_and_just_rule2(self): 319 | self.assertEqual( 320 | Just(0) and Just(6), 321 | Just(0) 322 | ) 323 | 324 | def test_combine_just_or_nothing_rule1(self): 325 | self.assertEqual( 326 | Just(5) or Nothing, 327 | Just(5) 328 | ) 329 | 330 | def test_combine_just_or_nothing_rule2(self): 331 | self.assertEqual( 332 | Just(0) or Nothing, 333 | Nothing 334 | ) 335 | -------------------------------------------------------------------------------- /tests/test_monad.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from oslash import StringWriter, MonadWriter 4 | from oslash.monadic import compose 5 | 6 | 7 | class TestMonadMonadic(unittest.TestCase): 8 | 9 | def test_monad_monadic_compose(self): 10 | unit, tell = StringWriter.unit, MonadWriter.tell 11 | 12 | def half(x): 13 | return tell("I just halved %s!" % x).bind(lambda _: unit(x//2)) 14 | 15 | quarter = compose(half, half) 16 | value, log = quarter(8).run() 17 | 18 | self.assertEqual(2, value) 19 | self.assertEqual("I just halved 8!I just halved 4!", log) 20 | -------------------------------------------------------------------------------- /tests/test_numerals.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from oslash.util.numerals import * # noqa 4 | 5 | 6 | class TestNumerals(unittest.TestCase): 7 | def test_iff_true(self): 8 | self.assertEqual( 9 | iff(true, 1, 2), 10 | 1 11 | ) 12 | 13 | def test_iff_false(self): 14 | self.assertEqual( 15 | iff(false, 1, 2), 16 | 2 17 | ) 18 | 19 | def test_printl_zero(self): 20 | self.assertEqual( 21 | printl(zero), 22 | 0 23 | ) 24 | 25 | def test_printl_one(self): 26 | self.assertEqual( 27 | printl(one), 28 | 1 29 | ) 30 | 31 | def test_succ_zero(self): 32 | self.assertEqual( 33 | printl(succ(zero)), 34 | 1 35 | ) 36 | -------------------------------------------------------------------------------- /tests/test_observable.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from oslash.observable import Observable 4 | from oslash.util import identity, compose 5 | 6 | # pure = Cont.pure 7 | unit = Observable.unit 8 | just = Observable.just 9 | call_cc = Observable.call_cc 10 | 11 | 12 | class TestObservable(unittest.TestCase): 13 | def test_observable_just(self): 14 | stream = Observable.just(42) 15 | 16 | def on_next(x): 17 | return "OnNext(%s)" % x 18 | 19 | self.assertEqual("OnNext(42)", stream.subscribe(on_next)) 20 | 21 | def test_observable_just_map(self): 22 | stream = Observable.just(42).map(lambda x: x*10) 23 | 24 | def on_next(x): 25 | return "OnNext(%s)" % x 26 | 27 | self.assertEqual("OnNext(420)", stream.subscribe(on_next)) 28 | 29 | def test_observable_just_flatmap(self): 30 | stream = Observable.just(42).flat_map(lambda x: just(x*10)) 31 | 32 | def on_next(x): 33 | return "OnNext(%s)" % x 34 | 35 | self.assertEqual("OnNext(420)", stream.subscribe(on_next)) 36 | 37 | def test_observable_filter(self): 38 | stream = Observable.just(42).filter(lambda x: x < 100) 39 | xs = [] 40 | 41 | def on_next(x): 42 | xs.append("OnNext(%s)" % x) 43 | stream.subscribe(on_next) 44 | self.assertEqual(["OnNext(42)"], xs) 45 | 46 | def test_cont_call_cc(self): 47 | f = lambda x: just(x*3) 48 | g = lambda x: just(x-2) 49 | 50 | def h(x, on_next): 51 | return f(x) if x == 5 else on_next(-1) 52 | 53 | stream = just(5) | ( 54 | lambda x: call_cc(lambda on_next: h(x, on_next))) | ( 55 | lambda y: g(y)) 56 | 57 | on_next = lambda x: "Done: %s" % x 58 | 59 | self.assertEqual( 60 | "Done: 13", 61 | stream.subscribe(on_next) 62 | ) 63 | 64 | 65 | class TestObservableFunctor(unittest.TestCase): 66 | 67 | def test_cont_functor_map(self): 68 | x = unit(42) 69 | f = lambda x: x * 10 70 | 71 | self.assertEqual( 72 | x.map(f), 73 | unit(420) 74 | ) 75 | 76 | def test_cont_functor_law_1(self): 77 | # fmap id = id 78 | x = unit(42) 79 | 80 | self.assertEqual( 81 | x.map(identity), 82 | x 83 | ) 84 | 85 | def test_cont_functor_law2(self): 86 | # fmap (f . g) x = fmap f (fmap g x) 87 | def f(x): 88 | return x+10 89 | 90 | def g(x): 91 | return x*10 92 | 93 | x = unit(42) 94 | 95 | self.assertEqual( 96 | x.map(compose(f, g)), 97 | x.map(g).map(f) 98 | ) 99 | 100 | 101 | class TestObservableMonad(unittest.TestCase): 102 | 103 | def test_cont_monad_bind(self): 104 | m = unit(42) 105 | f = lambda x: unit(x*10) 106 | 107 | self.assertEqual( 108 | m.bind(f), 109 | unit(420) 110 | ) 111 | 112 | def test_cont_monad_law_left_identity(self): 113 | # return x >>= f is the same thing as f x 114 | 115 | f = lambda x: unit(x+100000) 116 | x = 3 117 | 118 | self.assertEqual( 119 | unit(x).bind(f), 120 | f(x) 121 | ) 122 | 123 | def test_cont_monad_law_right_identity(self): 124 | # m >>= return is no different than just m. 125 | 126 | m = unit("move on up") 127 | 128 | self.assertEqual( 129 | m.bind(unit), 130 | m 131 | ) 132 | 133 | def test_cont_monad_law_associativity(self): 134 | # (m >>= f) >>= g is just like doing m >>= (\x -> f x >>= g) 135 | m = unit(42) 136 | f = lambda x: unit(x+1000) 137 | g = lambda y: unit(y*42) 138 | 139 | self.assertEqual( 140 | m.bind(f).bind(g), 141 | m.bind(lambda x: f(x).bind(g)) 142 | ) 143 | -------------------------------------------------------------------------------- /tests/test_reader.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from oslash import Reader 4 | from oslash.reader import MonadReader 5 | from oslash.util import identity, compose, fmap 6 | 7 | pure = Reader.pure 8 | unit = Reader.unit 9 | asks = MonadReader.asks 10 | 11 | 12 | env = 42 13 | 14 | class TestReader(unittest.TestCase): 15 | 16 | def test_reader_run(self) -> None: 17 | r = Reader(lambda name: "Hello, %s!" % name) 18 | greeting = r.run("adit") 19 | self.assertEqual(greeting, "Hello, adit!") 20 | 21 | def test_reader_asks(self) -> None: 22 | a = asks(len).run("Banana") 23 | self.assertEqual(6, a) 24 | 25 | 26 | class TestReaderFunctor(unittest.TestCase): 27 | 28 | def test_reader_functor_map(self) -> None: 29 | x = unit(42) 30 | f = lambda x: x * 10 31 | 32 | self.assertEqual( 33 | x.map(f).run(env), 34 | unit(420).run(env) 35 | ) 36 | 37 | def test_reader_functor_law_1(self) -> None: 38 | # fmap id = id 39 | x = unit(42) 40 | 41 | self.assertEqual( 42 | x.map(identity).run(env), 43 | x.run(env) 44 | ) 45 | 46 | def test_reader_functor_law2(self) -> None: 47 | # fmap (f . g) x = fmap f (fmap g x) 48 | def f(x): 49 | return x+10 50 | 51 | def g(x): 52 | return x*10 53 | 54 | x = unit(42) 55 | 56 | self.assertEqual( 57 | x.map(compose(f, g)).run(env), 58 | x.map(g).map(f).run(env) 59 | ) 60 | 61 | 62 | class TestReaderApplicative(unittest.TestCase): 63 | 64 | def test_reader_applicative_law_functor(self) -> None: 65 | # pure f <*> x = fmap f x 66 | x = unit(42) 67 | f = lambda e: e * 42 68 | 69 | self.assertEqual( 70 | pure(f).apply(x).run(env), 71 | x.map(f).run(env) 72 | ) 73 | 74 | def test_reader_applicative_law_identity(self) -> None: 75 | # pure id <*> v = v 76 | v = unit(42) 77 | 78 | self.assertEqual( 79 | pure(identity).apply(v).run(env), 80 | v.run(env) 81 | ) 82 | 83 | def test_reader_applicative_law_composition(self) -> None: 84 | # pure (.) <*> u <*> v <*> w = u <*> (v <*> w) 85 | 86 | w = unit(42) 87 | u = pure(lambda x: x * 42) 88 | v = pure(lambda x: x + 42) 89 | 90 | self.assertEqual( 91 | pure(fmap).apply(u).apply(v).apply(w).run(env), 92 | u.apply(v.apply(w)).run(env) 93 | ) 94 | 95 | def test_reader_applicative_law_homomorphism(self) -> None: 96 | # pure f <*> pure x = pure (f x) 97 | x = 42 98 | f = lambda x: x * 42 99 | 100 | self.assertEqual( 101 | pure(f).apply(unit(x)).run(env), 102 | unit(f(x)).run(env) 103 | ) 104 | 105 | def test_reader_applicative_law_interchange(self) -> None: 106 | # u <*> pure y = pure ($ y) <*> u 107 | 108 | y = 43 109 | u = pure(lambda x: x*42) 110 | 111 | self.assertEqual( 112 | u.apply(unit(y)).run(env), 113 | pure(lambda f: f(y)).apply(u).run(env) 114 | ) 115 | 116 | 117 | class TestReaderMonad(unittest.TestCase): 118 | 119 | def test_reader_monad_bind(self) -> None: 120 | m = unit(42) 121 | f = lambda x: unit(x*10) 122 | 123 | self.assertEqual( 124 | m.bind(f).run(env), 125 | unit(420).run(env) 126 | ) 127 | 128 | def test_reader_monad_law_left_identity(self) -> None: 129 | # return x >>= f is the same thing as f x 130 | 131 | f = lambda x: unit(x+100000) 132 | x = 3 133 | 134 | self.assertEqual( 135 | unit(x).bind(f).run(env), 136 | f(x).run(env) 137 | ) 138 | 139 | def test_reader_monad_law_right_identity(self) -> None: 140 | # m >>= return is no different than just m. 141 | 142 | m = unit("move on up") 143 | 144 | self.assertEqual( 145 | m.bind(unit).run(env), 146 | m.run(env) 147 | ) 148 | 149 | def test_reader_monad_law_associativity(self) -> None: 150 | # (m >>= f) >>= g is just like doing m >>= (\x -> f x >>= g) 151 | m = unit(42) 152 | f = lambda x: unit(x+1000) 153 | g = lambda y: unit(y*42) 154 | 155 | self.assertEqual( 156 | m.bind(f).bind(g).run(env), 157 | m.bind(lambda x: f(x).bind(g)).run(env) 158 | ) 159 | -------------------------------------------------------------------------------- /tests/test_state.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from typing import Callable 4 | from oslash import State 5 | from oslash.util import identity, compose 6 | 7 | # pure = State.pure 8 | unit = State.unit 9 | put = State.put 10 | get = State.get 11 | 12 | state = 42 13 | 14 | class TestState(unittest.TestCase): 15 | 16 | def test_state_greeter(self) -> None: 17 | 18 | def greeter() -> State: 19 | state = get().bind(lambda name: 20 | put("tintin").bind( 21 | lambda _: unit("hello, %s!" % name))) 22 | return state 23 | 24 | result = greeter().run("adit") 25 | self.assertEqual("hello, adit!", result[0]) 26 | self.assertEqual("tintin", result[1]) 27 | 28 | 29 | class TestStateFunctor(unittest.TestCase): 30 | 31 | def test_state_functor_map(self) -> None: 32 | x : State[int, int] = unit(42) 33 | f = lambda x: x * 10 34 | 35 | self.assertEqual( 36 | x.map(f).run(state), 37 | unit(420).run(state) 38 | ) 39 | 40 | def test_state_functor_law_1(self) -> None: 41 | # fmap id = id 42 | x = unit(42) 43 | 44 | self.assertEqual( 45 | x.map(identity).run(state), 46 | x.run(state) 47 | ) 48 | 49 | def test_state_functor_law2(self) -> None: 50 | # fmap (f . g) x = fmap f (fmap g x) 51 | def f(x): 52 | return x + 10 53 | 54 | def g(x): 55 | return x * 10 56 | 57 | x : State[int, int] = unit(42) 58 | 59 | self.assertEqual( 60 | x.map(compose(f, g)).run(state), 61 | x.map(g).map(f).run(state) 62 | ) 63 | 64 | 65 | class TestStateMonad(unittest.TestCase): 66 | 67 | def test_state_monad_bind(self) -> None: 68 | m : State[int, int] = unit(42) 69 | f : Callable[[int], State[int, int]]= lambda x: unit(x * 10) 70 | 71 | self.assertEqual( 72 | m.bind(f).run(state), 73 | unit(420).run(state) 74 | ) 75 | 76 | def test_state_monad_law_left_identity(self) -> None: 77 | # return x >>= f is the same thing as f x 78 | 79 | f = lambda x: unit(x + 100000) 80 | x = 3 81 | 82 | self.assertEqual( 83 | unit(x).bind(f).run(state), 84 | f(x).run(state) 85 | ) 86 | 87 | def test_state_monad_law_right_identity(self) -> None: 88 | # m >>= return is no different than just m. 89 | 90 | m = unit("move on up") 91 | 92 | self.assertEqual( 93 | m.bind(unit).run(state), 94 | m.run(state) 95 | ) 96 | 97 | def test_state_monad_law_associativity(self) -> None: 98 | # (m >>= f) >>= g is just like doing m >>= (\x -> f x >>= g) 99 | m = unit(42) 100 | f = lambda x: unit(x + 1000) 101 | g = lambda y: unit(y * 42) 102 | 103 | self.assertEqual( 104 | m.bind(f).bind(g).run(state), 105 | m.bind(lambda x: f(x).bind(g)).run(state) 106 | ) 107 | -------------------------------------------------------------------------------- /tests/test_util.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from oslash.util import compose, identity 4 | 5 | 6 | class TestCompose(unittest.TestCase): 7 | def test_identity(self): 8 | self.assertEqual(42, identity(42)) 9 | 10 | def test_compose_0(self): 11 | f_id = compose() 12 | self.assertEqual(42, f_id(42)) 13 | 14 | def test_compose_1(self): 15 | f = lambda x: x*42 16 | g = compose(f) 17 | self.assertEqual(420, g(10)) 18 | 19 | def test_compose_2(self): 20 | f = lambda x: x*42 21 | g = lambda y: y+10 22 | h = compose(g, f) 23 | self.assertEqual(430, h(10)) 24 | 25 | def test_compose_3(self): 26 | f = lambda x: x*42 27 | g = lambda y: y+10 28 | h = lambda z: z/2 29 | i = compose(h, g, f) 30 | self.assertEqual(215, i(10)) 31 | 32 | def test_compose_composition(self): 33 | u = lambda x: x * 42 34 | v = lambda x: x + 42 35 | w = 42 36 | 37 | a = compose(u, v)(w) 38 | b = u(v(w)) 39 | self.assertEqual(a, b) 40 | -------------------------------------------------------------------------------- /tests/test_writer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from oslash import Writer, StringWriter 4 | 5 | 6 | class TestWriterMonad(unittest.TestCase): 7 | def test_writer_return(self): 8 | w = StringWriter.unit(42) 9 | self.assertEqual(w.run(), (42, "")) 10 | 11 | def test_writer_unitint_log(self): 12 | IntWriter = Writer.create("IntWriter", int) 13 | w = IntWriter.unit(42) 14 | self.assertEqual(w.run(), (42, 0)) 15 | 16 | def test_writer_monad_bind(self): 17 | entry = "Multiplied by 10" 18 | m = StringWriter.unit(42).bind(lambda x: StringWriter(x*10, entry)) 19 | self.assertEqual(m, StringWriter(420, entry)) 20 | 21 | def test_writer_monad_law_left_identity(self): 22 | # return x >>= f == f x 23 | entry = "Added 100000 to %s" 24 | a = StringWriter.unit(42).bind(lambda x: StringWriter(x+100000, entry % x)) 25 | b = (lambda x: StringWriter(x+100000, entry % x))(42) 26 | self.assertEqual(a, b) 27 | 28 | def test_writer_monad_law_right_identity(self): 29 | # m >>= return is no different than just m. 30 | a = StringWriter.unit(42).bind(StringWriter.unit) 31 | self.assertEqual(a, StringWriter.unit(42)) 32 | 33 | def test_writer_monad_law_associativity(self): 34 | # (m >>= f) >>= g is just like doing m >>= (\x -> f x >>= g) 35 | add1000 = "Added 1000 to %s" 36 | mul100 = "Multiplied %s by 100" 37 | a = StringWriter.unit(42).bind( 38 | lambda x: StringWriter(x+1000, add1000 % x)).bind( 39 | lambda y: StringWriter(y*100, mul100 % y)) 40 | b = StringWriter.unit(42).bind( 41 | lambda x: StringWriter(x+1000, add1000 % x).bind( 42 | lambda y: StringWriter(y*100, mul100 % y) 43 | )) 44 | self.assertEqual(a, b) 45 | --------------------------------------------------------------------------------