对于那些不想学习Haskell以便理解Haskell的人来说,monad是什么?

时间:2018-04-16 12:28:06

标签: haskell functional-programming monads

编程中的monad是什么,可以解释为:

  1. 您不需要任何Haskell的先验知识来理解。
  2. 您可以继续使用或构建更主流的编程语言(如C ++ / Java / Scala)中的概念。
  3. 基本上,我在网上学习时遇到过这个术语。例如,这个Java 8 Stream教程提到了monads(http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/)。所以我想以有用的方式理解基本概念。 (与愚蠢的方式相反,想象一下编程为橙色,那么monad将是葡萄柚......"有点像。)

    然而,每个monad教程似乎都需要先前的知识(或试图在运行中教你)Haskell或Haskell语法。

2 个答案:

答案 0 :(得分:6)

我将尝试使用Scala语法和语义给出最简单的解释。

Monad是任何具有以下签名的类型:

trait Monad[A] {
  def flatMap[B](f: A ⇒ Monad[B]): Monad[B]
}

def unit[A](x: A): Monad[A]

并满足以下法律:

  • 左侧身份

    unit(x).flatMap(f) == f(x)
    
  • 正确的身份

    m.flatMap(unit _) == m
    
  • 结合性

    m.flatMap(f).flatMap(g) == m.flatMap(f(_).flatMap(g))
    

那就是。这就是monad的美丽。满足这些约束的任何都是monad。并且有很多东西可以满足这些限制:列表,集合,词典,地图,选项,数据库查询,延续,错误,副作用,口译员,编译器,......

monad的强大功能来自足够的结构的组合,你可以编写很多操作在monad上的操作,同时这么少的结构很多东西都是单子。这两个属性的组合意味着您可以编写许多一次的操作,然后应用于许多不同类型的对象。

这是monad的问题之一:它们的普遍性。 monad是一种非常通用的抽象,它使得它不是立即显而易见的。我的意思是,集合,列表,数据库查询,副作用,延续和编译器有什么共同之处?

答案 1 :(得分:4)

要把它带到C ++(metaclasses之前,这最终会给出一种正确表达方式),monad的最佳描述是这样的:

  • 参数化类(类似于STL容器类型),即表格类

    template <typename a>
    class M;
    
  • 至少支持以下功能:

    • “琐碎的注射”

      template <typename a, typename b>
      M<a> pure (a x);
      

      数学家称之为η,Haskell传统上称之为return,其他语言通常称为unit

    • Functor 映射

      template <typename a, typename b>
      M<b> fmap (std::function<b(a)> f, M<a> m);
      
    • 展平操作

      template <typename a, typename b>
      M<a> join (M< M<a> > mm);
      

      哪位数学家称​​μ。许多编程语言(包括Haskell)本身并没有实现,而是与fmap结合使用,因此它被称为flatMap>>=。但join是最简单的形式。

  • 以满足monad laws

数组类型示例:

template <typename a>
struct array {
  std::vector<a> contents;
};

template <typename a>
array<a> pure(a x) {
  return array<a>{{std::vector<a>({x})}};   // array with only a single element.
}

template <typename a, typename b>
array<b> fmap(std::function<b(a)> f, array<a> m) {
  std::vector<b> resultv;
  for(auto& x: m) {
    resultv.push_back(f(x));
  }
  return array<b>{{resultv}};   // array with the elements transformed by the given function
}

template <typename a>
array<a> join(array< array<a> > mm) {
  std::vector<a> resultv;

  for(auto& row: mm.contents) {
    for(auto& x: row.contents) {
      resultv.push_back(x);
    }
  }
  return array<a>{{resultv}};   // array with all the rows concatenated.
}

那么,有什么用呢?好吧,fmap本身非常有用,当你很快就想在一个数组的所有元素上映射lambda(允许改变类型)时,不必乱用任何迭代器(不像std::transform })。但是fmap并不真正需要monad。

真正发挥作用的是一般的排序行动。这个数组示例不会很清楚,所以让我介绍另一个monad:

template <typename a>
struct writer {
  std::string logbook;
  a result;
};

template <typename a, typename b>
writer<a> pure (a x) {
  return writer<a>{{"", x}};  // nothing to log yet
}

template <typename a, typename b>
writer<b> fmap (std::function<b(a)> f, writer<a> m){
  return writer<b>{{m.logbook, f(m.result)}};
                      // simply keep the log as-is
}

template <typename a, typename b>
writer<a> join (writer< writer<a> > mm) {
  return writer<a>{{mm.logbook + mm.result.logbook, m.result.result}};
      // Concatenate the two logs, and keep the inner value
}

writer更像是着名的Haskell monad IOwriter类型允许您将任意日志写入功能组合在一起,而不必担心它,收集所有日志信息。

你可能想知道: 在哪里记录?以上操作都没有实际生成任何日志条目!事实上他们没有 - 事实上,如果他们这样做,那么monad法则会受到侵犯! Monads不是特定的行为,预先建立的价值观。相反,他们只是给你一个非常通用的框架,用于“粘合”这样的行为。

这种粘合合成器的一个简单示例是replicateM,它采用单个monadic动作并按顺序执行 n 次,收集所有结果。不幸的是,这完全没有在C ++中正确输入,但这是一个只适用于编写器monad的专用版本。首先,让我们快速实现前面提到的组合fmap-join,因为它在实践中比join更方便:

template<typename a, typename b>
writer<b> flatMap(std::function<writer<b>(a)> f, writer<a> xs) {
  return join(fmap(f,xs));
}

template <typename a>
writer<array<a>> replicateM (int n, writer<a> m) {
  if (n>0) {
    writer<array<a>> resultv = fmap(pure, m);
    for (int i=1; i<n; ++i) {
      resultv = flatMap( [&](array<a> xs){
                           return fmap( [&](a x){
                                           return xs.push_back(x);}
                                      , m );}
                       , resultv);
    }
  } else {
    return pure(std::vector<a>());
  }
}

请注意,上面的代码都没有实际使用writer特有的任何内容,因此我可以复制并粘贴它并将其用于任何其他monad。 (或者,使用具有更高亲缘性的多态性的语言,只需一次性编写。)

replicateMwriter的情况下做了什么,坦率地说是非常愚蠢的 - 它只是重复相同的日志消息n次,并复制结果值n次。但是,这只是最简单的示例:monad还可以具有更多功能,例如,在IO monad中,每次调用都可能产生不同的结果(例如,因为它从标准输入读取)。通用monad接口允许您抽象出各种不同的副作用,但仍然清楚地跟踪在给定上下文中可能发生的副作用。

不幸的是,C ++社区滥用单词“functor”只是为了描述功能对象。虽然这是一个相关的概念,但functor实际上不止于此。