就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?

它解决了什么问题,最常用的地方是什么?

使现代化

为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序?


当前回答

monad是一个函数数组

(Pst:函数数组只是一个计算)。

实际上,这些函数不是真正的数组(一个单元格数组中的一个函数),而是由另一个函数>>=链接。>>=允许调整函数i的结果以馈送函数i+1,并在它们之间执行计算或者甚至不调用函数i+1。

这里使用的类型是“带上下文的类型”。这是一个带有“标记”的值。被链接的函数必须采用“裸值”并返回标记结果。>>=的职责之一是从上下文中提取裸值。还有一个函数“return”,它接受一个裸值并将其与一个标记一起放置。

Maybe的一个例子。让我们使用它来存储一个简单的整数,以便进行计算。

-- a * b
multiply :: Int -> Int -> Maybe Int
multiply a b = return  (a*b)

-- divideBy 5 100 = 100 / 5
divideBy :: Int -> Int -> Maybe Int
divideBy 0 _ = Nothing -- dividing by 0 gives NOTHING
divideBy denom num = return (quot num denom) -- quotient of num / denom

-- tagged value
val1 = Just 160 

-- array of functions feeded with val1
array1 = val1 >>= divideBy 2  >>= multiply 3 >>= divideBy  4 >>= multiply 3

-- array of funcionts created with the do notation
-- equals array1 but for the feeded val1
array2 :: Int -> Maybe Int
array2 n = do
       v <- divideBy 2  n
       v <- multiply 3 v
       v <- divideBy 4 v
       v <- multiply 3 v
       return v

-- array of functions, 
-- the first >>= performs 160 / 0, returning Nothing
-- the second >>= has to perform Nothing >>= multiply 3 ....
-- and simply returns Nothing without calling multiply 3 ....
array3 = val1 >>= divideBy 0  >>= multiply 3 >>= divideBy  4 >>= multiply 3

main = do
     print array1
     print (array2 160)
     print array3

为了说明monad是带有助手操作的函数数组,请考虑与上述示例等效,仅使用一个实函数数组

type MyMonad = [Int -> Maybe Int] -- my monad as a real array of functions

myArray1 = [divideBy 2, multiply 3, divideBy 4, multiply 3]

-- function for the machinery of executing each function i with the result provided by function i-1
runMyMonad :: Maybe Int -> MyMonad -> Maybe Int
runMyMonad val [] = val
runMyMonad Nothing _ = Nothing
runMyMonad (Just val) (f:fs) = runMyMonad (f val) fs

它的用法如下:

print (runMyMonad (Just 160) myArray1)

其他回答

如果您曾经使用过Powershell,Eric描述的模式听起来应该很熟悉。Powershell cmdlet是monad;功能组成由管道表示。

杰弗里·斯诺弗对埃里克·梅杰的采访更为详细。

你最近有一篇演讲《Monadologie——关于类型焦虑的专业帮助》(Christopher League,2010年7月12日),这篇演讲对延续和monad的话题非常有趣。这个(幻灯片)演示的视频实际上可以在vimeo上获得。Monad部分开始于37分钟左右,在这段一小时的视频中,从58张幻灯片中的第42张幻灯片开始。

它被称为“函数式编程的主要设计模式”,但示例中使用的语言是Scala,它既是面向对象的又是函数式的。您可以在Debasish Ghosh(2008年3月27日)的博客文章“Monads-在Scala中抽象计算的另一种方法”中阅读更多关于Monad的内容。

如果类型构造函数M支持以下操作,那么它就是monad:

# the return function
def unit[A] (x: A): M[A]

# called "bind" in Haskell 
def flatMap[A,B] (m: M[A]) (f: A => M[B]): M[B]

# Other two can be written in term of the first two:

def map[A,B] (m: M[A]) (f: A => B): M[B] =
  flatMap(m){ x => unit(f(x)) }

def andThen[A,B] (ma: M[A]) (mb: M[B]): M[B] =
  flatMap(ma){ x => mb }

例如(在Scala中):

选项是monad

    def unit[A] (x: A): Option[A] = Some(x)

    def flatMap[A,B](m:Option[A])(f:A =>Option[B]): Option[B] =
      m match {
       case None => None
       case Some(x) => f(x)
      }

列表为Monad

    def unit[A] (x: A): List[A] = List(x)

    def flatMap[A,B](m:List[A])(f:A =>List[B]): List[B] =
      m match {
        case Nil => Nil
        case x::xs => f(x) ::: flatMap(xs)(f)
      }

Monad在Scala中非常重要,因为它是为了利用Monad结构而构建的方便语法:

对于Scala的理解:

for {
  i <- 1 to 4
  j <- 1 to i
  k <- 1 to j
} yield i*j*k

由编译器翻译为:

(1 to 4).flatMap { i =>
  (1 to i).flatMap { j =>
    (1 to j).map { k =>
      i*j*k }}}

关键抽象是flatMap,它通过链接绑定计算。flatMap的每次调用都返回相同的数据结构类型(但值不同),作为链中下一个命令的输入。

在上面的代码段中,flatMap将闭包(SomeType)=>List[AanotherType]作为输入,并返回List[Aanother Type]。需要注意的一点是,所有flatMap都采用相同的闭包类型作为输入,并返回与输出相同的类型。

这就是“绑定”计算线程的原因——为了理解,序列中的每一项都必须遵守相同的类型约束。


如果您执行两个操作(可能失败)并将结果传递给第三个,例如:

lookupVenue: String => Option[Venue]
getLoggedInUser: SessionID => Option[User]
reserveTable: (Venue, User) => Option[ConfNo]

但如果不利用Monad,你会得到复杂的OOP代码,比如:

val user = getLoggedInUser(session)
val confirm =
  if(!user.isDefined) None
  else lookupVenue(name) match {
    case None => None
    case Some(venue) =>
      val confno = reserveTable(venue, user.get)
      if(confno.isDefined)
        mailTo(confno.get, user.get)
      confno
  }

而使用Monad,您可以像所有操作一样使用实际类型(地点、用户),并隐藏选项验证内容,这都是因为for语法的平面图:

val confirm = for {
  venue <- lookupVenue(name)
  user <- getLoggedInUser(session)
  confno <- reserveTable(venue, user)
} yield {
  mailTo(confno, user)
  confno
}

只有当所有三个函数都具有Some[X]时,才会执行屈服部分;任何“无”将直接返回以确认。


So:

Monad允许在函数编程中进行有序计算,这允许我们以一种很好的结构化形式(有点像DSL)对动作序列进行建模。最大的能力来自于将服务于不同目的的monad组合成应用程序中的可扩展抽象的能力。monad对动作的排序和线程化由语言编译器完成,该语言编译器通过闭包的魔力进行转换。


顺便说一句,Monad不是FP中使用的唯一计算模型:

范畴理论提出了许多计算模型。其中计算的Arrow模型莫纳德计算模型计算的应用模型

为什么我们需要单子?

我们只想使用函数编程。(毕竟是“功能编程”-FP)。然后,我们遇到了第一个大问题。这是一个程序:f(x)=2*xg(x,y)=x/y我们怎么能说首先要执行什么?我们如何使用不超过个函数来形成一个有序的函数序列(即程序)?解决方案:组合函数。如果你先要g,然后要f,只需写f(g(x,y))。好的,但是。。。更多问题:某些函数可能会失败(即g(2,0),除以0)。我们在FP中没有“例外”。我们如何解决它?解决方案:让我们允许函数返回两种东西:而不是g:Real,Real->Real(函数从两个实数转换为实数),让我们允许g:Real、Real->Real|Nothing(函数从一个实数转换成(实数或零))。但函数应该(更简单地)只返回一件事。解决方案:让我们创建一种要返回的新类型的数据,一种“装箱类型”,它可能包含一个真实的数据,也可能只是一个空数据。因此,我们可以有g:真实,真实->可能真实。好的,但是。。。f(g(x,y))现在发生了什么?f还没有准备好使用“也许真的”。而且,我们不想改变我们可以与g连接的每一个函数,以使用Maybe Real。解决方案:让我们有一个特殊的函数来“连接”/“组合”/“链接”函数。这样,我们就可以在幕后调整一个函数的输出,以支持下一个函数。在我们的例子中:g>>=f(连接/合成g到f)。我们希望>>=获取g的输出,检查它,如果它是Nothing,则不要调用f并返回Nothing;或者相反,提取装箱的实数并用它来馈送f。(此算法只是Maye类型的>>=的实现)。出现了许多其他问题,可以使用相同的模式来解决:1。使用“框”来编码/存储不同的含义/值,并具有像g这样的函数来返回这些“框值”。2.让作曲家/链接器g>>=f帮助将g的输出连接到f的输入,这样我们就不必改变f。使用该技术可以解决的显著问题有:具有函数序列中的每个函数(“程序”)可以共享的全局状态:解StateMonad。我们不喜欢“不纯函数”:对相同输入产生不同输出的函数。因此,让我们标记这些函数,使它们返回一个标记/装箱的值:IOmonad。

完全幸福!!!!

快速解释:

单体(在函数式编程中)是具有上下文相关行为的函数。

上下文作为参数传递,从先前的monad调用返回。它使它看起来像是同一个参数在后续调用中产生了不同的返回值。

等效值:Monad是其实际参数取决于调用链的过去调用的函数。

典型示例:有状态函数。

FAQ

等等,你说的“行为”是什么意思?

行为是指特定输入的返回值和副作用。

但它们有什么特别之处?

在过程语义中:没有。但它们仅使用纯函数进行建模。这是因为像Haskell这样的纯函数编程语言只使用本身没有状态的纯函数。

但是,国家从何而来?

状态性来自函数调用执行的顺序性。它允许嵌套函数通过多个函数调用拖动某些参数。这将模拟状态。monad只是一种软件模式,它将这些附加参数隐藏在光鲜亮丽的函数的返回值后面,通常称为return和bind。

为什么在Haskell中输入/输出是monad?

因为显示的文本是操作系统中的一种状态。如果多次读取或写入同一文本,则每次调用后操作系统的状态将不相同。相反,输出设备将显示文本输出的3倍。为了对操作系统做出正确的反应,Haskell需要将操作系统状态建模为monad。

从技术上讲,你不需要monad的定义。纯粹的函数式语言可以将“唯一性类型”的概念用于相同的目的。

单子在非功能语言中存在吗?

是的,基本上,解释器是一个复杂的monad,解释每个指令并将其映射到操作系统中的一个新状态。

详细说明:

monad(在函数式编程中)是一种纯函数式软件模式。monad是一个自动维护的环境(一个对象),可以在其中执行一系列纯函数调用。函数结果修改或与该环境交互。

换句话说,monad是一个“函数中继器”或“函数链接器”,它在自动维护的环境中链接和评估参数值。链接的参数值通常是“更新函数”,但实际上可以是任何对象(具有组成容器的方法或容器元素)。monad是在每个求值参数前后执行的“粘合代码”。这个粘合代码函数“bind”应该将每个参数的环境输出集成到原始环境中。

因此,monad以特定于特定monad的实现方式连接所有参数的结果。控制和数据是否或如何在参数之间流动也是特定于实现的。

这种交织执行允许模拟完整的命令式控制流(如GOTO程序中的)或并行执行,仅使用纯函数,还可以在函数调用之间进行副作用、临时状态或异常处理,即使应用的函数不知道外部环境。

编辑:请注意,monads可以以任何类型的控制流图来评估功能链,甚至是非确定性NFA式的方式,因为剩余的链是延迟评估的,可以在链的每个点进行多次评估,这允许在链中进行回溯。

使用monad概念的原因是纯函数范式,它需要一个工具来以纯方式模拟典型的无可指责的建模行为,而不是因为它们做了一些特殊的事情。

面向OOP人群的修道院

在OOP中,monad是一个典型的对象

通常称为return的构造函数,它将值转换为环境的初始实例一种可链接的参数应用程序方法,通常称为bind,它使用作为参数传递的函数的返回环境来维护对象的状态。

有些人还提到了第三个函数join,它是bind的一部分。因为“参数函数”在环境中求值,所以它们的结果嵌套在环境本身中。join是“取消嵌套”结果(使环境变平)的最后一步,用新环境替换环境。

monad可以实现Builder模式,但允许更广泛的使用。

示例(Python)

我认为monad最直观的例子是Python中的关系运算符:

result =  0 <= x == y < 3

您可以看到它是一个monad,因为它必须携带一些布尔状态,而这些状态是单个关系运算符调用所不知道的。

如果您考虑如何在低级别上实现它而不发生短路行为,那么您将得到一个monad实现:

# result = ret(0)
result = (0, true)
# result = result.bind(lambda v: (x, v <= x))
result[1] = result[1] and result[0] <= x
result[0] = x
# result = result.bind(lambda v: (y, v == y))
result[1] = result[1] and result[0] == y
result[0] = y
# result = result.bind(lambda v: (3, v < 3))
result[1] = result[1] and result[0] < 3
result[0] = 3
result = result[1]      # not explicit part of a monad

真正的monad最多只能计算一次每个参数。

现在考虑一下“result”变量,就会得到这个链:

ret(0) .bind (lambda v: v <= x) .bind (lambda v: v == y) .bind (lambda v: v < 3)

monad在OO中是否具有“自然”解释取决于monad。在像Java这样的语言中,您可以将may monad转换为检查空指针的语言,这样失败的计算(即,在Haskell中生成Nothing)会将空指针作为结果发出。您可以将状态monad转换为通过创建可变变量和更改其状态的方法生成的语言。

monad是内函子范畴中的幺半群。

这句话所表达的信息非常深刻。你在一个monad中使用任何命令式语言。monad是一种“有序”的领域特定语言。它满足某些有趣的财产,这些属性使单子函数成为“命令式编程”的数学模型。Haskell使定义小型(或大型)命令式语言变得容易,这些语言可以以多种方式组合。

作为一名OO程序员,您使用语言的类层次结构来组织可以在上下文中调用的函数或过程的类型,即您所称的对象。monad也是对这个概念的抽象,因为不同的monad可以以任意方式组合,有效地将所有子monad的方法“导入”到范围中。

从体系结构上讲,然后使用类型签名来明确表示可以使用哪些上下文来计算值。

可以使用monad转换器来实现这一目的,并且有一个高质量的所有“标准”monad集合:

列表(通过将列表视为域进行非确定性计算)可能(计算可能失败,但报告不重要)错误(可能失败并需要异常处理的计算Reader(可以由普通Haskell函数组合表示的计算)编写器(使用顺序“渲染”/“记录”(到字符串、html等)进行计算)续(续)IO(取决于底层计算机系统的计算)状态(上下文包含可修改值的计算)

具有相应的monad变压器和类型类别。类型类允许通过统一monad的接口来组合monad,从而使具体monad可以实现monad“类”的标准接口。例如,模块Control.Monad.State包含一个类MonadState s m,(State s)是表单的一个实例

instance MonadState s (State s) where
    put = ...
    get = ...

长话短说,monad是一个函子,它将“上下文”附加到一个值上,它可以向monad中注入一个值,并且可以根据附加到它上的上下文来评估值,至少是以受限的方式。

So:

return :: a -> m a

是一个函数,它将a类型的值注入到m类型的monad“action”中。

(>>=) :: m a -> (a -> m b) -> m b

是一个执行monad操作、评估其结果并将函数应用于结果的函数。(>>=)的妙处在于结果在同一个monad中。换句话说,在m>>=f中,(>>=)从m中提取结果,并将其绑定到f,这样结果就在monad中。(或者,我们可以说(>>=)将f拉入m,并将其应用于结果。)因此,如果我们有f::a->m b和g::b->m c,我们可以“排序”动作:

m >>= f >>= g

或者,使用“do符号”

do x <- m
   y <- f x
   g y

(>>)的类型可能是发光的。它是

(>>) :: m a -> m b -> m b

它对应于C等过程语言中的(;)运算符。它允许使用以下表示法:

m = do x <- someQuery
       someAction x
       theNextAction
       andSoOn

在数学和哲学逻辑中,我们有框架和模型,这些框架和模型“自然”地用单子主义建模。解释是一种函数,它查看模型的域,并计算命题(或公式,在推广下)的真值(或推广)。在必要性的模态逻辑中,我们可能会说,如果命题在“每个可能的世界”中都是真的,那么它是必要的——如果它在每个可容许的域中都是真实的。这意味着命题语言中的模型可以具体化为一个模型,其域由不同模型的集合组成(一个对应于每个可能的世界)。每个monad都有一个名为“join”的方法,该方法将分层,这意味着结果为monad动作的每个monad动作都可以嵌入到monad中。

join :: m (m a) -> m a

更重要的是,这意味着monad在“层堆叠”操作下关闭。这就是monad转换器的工作原理:它们通过为以下类型提供“类联接”方法来组合monad:

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

这样我们就可以将(MaybeT m)中的作用转换为m中的作用,有效地折叠层。在本例中,runMaybeT::MaybeT m a->m(Maybe a)是我们的类联接方法。(MaybeT m)是一个monad,MaybeT::m(Maybe a)->MaybeT ma实际上是m中一个新类型monad动作的构造函数。

函子的自由单元是通过堆叠f生成的单元,这意味着f的每个构造函数序列都是自由单元的元素(或者更确切地说,是与f的构造函数序列树形状相同的元素)。自由单体是一种用最少的锅炉板构建柔性单体的有用技术。在Haskell程序中,我可能会使用自由monad来为“高级系统编程”定义简单monad,以帮助维护类型安全(我只是在使用类型及其声明。使用组合子可以直接实现):

data RandomF r a = GetRandom (r -> a) deriving Functor
type Random r a = Free (RandomF r) a


type RandomT m a = Random (m a) (m a) -- model randomness in a monad by computing random monad elements.
getRandom     :: Random r r
runRandomIO   :: Random r a -> IO a (use some kind of IO-based backend to run)
runRandomIO'  :: Random r a -> IO a (use some other kind of IO-based backend)
runRandomList :: Random r a -> [a]  (some kind of list-based backend (for pseudo-randoms))

Monastem是您可能称之为“解释器”或“命令”模式的基础架构,抽象为最清晰的形式,因为每个单元计算都必须“运行”,至少是微不足道的。(运行时系统为我们运行IO monad,是任何Haskell程序的入口点。IO通过按顺序运行IO操作来“驱动”其余的计算)。

join的类型也是我们得到monad是内函子范畴中的幺半群的陈述的地方。由于其类型,联接对于理论目的来说通常更为重要。但了解类型意味着了解单子。Join和monad变换器的类Join类型在函数组合的意义上是内函子的有效组合。把它放在类似Haskell的伪语言中,

Foo::m(m a)<->(m.m)a