What the #@(% are Monads? (a beginner's guide)
What the #@(% are Monads? (a beginner’s guide)
到底什么是 Monad?(初学者指南)
A monad is just a monoid in the category of endofunctors. Oh… you aren’t fluent in the arcane language of Category Theory? Hm… I suppose an example will have to do. Monad 不过就是自函子范畴上的一个幺半群。噢……你不精通范畴论那种晦涩难懂的语言?嗯……看来我得举个例子才行。
Alright. Let’s say we’re programming a small class to manage an Inventory in a game. We want to add, remove, and display the items in the inventory. Let’s go ahead and write out some simple code: 好吧。假设我们正在编写一个小型类来管理游戏中的物品栏(Inventory)。我们想要实现添加、移除和显示物品的功能。让我们先写一些简单的代码:
class Inventory:
def __init__(self):
self.items = []
def add(self, item):
self.items.append(item)
return self
def remove(self, item):
self.items.remove(item)
return self
def display(self):
print("Inventory: ", self.items)
We can use the Inventory class with ease: 我们可以轻松地使用这个 Inventory 类:
Inventory().add("Sword").add("Potion").add("Shield").remove("Potion").display()
# Output: Inventory: ['Sword', 'Shield']
So far, so good. One more thing though; the player can only hold a limited amount of items. We just need to change the __init__ and add functions:
目前为止一切顺利。但还有一件事:玩家只能携带有限数量的物品。我们只需要修改 __init__ 和 add 函数:
class Inventory:
def __init__(self, capacity):
self.items = []
self.capacity = capacity
def add(self, item):
if len(self.items) >= self.capacity:
# inventory is over capacity!
raise Exception("Out of Inventory Space")
else:
self.items.append(item)
return self
# implementation of remove() and display() omitted; same as above
Ok… we’re done. So, to recap. We made an Inventory class, where we can add, remove, and display the state of the list. It has a max capacity, and throws an Exception when we surpass inventory capacity; we can use try/except to catch and display them neatly instead of letting them crash the program. 好了……完成了。总结一下:我们创建了一个 Inventory 类,可以添加、移除并显示列表状态。它有最大容量限制,当超过容量时会抛出异常;我们可以使用 try/except 来捕获并优雅地显示错误,而不是让程序崩溃。
Alright, let’s just test it real quick: 好吧,让我们快速测试一下:
try:
Inventory(capacity=10).add("Sword").add("Potion").remove("sword").display()
except Exception:
print("Inventory out of space: Try removing some items first.")
And, just as expected, we see Oh… that’s not right… 正如预期的那样,我们看到了……噢,不对劲……
Inventory out of space: Try removing some items first.
We’re supposed to have plenty of space. OHHH. Wait… Calling Inventory.remove(item) can also result in an Exception, when we pass in an item that doesn’t exist in the list we’re removing from.
我们明明应该有足够的空间。噢……等等……调用 Inventory.remove(item) 也会导致异常,当我们传入一个列表中不存在的物品时。
I mean… I guess we could surround every single use of Inventory with try/catch and use an if-statement to check the Exception but… it’s not really elegant anymore. The first version was so simple to use; data simply flowed through the operations and there wasn’t any complexity. Maybe there’s a better way to do all this. It’s time to call upon one of the dark arts of functional programming. 我的意思是……我想我们可以用 try/catch 包裹每一次 Inventory 的调用,并用 if 语句检查异常,但这……已经不再优雅了。第一个版本使用起来非常简单;数据在操作中顺畅流动,没有任何复杂性。也许有更好的方法来处理这一切。是时候召唤函数式编程的“黑魔法”了。
Alright. The problem is that our functions don’t fully behave predictably. They might throw an error, or they might return normally. So, to make them behave predictably, what if we made them always return, even if an error happened? 好吧。问题在于我们的函数行为不够完全可预测。它们可能会抛出错误,也可能正常返回。那么,为了让它们行为可预测,如果我们让它们始终返回结果,即使发生了错误呢?
We could technically return an Exception object in Python, but that doesn’t really solve our problem. What we want is a clear way to represent success or failure as part of the function’s normal output. We could do so with something like this: 从技术上讲,我们可以在 Python 中返回一个 Exception 对象,但这并不能真正解决问题。我们想要的是一种清晰的方式,将成功或失败作为函数正常输出的一部分。我们可以这样做:
class Result:
def __init__(self, value, is_error):
self.is_error = is_error
if is_error:
self.error = value
else:
self.ok = value
# helper functions for easily constructing Ok/Error
def Ok(value): return Result(value, False)
def Error(value): return Result(value, True)
Now, when something returns a Result, we can check is_error to determine whether it succeeded or failed. Alright. Let’s modify our Inventory code to return Results instead of throwing errors.
现在,当某个函数返回 Result 时,我们可以检查 is_error 来确定它是成功还是失败。好吧,让我们修改 Inventory 代码,使其返回 Result 而不是抛出错误。
class Inventory:
def __init__(self, capacity):
self.items = []
self.capacity = capacity
def add(self, item):
if len(self.items) >= self.capacity:
return Error("OutOfSpace")
else:
self.items.append(item)
return Ok(self)
def remove(self, item):
if item not in self.items:
return Error("ItemNotFound")
else:
self.items.remove(item)
return Ok(self)
def display(self):
print("Inventory: ", self.items)
return Ok(self)
Alright, but how does this even solve anything? I mean you still need to unwrap the Result every single time: 好吧,但这到底解决了什么问题?我的意思是,你仍然需要每次都去解包(unwrap)这个 Result:
inventory_v0 = Inventory(3)
inventory_v1 = inventory_v0.add("Sword")
if inventory_v1.is_error:
print(inventory_v1.error)
else:
inventory_v2 = inventory_v1.ok.remove("Potion")
if inventory_v2.is_error:
print(inventory_v2.error)
else:
# and so on
What progress have we even made? Now it’s worse off than before… No. Stay steadfast. We’re right at the boundary and we cannot turn back. We must make the final, daring leap. 我们到底取得了什么进展?现在情况比以前更糟了……不,要坚定。我们已经站在了边界上,不能回头。我们必须完成最后那大胆的一跃。
The problem isn’t the Result, it’s that we have to keep unwrapping it. Programmers are lazy; how can we be expected to do such a boring chore again and again? It’s madness! So, we delegate the work to a new method, called and_then:
问题不在于 Result,而在于我们必须不断地解包它。程序员是懒惰的;怎么能指望我们一遍又一遍地做这种无聊的苦差事呢?这简直是疯了!所以,我们将这项工作委托给一个名为 and_then 的新方法:
class Result:
def __init__(self, value, is_error):
self.is_error = is_error
if is_error:
self.error = value
else:
self.ok = value
def and_then(self, f):
if self.is_error:
return self
else:
return f(self.ok)
So, and_then takes a function. If the result is an error, it propagates that error down the chain. Otherwise, it applies the function to the value inside. Interestingly enough, nothing in and_then specifically knows about errors. It only knows how to inspect the Result and decide what to do based on which variant it contains.
所以,and_then 接收一个函数。如果结果是错误,它会将该错误沿链向下传播。否则,它会将函数应用于内部的值。有趣的是,and_then 内部并不专门了解错误。它只知道如何检查 Result,并根据它包含的变体决定做什么。
To use it, all we need to do is convert Inventory’s methods to functions: 要使用它,我们只需要将 Inventory 的方法转换为函数:
# (Inventory methods converted to standalone functions)
# (Inventory 方法已转换为独立函数)
And done! Alright. Now everything is put into place. Using Inventory now may look a bit strange at first, but it’s quite elegant: 完成了!好吧。现在一切都各就各位了。现在使用 Inventory 起初看起来可能有点奇怪,但它非常优雅:
inventory = Ok(Inventory(capacity=3)) \
.and_then(lambda x: add(x, "Sword")) \
.and_then(lambda x: add(x, "Potion")) \
.and_then(display)
if inventory.is_error:
# handle error gracefully
Why the lambdas? Well, add takes two arguments: an Inventory and an item… 为什么要用 lambda?嗯,add 接收两个参数:一个 Inventory 和一个物品……