我仍然蹒跚而行,但我不禁注意到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(NaN
和DivideByZero
作为特殊情况)...
就像我说的那样,早上3点。
答案 0 :(得分:23)
这几乎是微不足道的,但第一个问题是with
不是一个函数,并没有将函数作为参数。您可以通过编写with
的函数包装器来轻松解决这个问题:
def withf(context, f):
with context as x:
f(x)
由于这是微不足道的,因此您无法区分withf
和with
。
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__
方法) 。 bind
和with
类型之间的类型为M a -> (a -> N b) -> N b
。要成为monad,with
在b
不是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)
我建议阅读yield
与with
相比更多与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
,然后运行退出
语句。
(另一个巨大的差异是身体应该依赖于 输入陈述。我再次认为应该是可以修复的。)