Python是“与”monadic?

时间:2011-08-20 09:53:31

标签: python functional-programming monads

像许多在我之前的蛮干先锋一样,我正在努力穿越理解莫纳德的无轨荒地。

我仍然蹒跚而行,但我不禁注意到Python with语句中某种类似monad的质量。考虑一下这个片段:

with open(input_filename, 'r') as f:
   for line in f:
       process(line)

open()调用视为“单元”,将块本身视为“绑定”。实际的monad没有暴露(呃,除非f是monad),但模式就在那里。不是吗?或者我只是误将所有FP误认为是monadry?或者它只是凌晨3点,似乎有什么可信的?

一个相关问题:如果我们有单子,我们是否需要例外?

在上面的片段中,可以从代码中隐藏I / O中的任何故障。磁盘损坏,缺少指定文件和空文件都可以被视为相同。所以不需要可见的IO异常。

当然,Scala的Option类型类已经消除了可怕的Null Pointer Exception。如果您将数字重新编号为Monads(NaNDivideByZero作为特殊情况)...

就像我说的那样,早上3点。

4 个答案:

答案 0 :(得分:23)

这几乎是微不足道的,但第一个问题是with不是一个函数,并没有将函数作为参数。您可以通过编写with的函数包装器来轻松解决这个问题:

def withf(context, f):
    with context as x:
        f(x)

由于这是微不足道的,因此您无法区分withfwith

with作为monad的第二个问题是,作为语句而不是表达式,它没有值。如果你可以给它一个类型,它将是M a -> (a -> None) -> None(这实际上是上面withf的类型)。实际上,您可以使用Python的_来获取with语句的值。在Python 3.1中:

class DoNothing (object):
    def __init__(self, other):
        self.other = other
    def __enter__(self):
        print("enter")
        return self.other
    def __exit__(self, type, value, traceback):
        print("exit %s %s" % (type, value))

with DoNothing([1,2,3]) as l:
    len(l)

print(_ + 1)

由于withf使用函数而不是代码块,_的替代方法是返回函数的值:

def withf(context, f):
    with context as x:
        return f(x)

还有另一件事阻止with(和withf)成为monadic绑定。块的值必须是monadic类型,其类型构造函数与with项相同。实际上,with更通用。考虑到agf的注意,每个接口都是一个类型构造函数,我将with的类型与M a -> (a -> b) -> b挂钩,其中M是上下文管理器接口(__enter____exit__方法) 。 bindwith类型之间的类型为M a -> (a -> N b) -> N b。要成为monad,withb不是M a时必须在运行时失败。此外,虽然您可以单独使用with作为绑定操作,但这样做很少有意义。

你需要做出这些微妙区别的原因是,如果你错误地认为with是monadic,你最终会误用它并编写因类型错误而失败的程序。换句话说,你会写垃圾。你需要做的是区分一个特定事物的构造(例如monad)和一个可以以那个东西的方式使用的构造(例如,monad)。后者需要程序员的纪律,或者强制执行纪律的其他结构的定义。这是with的近似monadic版本(类型为M a -> (a -> b) -> M b):

def withm(context, f):
    with context as x:
        return type(context)(f(x))

在最后的分析中,你可以认为with就像一个组合子,但比monad所需的组合子(它是绑定的)更通用。使用monad的函数可能多于两个所需的函数(例如,list monad也有cons,append和length),所以如果为上下文管理器定义了适当的bind运算符(例如withm),那么{{在涉及monad的意义上,1}}可能是monadic。

答案 1 :(得分:10)

正下方定义Wikipedia says

  

在面向对象的编程术语中,类型构造将对应于monadic类型的声明,单元函数扮演构造函数方法的角色,绑定操作包含执行其注册的回调所需的逻辑(monadic)函数)。

这听起来与上下文管理器协议,对象的上下文管理器协议的实现以及with语句完全相同。

来自@Owen对此帖的评论:

  

Monads,在最基本的层面上,或多或少是一种使用延续传递方式的很酷的方式:>> =采用“制作人”和“回调”;这基本上也是这样的:像open(...)这样的生产者和一旦创建它就被调用的代码块。

完整的维基百科定义:

  

一种类型构造,为每种基础类型定义如何获得相应的monadic类型。在Haskell的表示法中,monad的名称表示类型构造函数。如果M是monad的名称而t是数据类型,则“M t”是monad中的对应类型。

这对我来说就像context manager protocol

  

一个单元函数,它将基础类型中的值映射到相应monadic类型中的值。结果是相应类型中的“最简单”值,其完全保留原始值(简单性被适当地理解为monad)。在Haskell中,由于在后面描述的标记中使用它的方式,该函数被称为返回。单位函数具有多态类型t→M t。

对象实际执行上下文管理器协议。

  

多态类型(M t)→(t→M u)→(M u)的绑定操作,其中Haskell由中缀运算符表示> =。它的第一个参数是monadic类型的值,它的第二个参数是一个函数,它从第一个参数的基础类型映射到另一个monadic类型,其结果是在其他monadic类型中。

这对应于the with statement及其套件。

所以是的,我会说with是一个单子。我搜索了PEP 343以及所有相关的拒绝和撤回的PEP,并且没有人提到“monad”这个词。它当然适用,但似乎with语句的目标似乎是资源管理,monad只是获取它的有用方法。

答案 2 :(得分:8)

Haskell对于文件具有等效的with,它被称为withFile。这样:

with open("file1", "w") as f:
    with open("file2", "r") as g:
        k = g.readline()
        f.write(k)

相当于:

withFile "file1" WriteMode $ \f ->
  withFile "file2" ReadMode $ \g ->
    do k <- hGetLine g
       hPutStr f k

现在,withFile可能看起来像monadic。它的类型是:

withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r

右侧看起来像(a -> m b) -> m b

另一个相似之处:在Python中,您可以跳过as,在Haskell中,您可以使用>>代替>>=(或do块而不使用<- }箭头)。

所以我会回答这个问题:是withFile monadic?

您可以认为它可以这样写:

do f <- withFile "file1" WriteMode
   g <- withFile "file2" ReadMode
   k <- hGetLine g
   hPutStr f k

但这不是类型检查。它不能。

这是因为在Haskell中 IO monad是顺序的:如果你写

do x <- a
   y <- b
   c

执行a后,执行b,然后执行c。没有“回溯” 最后清理a或类似的事情。另一方面,withFile 执行块后必须关闭句柄。

还有另一个名为continuation monad的monad,允许这样做 的东西。但是,你现在有两个monad,IO和continuation,并且同时使用两个monad的效果需要使用monad变换器。

import System.IO
import Control.Monad.Cont

k :: ContT r IO ()
k = do f <- ContT $ withFile "file1" WriteMode 
       g <- ContT $ withFile "file2" ReadMode 
       lift $ hGetLine g >>= hPutStr f

main = runContT k return
那是丑陋的。所以答案是:有点,但这需要处理许多细微之处,使问题变得相当不透明。

Python的with只能模拟monads可以做的有限的一点 - 添加输入和完成代码。我认为你不能模拟,例如。

do x <- [2,3,4]
   y <- [0,1]
   return (x+y)

使用with(可能有一些肮脏的黑客攻击)。相反,用于:

for x in [2,3,4]:
    for y in [0,1]:
        print x+y

还有一个Haskell函数 - forM

forM [2,3,4] $ \x ->
  forM [0,1] $ \y ->
    print (x+y)

我建议阅读yieldwith相比更多与monads相似的内容: http://www.valuedlessons.com/2008/01/monads-in-python-with-nice-syntax.html

  

一个相关问题:如果我们有单子,我们是否需要例外?

基本上没有,而不是抛出A或返回B的函数,你可以创建一个返回Either A B的函数。 Either A的monad将表现得像异常一样 - 如果一行代码将返回错误,整个块将会出现。

但是,这意味着除法将具有类型Integer -> Integer -> Either Error Integer,依此类推,以便将除法除以零。您必须在使用除法的任何代码中检测错误(显式模式匹配或使用绑定),或者甚至出现错误的可能性。 Haskell使用异常来避免这样做。

答案 3 :(得分:3)

我已经不必要地考虑了这一点,我相信答案是“是的,当它以某种方式使用时”(谢谢outis :), 但不是因为我之前想过的原因。

我在对agf's answer的评论中提到,>>=只是继续传递风格 - 给它一个生产者和一个回调,它“运行”生产者并将其提供给 打回来。但那不是真的。同样重要的是>>=必须运行 生产者与回调结果之间的一些交互

对于List monad,这将是连接列表。这个 互动是使monad特别的原因。

但我相信Python的with 进行此次互动,而不是 你可能期待的方式。

这是一个使用两个with语句的示例python程序:

class A:

    def __enter__(self):
        print 'Enter A'

    def __exit__(self, *stuff):
        print 'Exit A'

class B:

    def __enter__(self):
        print 'Enter B'

    def __exit__(self, *stuff):
        print 'Exit B'

def foo(a):
    with B() as b:
        print 'Inside'

def bar():
    with A() as a:
        foo(a)

bar()

运行时输出为:

Enter A
Enter B
Inside
Exit B
Exit A

现在,Python是imperative language,因此不仅仅是生成数据 产生副作用。但您可以将这些副作用视为数据 (比如IO ()) - 你不能用你可以结合的所有很酷的方式来组合它们 IO (),但他们达到了同一目标。

所以你应该关注的是那些操作的排序 - 也就是说, 打印报表的顺序。

现在比较Haskell中的相同程序:

data Context a = Context [String] a [String]
    deriving (Show)

a = Context ["Enter A"] () ["Exit A"]
b = Context ["Enter B"] () ["Exit B"]

instance Monad Context where
    return x = Context [] x []
    (Context x1 p y1) >>= f =
        let
            Context x2 q y2 = f p
        in
            Context (x1 ++ x2) q (y2 ++ y1)

foo :: a -> Context String
foo _ = b >> (return "Inside")

bar :: () -> Context String
bar () = a >>= foo

main = do
    print $ bar ()

产生:

Context ["Enter A","Enter B"] "Inside" ["Exit B","Exit A"]

顺序是一样的。

两个程序之间的类比非常直接:Context有一些 “输入”位,“主体”和一些“退出”位。我使用String代替 IO行动,因为它更容易 - 我认为它应该与IO行动类似(如果不是,请纠正我)。

>>=的{​​{1}}确实完全 Python中的Context:它运行的是 输入语句,将值提供给with,然后运行退出 语句。

(另一个巨大的差异是身体应该依赖于 输入陈述。我再次认为应该是可以修复的。)