Skip to content

Instantly share code, notes, and snippets.

@bwbg
Last active August 29, 2015 14:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bwbg/c844de3b8a19a75d763f to your computer and use it in GitHub Desktop.
Save bwbg/c844de3b8a19a75d763f to your computer and use it in GitHub Desktop.
A Python-implementation of an error monad.
#!/usr/bin/env python3
# -----------------------------------------------------------------------------
# The MIT License (MIT)
#
# Copyright (c) 2014, Heiko Möllerke
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# -----------------------------------------------------------------------------
__author__ = 'Heiko Möllerke'
__version__ = '0.1.1'
__date__ = '2014-09-26'
import collections
from functools import wraps, partial, reduce
class ErrorM(collections.namedtuple('_ErrorM', ['value'])):
__slots__ = ()
unit = None # Will be patched in later.
def __eq__(self, other):
return type(self) == type(other) and self.value == other.value
def __ne__(self, other):
return not self == other
def __rshift__(self, mf):
"""API-sugar for the bind-functions."""
return self.bind(mf)
class Success(ErrorM):
__slots__ = ()
def bind(self, mf):
return mf(self.value)
class Failure(ErrorM):
__slots__ = ()
def bind(self, mf):
return self
Unit = Success
ErrorM.unit = Unit
def lift(f):
@wraps(f)
def mf(x):
try:
return Success(f(x))
except Exception as err:
return Failure(str(err))
return mf
def compose(f, g): # Utility function
"""Composes two functions `f` and `g` into a new function."""
return lambda *args, **kw: g(f(*args, **kw))
def _mcompose(M, *mfs): # Utility function
"""Composes the given (monadic) functions."""
return compose(M.unit, partial(reduce, M.__rshift__, mfs))
mcompose = partial(_mcompose, ErrorM)
#!/usr/bin/env python3
# -----------------------------------------------------------------------------
# The MIT License (MIT)
#
# Copyright (c) 2014, Heiko Möllerke
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# -----------------------------------------------------------------------------
import unittest
from error import Unit, Success, Failure, lift, mcompose
from operator import add, sub, mul, truediv
from functools import partial
# Lift some regular functions.
madd_four = lift(partial(add, 4))
msub_five_by = lift(partial(sub, 5))
mmul_three = lift(partial(mul, 3))
mdiv_ten_by = lift(partial(truediv, 10))
class TestErrorMonadLaws(unittest.TestCase):
def test_mcompose(self):
assert Unit(2) >> mcompose(madd_four, mmul_three, madd_four) == Unit(22)
def test_lift(self):
assert Unit(3) >> madd_four == Unit(7)
assert Unit(2) >> msub_five_by == Unit(3)
assert Unit(0) >> mdiv_ten_by == Failure('division by zero')
def test_left_identity_success(self):
x = 5
m = Unit(x)
f = madd_four
assert m >> f == f(x)
def test_right_identity_success(self):
m = Success(-2)
assert m >> Unit == m
def test_associativity_success(self):
m = Unit(42)
f = madd_four; g = mmul_three
assert (m >> f) >> g == m >> (lambda x: f(x) >> g)
def test_left_identity_fail(self):
m = Failure('Something went wrong!')
f = madd_four
assert m >> f == m # f(x) won't match as it won't fail.
def test_right_identity_fail(self):
m = Failure('Something went wrong!')
assert m >> Unit == m
def test_associativity_fail(self):
m = Failure('Something went wrong!')
f = madd_four; g = mmul_three
assert (m >> f) >> g == m >> (lambda x: f(x) >> g)
if __name__ == '__main__':
unittest.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment