Abstract
On this article, I’ll try to do Scala-like error and output handling with Python.This is my personal memo.
Code
Python’s function doesn’t need type information of the arguments to write. So, it is not necessary to make the class for output value and error in neat way. But as an assumption that I use type information on some functions, I’ll write Either class, which is extended by Right and Left.Right will wrap the output of functions when it doesn’t raise any error. Left will wrap the error.
from typing import Any
class Either:
pass
class Right(Either):
def __init__(self, content: Any):
self.content = content
class Left(Either):
def __init__(self, error):
self.error = error
To use these classes, I can define the function which automatically wraps output of the function. This higher order function will unwrap the input if the input is wrapped, execute the function and wrap the output by Right or Left. It assumes that the function which is the first argument will take just one argument.
from typing import Any, Union
from .base import Either, Right, Left
def Try(fn, obj: Union[Any, Either]) -> Either:
if type(obj) == Right:
try:
return Right(fn(obj.content))
except Exception as e:
return Left(e)
elif type(obj) == Left:
return obj
else:
try:
return Right(fn(obj))
except Exception as e:
return Left(e)
I’ll define some functions and check the behavior.
def fun_1(obj):
return obj * 2
def fun_2(obj):
try:
return 10 / obj
except ZeroDivisionError as e:
raise e
def fun_3(obj):
return obj + 1
When the function succeeds to execute, the output is wrapped by Right.
fun_1_output = Try(fun_1, 10)
print(type(fun_1_output))
<class 'toolbox.base.Right'>
The value of the function’s output is stored on the
content
field.print(fun_1_output.content)
20
Because
Try()
unwraps the input if it is wrapped, I can use the output of Try()
as the next Try()
's input.fun_1_2_output = Try(fun_2, Try(fun_1, 10))
I can check the value of the function with the same manner before.
print(fun_1_2_output.content)
0.5
When the function raises the exception, the output of the function is wrapped by Left.
fun_2_output = Try(fun_2, 0)
The type of the
fun_2_outptu
is Left.print(type(fun_2_output))
<class 'toolbox.base.Left'>
Left contains error information.
fun_2_output.error
ZeroDivisionError('division by zero')
Of course I can raise this error.
raise fun_2_output.error
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
<ipython-input-10-91cc3f6e850c> in <module>
----> 1 raise fun_2_output.error
~/Project/personal/toolbox/toolbox/manager.py in Try(fn, obj)
13 else:
14 try:
---> 15 return Right(fn(obj))
16 except Exception as e:
17 return Left(e)
<ipython-input-2-043eb3acd818> in fun_2(obj)
6 return 10 / obj
7 except ZeroDivisionError as e:
----> 8 raise e
9
10 def fun_3(obj):
<ipython-input-2-043eb3acd818> in fun_2(obj)
4 def fun_2(obj):
5 try:
----> 6 return 10 / obj
7 except ZeroDivisionError as e:
8 raise e
ZeroDivisionError: division by zero
When the input type is Left,
Try()
just passes the input value to output value.On the case below,
fun_2
raises exception. The Left instance contains that error information and on the Try(fun_3, ..)
, that is just past to the output.fun_2_3_output = Try(fun_3, Try(fun_2, 0))
If I raise the
fun_2_3_output
's error, the error which happened on the Try(fun_2, 0)
is raised.raise fun_2_3_output.error
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
<ipython-input-12-cf4375ab93a6> in <module>
----> 1 raise fun_2_3_output.error
~/Project/personal/toolbox/toolbox/manager.py in Try(fn, obj)
13 else:
14 try:
---> 15 return Right(fn(obj))
16 except Exception as e:
17 return Left(e)
<ipython-input-2-043eb3acd818> in fun_2(obj)
6 return 10 / obj
7 except ZeroDivisionError as e:
----> 8 raise e
9
10 def fun_3(obj):
<ipython-input-2-043eb3acd818> in fun_2(obj)
4 def fun_2(obj):
5 try:
----> 6 return 10 / obj
7 except ZeroDivisionError as e:
8 raise e
ZeroDivisionError: division by zero
With functional programming language like funcy, it works on the same manner.
On the code below, the three function is sequentially composed to the one. If I use
Try()
to this with the input value 0
, fun_2
raises exception and that is stored and past to the final output.import funcy
composed_functions = funcy.rcompose(fun_1, fun_2, fun_3)
raise Try(composed_functions, 0).error
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
<ipython-input-13-e72ffcadb978> in <module>
3 composed_functions = funcy.rcompose(fun_1, fun_2, fun_3)
4
----> 5 raise Try(composed_functions, 0).error
~/Project/personal/toolbox/toolbox/manager.py in Try(fn, obj)
13 else:
14 try:
---> 15 return Right(fn(obj))
16 except Exception as e:
17 return Left(e)
~/.pyenv/versions/3.6.7/envs/toolbox/lib/python3.6/site-packages/funcy/funcs.py in <lambda>(*a, **kw)
104 """Composes passed functions."""
105 if fs:
--> 106 pair = lambda f, g: lambda *a, **kw: f(g(*a, **kw))
107 return reduce(pair, map(make_func, fs))
108 else:
~/.pyenv/versions/3.6.7/envs/toolbox/lib/python3.6/site-packages/funcy/funcs.py in <lambda>(*a, **kw)
104 """Composes passed functions."""
105 if fs:
--> 106 pair = lambda f, g: lambda *a, **kw: f(g(*a, **kw))
107 return reduce(pair, map(make_func, fs))
108 else:
<ipython-input-2-043eb3acd818> in fun_2(obj)
6 return 10 / obj
7 except ZeroDivisionError as e:
----> 8 raise e
9
10 def fun_3(obj):
<ipython-input-2-043eb3acd818> in fun_2(obj)
4 def fun_2(obj):
5 try:
----> 6 return 10 / obj
7 except ZeroDivisionError as e:
8 raise e
ZeroDivisionError: division by zero