一句话介绍,monad是封装一些控制流(例如try / catch)的数据类型(例如int)。
以我在软件行业的工作经验,成功完成某件事并且仅完成某件事而不是失败,是相当困难的。 随着代码库的扩展,当代码库较小时,并不复杂的事情(例如错误模型的设计和管理)变得非常困难。例如,在工作中编写代码时,可能会遇到以下三件事:
- 条件语句(如if / else)
- 错误处理
- 返回不同类型的值
这些都是很常见的代码,但是在适当的上下文中,可能需要进行大量推理以确保所有情况的正确性。
现在,我们在python3.6里,实现除法:
def div(a, b):
return a / b
大部分情况下,这些代码能正常执行:
>>> def div(a, b):
... return a / b
...
>>> div(4, 2)
2.0
>>> type(div(4, 2))
<type 'float'>
>>>
如果输入b=0,会出现如下异常:
>>> def div(a, b):
... return a / b
...
>>> div(5, 0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in div
ZeroDivisionError: division by zero
>>>
上述代码作为自动化服务的一部分存在,执行失败,可能会导致服务的其余部分开始失败,并迫使所有受影响的开发人员梳理日志,从而找到根本原因。
此外,您可能并不总是拥有其他一些可以处理传播的错误的父服务,或者您正在构建处理来自其他服务的传播的错误的父服务,因此您无法承受“快速失败”的麻烦。
也许您当前正在处理一些数据块,并且如果未能处理该数据块的一个子集,则必须丢弃整个数据块以及处理该数据块其余部分所花费的时间。
因此,让我们添加优雅处理异常的功能,以免终止程序执行。 但是,添加异常处理逻辑会很快变得混乱。
def div(a, b):
try:
return a / b
except ZeroDivisionError:
pass # WHAT TO DO HERE??
# General catch-all for unhandled
# edge cases
except Exception as err:
raise err
您可以在该方法中引发任意数量的异常类型(例如,客户端进程中随时可能引发的异常是KeyboardInterrupt)。 这将导致许多不同的except子句,这些子句将根据您要捕获的异常类型的数量来扩展try / catch块)。
其次,如果您将异常处理作为任何主线程序执行的一部分来应用,则您将失去从处理逻辑及之前的操作中准确跟踪任何异常的能力。
所以你会怎么做? 您无法返回整数,这不仅是因为数学上没有定义且不可能,而且没有合理的整数默认值,因为数字行上的任何整数可能是此方法的两个输入的另一个有效结果。 如果b为1,div(a,1)将返回a,其中a是所有整数的集合。
也许您可以尝试返回其他类型的值(例如NoneType):
def div(a, b):
if b == 0:
return None
return a / b
看起来,异常被很好解决了。是这样吗?
>>> def div(a, b):
... if b == 0:
... return None
... return a / b
...
>>> div(5, 0)
>>> type(div(5, 0))
<type 'NoneType'>
不是的。 类型NoneType不具有与类型int相同的属性,如果您使用鸭子类型输入,可能会导致错误,因为这会像Python这样的动态类型化语言允许并鼓励:
>>> None + 1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
>>>
所以,如果你在另一个程序中使用这个定义,你就有可能在下游导致级联失败。假设您正在对一些任意数据执行基本的map/reduce操作:
>>> mylist = [0, 1, 2, 3]
>>> def mapper(n):
... return div(5, n)
...
>>> map(mapper, mylist)
[None, 5, 2, 1]
>>> def reducer(a, b):
... return a + b
...
>>> reduce(reducer, map(mapper, mylist))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in reducer
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
>>>
请注意,堆栈跟踪与map / reduce操作绝对无关,后者已正确实现,但与div()方法的基础实现有关。 这不仅阻止了对编写的源代码的有效流水线,而且还引入了另一层混淆,这增加了调试开销。
您可以尝试用具有某些类型int属性(例如add)的超类实例替换类型int吗? 不幸的是没有; 由于int类型直接继承自object类型,因此没有真正可以替代的具体超类:
# '__mro__' stands for "Method Resolution
# Order". This resolves issues with multiple
# inheritance like "the diamond problem",
# where method resolution may be ambiguous:
# https://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem
#
# Guido van Rossum wrote more about method
# resolution order here:
# https://python-history.blogspot.com/2010/06/method-resolution-order.html
>>> int.__mro__
(<class 'int'>, <class 'object'>)
>>> type(None).__mro__
(<class 'NoneType'>, <class 'object'>)
>>>
简单的事情应该保持简单,我们可以清楚地看到,为问题添加条件和特殊处理的简单途径会使事情变得复杂。 也许还有另一种方式。 这就是我学习monad。
理解monad的好处的关键是意识到表达式本身不会引起异常,对表达式的执行会导致异常。 如果您可以将表达式解析为以后可以求值的类型,则可以将异常作为类型定义的一部分进行处理,这可以为静态分析工具或编译时提供处理接口。
在这一点上,我想介绍三种不同的“事物”:Maybe,Just和Nothing。 它们是monad,它们可以帮助我们实现更安全的除法版本,而不会遭受上述替代方法带来的许多负面影响。
class _Maybe(object):
"""An implementation of the 'Maybe' monad.
This class definition exists to add any monadic attributes or operators.
Since we're talking about 'Maybe', 'Nothing', and 'Just' only, and since
'Nothing' and 'Just' inherit from 'Maybe', we'll wrap any basic monadic
attributes as part of this class definition.
"""
def Maybe(cls):
"""Parametrization of a typedef to include monadic attributes. This method
enables dynamic classdef generation by inheriting a base class passed in as
an input argument and overriding class attribute '__bases__'.
This is a psuedo-definition of generics, which may assist understanding of
monads between a statically typed language like Haskell and a dynamically
typed language like Python.
See the Python documentation for more information on method 'type()' and
attribute '__bases__':
https://docs.python.org/3/library/functions.html#type
https://docs.python.org/3/library/stdtypes.html#class.__bases__
"""
# NOTE: Concrete implementation of Maybe() should be left to concrete base
# types.
raise NotImplementedError
class _Just(_Maybe):
"""A successful evaluation of a Maybe monad.
"""
def Just(cls):
"""Functional instantiation of `Just`.
"""
return type('Just(%s)' % cls.__name__, (_Just, cls), {})
class _Nothing(_Maybe):
"""A failed evaluation of a Maybe monad.
"""
# Ensure that `Nothing` is unique; uniqueness implies same `id()` result, which
# implies a global object (singleton), much like `None = type(None)()` is a
# singleton.
#
# >>> id(type(None)())
# 4562745448 # Some object ID, this may vary.
# >>> id(None)
# 4562745448 # Same object ID as before.
# >>>
Nothing = _Nothing()
这样int类型是这样了:
>>> Just(int)(1)
1
>>> type(Just(int)(1))
<class '__main__.Just(int)'>
应用多重继承以将效果渲染到参数化的基本类型上。 通过使用函数type()和扩展了基础对象属性bases的扩展构造函数方法,此功能可以进一步发挥作用。 这满足了Maybe monad的一个属性,因为它们可以将纯效果(例如数学表达式)转换为不纯效果(例如用类型替换try / catch语句)。
因此,除法可以这样实现:
def safediv(a : Maybe(int), b : Maybe(int)) -> Maybe(float):
if (
a is Nothing or
b is Nothing or
b == 0
):
return Nothing
return Just(float)(a / b)
在此上下文中应用的monad的类型参数化方面对控制流的一部分应如何失败产生了限制:它仅返回Nothing。 因为它是代表错误的类型,并且因为用Nothing调用monadic方法不会产生任何信息(强制幂等),所以运行时不必担心必须引发异常来避免传播以后无法处理的错误。 只要类型匹配,就可以安全运行。 错误对于流水线来说是安全的,流水线化允许开发具有任意复杂度的系统。
现在,我们来实现monad类
# Removing docstrings and comments for sake of brevity, entire script is
# referenced at the end of this post.
class _Maybe(object):
def __init__(self, data=None):
self.data = data
raise NotImplementedError
def bind(self, function):
raise NotImplementedError
def __rshift__(self, function):
if not isinstance(function, _Maybe):
error_message = 'Can only pipeline monads.'
raise TypeError(error_message)
if not callable(function):
function = lambda _ : function
return self.bind(function)
# ...
class _Just(_Maybe):
def __init__(self, data=None):
self.data = data
def bind(self, function):
return function(self)
# ...
class _Nothing(_Maybe):
"""A failed evaluation of a Maybe monad.
"""
def __init__(self, _=None):
def __str__(self):
return "Nothing"
def bind(self, _):
return self
可以看到,基于monad类定义,方法绑定的实现是不同的。Nothing总是绑定到Nothing,Just绑定到函数中应用的自身。因此,管道中的状态以Just发生变化,并保持为Nothing的幂等。
接下来,我们可以为division方法实现一种形式的柯里化,这样我们就可以将monadic方法调用与monadic数据以相同的方式进行流水线处理:
def curry(method):
num_args = len(inspect.signature(method).parameters)
def build_reader(argument_values, num_args):
if num_args == 0:
return method(*argument_values)
else:
return lambda x: build_reader(argument_values + [x], num_args - 1)
# NOTE: This Reader class is important to understand when implementing the
# full solution. For now, think of it as a helper class to implementing
# curry using our existing monad class definitions. It's available in the
# full script available at the end of this post.
return Reader(build_reader([], num_args))
这样就可以用装饰器,柯里化函数:
@curry
def add(a : Maybe(int), b : Maybe(int)) -> Maybe(int):
if (
a is Nothing or
b is Nothing
):
return Nothing
return Just(int)(a.data + b.data)
@curry
def div(denominator : Maybe(int), numerator : Maybe(int)) -> Maybe(float):
if (
numerator is Nothing or
denominator is Nothing or
denominator.data == 0
):
return Nothing
return Just(float)(numerator.data / denominator.data)
最终,就可以这样调用了
>>> from monads import *
# Basic addition
>>> Just(int)(7) >> add(Just(int)(8))
15
# Basic division
>>> Just(int)(5) >> div(Just(int)(10))
0.5
# Division by zero, resulting in `Nothing`
>>> Just(int)(7) >> add(Just(int)(8)) >> div(Just(int)(0))
<monads._Nothing object at 0x1040b0b90>
# Add after division by zero, still returns `Nothing`
>>> Just(int)(7) >> add(Just(int)(8)) >> div(Just(int)(0)) >> add(Just(int)(15))
<monads._Nothing object at 0x1040b0b90>
网友评论