做任务赚话费的网站,快速搭建网站2020,app制作软件排名,wordpress 插件 活动functor本文最初是我们使用RxJava进行React式编程的附录。 但是#xff0c;尽管与React式编程非常相关#xff0c;但对monad的介绍却不太适合。 因此#xff0c;我决定将其取出并作为博客文章单独发布。 我知道#xff0c;“ 我对单子的自己的#xff0c;一半正确和一半的… functor 本文最初是我们使用RxJava进行React式编程的附录。 但是尽管与React式编程非常相关但对monad的介绍却不太适合。 因此我决定将其取出并作为博客文章单独发布。 我知道“ 我对单子的自己的一半正确和一半的完整解释 ”是编程博客上的新“ Helloworld ”。 然而本文从Java数据结构和库的特定角度研究了函子和monad。 因此我认为值得分享。 RxJava的设计和构建基于非常基本的概念例如函子 monoid和monad 。 尽管Rx最初是为命令式C语言建模的并且我们正在学习RxJava并在类似的命令式语言上工作但该库还是源于函数式编程。 在意识到RxJava API的紧凑性之后您应该不会感到惊讶。 几乎只有少数几个核心类通常是不可变的并且所有内容都主要由纯函数组成。 随着函数式编程或函数式样式的最新兴起最普遍地用Scala或Clojure等现代语言表示monads成为了广泛讨论的话题。 他们周围有很多民间传说 monad是endofunctors类别中的monoid这是什么问题 詹姆斯·伊里 该monad的诅咒是一旦您获得了顿悟一旦您理解了“哦就是这样”您就失去了向任何人解释它的能力。 道格拉斯·克罗克福德 绝大多数程序员尤其是那些没有函数式编程背景的程序员都倾向于认为monad是某种神秘的计算机科学概念因此从理论上讲它对他们的编程事业无济于事。 这种消极的观点可以归因于数十篇文章或博客文章太抽象或太狭窄。 但是事实证明甚至标准的Java库都存在monad特别是自Java Development KitJDK8起稍后会有更多介绍。 绝对妙不可言的是一旦您第一次了解monad突然之间就会有几个完全不相同的目的无关的类和抽象变得熟悉。 Monad概括了各种看似独立的概念因此学习Monad的另一种化身只需很少的时间。 例如您不必学习CompletableFuture在Java 8中的工作方式一旦意识到它是monad就可以精确地知道它是如何工作的并且可以从其语义中得到什么。 然后您会听说RxJava听起来有很多不同但是由于Observable是monad因此没有太多可添加的。 您已经不知不觉中已经遇到过许多其他的单子示例。 因此即使您实际上没有使用RxJava本节也将是有用的复习。 函子 在解释什么是monad之前让我们研究一个称为functor的简单结构。 函子是封装某些值的类型化数据结构。 从语法的角度来看函子是具有以下API的容器 import java.util.function.Function;interface FunctorT {R FunctorR map(FunctionT, R f);} 但是仅仅语法是不足以了解什么是函子。 functor提供的唯一操作是带函数f map() 。 此函数接收框内的任何内容对其进行转换并将结果按原样包装到另一个函子中。 请仔细阅读。 FunctorT始终是不可变的容器因此map不会使执行该操作的原始对象发生突变。 取而代之的是它返回包装在全新函子中的结果或结果–请耐心等待该函子可能是类型R 此外在应用标识函数即map(x - x)时函子不应执行任何操作。 这种模式应始终返回相同的函子或相等的实例。 通常将FunctorT与保存T实例进行比较其中与该值交互的唯一方法是对其进行转换。 但是没有从函子解开或逃逸的惯用方式。 值始终在函子的上下文内。 函子为什么有用 它们使用一个统一的适用于所有对象的统一API来概括多个通用习语如集合promiseOptionals等。 让我介绍几个函子以使您更流畅地使用此API interface FunctorT,F extends Functor?,? {R F map(FunctionT,R f);
}class IdentityT implements FunctorT,Identity? {private final T value;Identity(T value) { this.value value; }public R IdentityR map(FunctionT,R f) {final R result f.apply(value);return new Identity(result);}} 需要额外的F类型参数来进行Identity编译。 在前面的示例中您看到的是最简单的函子仅包含一个值。 您只能使用map方法内部的值对其进行转换但无法提取它。 这被认为超出了纯函子的范围。 与函子进行交互的唯一方法是应用类型安全的转换序列 IdentityString idString new Identity(abc);
IdentityInteger idInt idString.map(String::length); 或流利地就像您编写函数一样 Identitybyte[] idBytes new Identity(customer).map(Customer::getAddress).map(Address::street).map((String s) - s.substring(0, 3)).map(String::toLowerCase).map(String::getBytes); 从这个角度来看在函子上的映射与调用链式函数没有太大不同 byte[] bytes customer.getAddress().street().substring(0, 3).toLowerCase().getBytes(); 您为什么还要烦恼这种冗长的包装不仅不提供任何附加值而且也无法将内容提取回去 好吧事实证明您可以使用此原始函子抽象对其他几个概念建模。 例如从Java 8开始的java.util.OptionalT是带有map()方法的函子。 让我们从头开始实现它 class FOptionalT implements FunctorT,FOptional? {private final T valueOrNull;private FOptional(T valueOrNull) {this.valueOrNull valueOrNull;}public R FOptionalR map(FunctionT,R f) {if (valueOrNull null)return empty();elsereturn of(f.apply(valueOrNull));}public static T FOptionalT of(T a) {return new FOptionalT(a);}public static T FOptionalT empty() {return new FOptionalT(null);}} 现在变得有趣了。 FOptionalT函子可以保存一个值但也可以为空。 这是一种对null进行编码的类型安全的方法。 构造FOptional方法有两种通过提供值或创建empty()实例。 在这两种情况下就像Identity FOptional是不可变的我们只能从内部与值交互。 FOptional不同之FOptional在于如果转换函数f为空则它可能不会应用于任何值。 这意味着函子可能不必完全封装类型T一个值。 它也可以包装任意数量的值就像List …functor import com.google.common.collect.ImmutableList;class FListT implements FunctorT, FList? {private final ImmutableListT list;FList(IterableT value) {this.list ImmutableList.copyOf(value);}Overridepublic R FList? map(FunctionT, R f) {ArrayListR result new ArrayListR(list.size());for (T t : list) {result.add(f.apply(t));}return new FList(result);}
} API保持不变在转换T - R使用函子但是行为却大不相同。 现在我们对FList每个项目应用转换以声明方式转换整个列表。 因此如果您有一个customers列表并且想要他们的街道列表则非常简单 import static java.util.Arrays.asList;FListCustomer customers new FList(asList(cust1, cust2));FListString streets customers.map(Customer::getAddress).map(Address::street); 这不再像说customers.getAddress().street()那样简单您不能在一组客户上调用getAddress() 必须在每个单独的客户上调用getAddress() 然后将其放回一个集合中。 顺便说一句Groovy发现这种模式是如此普遍以至于实际上有一个语法糖 customer*.getAddress()*.street() 。 该运算符称为散点图实际上是变相的map 。 也许您想知道为什么我要在map list手动遍历list而不是使用Java 8中的Stream list.stream().map(f).collect(toList()) 这会响吗 如果我告诉您Java java.util.stream.StreamT也是一个函子怎么办 顺便说一句一个单子 现在您应该看到函子的第一个好处–它们抽象了内部表示形式并为各种数据结构提供了一致且易于使用的API。 作为最后一个示例让我介绍类似于Future promise函数。 Promise “承诺”某一天将提供一个值。 它尚未出现可能是因为产生了一些后台计算或者我们正在等待外部事件。 但是它将在将来出现。 完成PromiseT的机制并不有趣但是函子的性质是 PromiseCustomer customer //...
Promisebyte[] bytes customer.map(Customer::getAddress).map(Address::street).map((String s) - s.substring(0, 3)).map(String::toLowerCase).map(String::getBytes); 看起来很熟悉 这就是重点 Promise函子的实现超出了本文的范围甚至不重要。 不用说我们非常接近从Java 8实现CompletableFuture 并且几乎从RxJava中发现了Observable 。 但是回到函子。 PromiseCustomer尚未持有Customer的值。 它有望在将来具有这种价值。 但是我们仍然可以像使用FOptional和FList一样映射此类函子–语法和语义完全相同。 行为遵循函子表示的内容。 调用customer.map(Customer::getAddress)产生PromiseAddress 这意味着map是非阻塞的。 customer.map() 不会等待基础的customer承诺完成。 相反它返回另一个不同类型的承诺。 当上游承诺完成时下游承诺将应用传递给map()的函数并将结果传递给下游。 突然我们的函子使我们能够以非阻塞方式流水线进行异步计算。 但是您不必理解或学习-因为Promise是函子所以它必须遵循语法和法则。 函子还有许多其他很好的例子例如以组合方式表示值或错误。 但是现在是时候看看单子了。 从函子到单子 我假设您了解函子是如何工作的为什么它们是有用的抽象。 但是函子并不像人们期望的那样普遍。 如果您的转换函数作为参数传递给map()那个返回函子实例而不是简单值会发生什么 好吧函子也只是一个值所以没有坏事发生。 将返回的所有内容放回函子中以便所有行为都保持一致。 但是假设您有以下方便的方法来解析String FOptionalInteger tryParse(String s) {try {final int i Integer.parseInt(s);return FOptional.of(i);} catch (NumberFormatException e) {return FOptional.empty();}
} 例外是会破坏类型系统和功能纯度的副作用。 在纯函数语言中没有例外的地方毕竟我们从来没有听说过在数学课上抛出例外对吗 错误和非法条件使用值和包装器明确表示。 例如 tryParse()接受一个String但并不简单地返回int或在运行时静默引发异常。 通过类型系统我们明确告知tryParse()可能失败字符串格式错误不会有任何异常或错误。 此半故障由可选结果表示。 有趣的是Java已经检查了必须声明和处理的异常因此从某种意义上讲Java在这方面比较纯净它没有隐藏副作用。 但是无论好坏通常在Java中不建议使用检查异常因此让我们回到tryParse() 。 用已经包装在FOptional String组成tryParse似乎很有用 FOptionalString str FOptional.of(42);
FOptionalFOptionalInteger num str.map(this::tryParse); 这不足为奇。 如果tryParse()返回一个int您将得到FOptionalInteger num 但是由于map()函数返回FOptionalInteger本身因此它被包装两次成为笨拙的FOptionalFOptionalInteger 。 请仔细查看类型您必须了解为什么在这里获得此双重包装。 除了看上去很恐怖之外在函子中放一个函子会破坏构图和流畅的链接 FOptionalInteger num1 //...
FOptionalFOptionalInteger num2 //...FOptionalDate date1 num1.map(t - new Date(t));//doesnt compile!
FOptionalDate date2 num2.map(t - new Date(t)); 在这里我们尝试通过将int转换为 Date 来映射FOptional的内容。 具有FunctorInteger int - Date的功能我们可以轻松地从FunctorInteger为FunctorDate 我们知道它是如何工作的。 但是在num2情况下情况变得复杂。 num2.map()接收的输入不再是int而是FOoptionInteger 显然java.util.Date没有这样的构造函数。 我们通过双重包裹来破坏了函子。 但是具有返回函子而不是简单值的函数非常普遍例如tryParse() 因此我们不能简单地忽略这种要求。 一种方法是引入一种特殊的无参数join()方法以“展平”嵌套函子 FOptionalInteger num3 num2.join() 它可以工作但是因为这种模式太普遍了所以引入了一种名为flatMap()特殊方法。 flatMap()与map非常相似但希望作为参数接收的函数返回函子-或monad是精确的 interface MonadT,M extends Monad?,? extends FunctorT,M {M flatMap(FunctionT,M f);
} 我们仅得出结论 flatMap只是一种语法糖可以实现更好的组合。 但是flatMap方法通常称为Haskell的bind或 具有所有不同之处因为它允许以纯函数式的形式构成复杂的转换。 如果FOptional是monad的实例则解析突然可以按预期进行 FOptionalInteger num FOptional.of(42);
FOptionalInteger answer num.flatMap(this::tryParse); Monads不需要实现map 可以轻松地在flatMap()之上实现它。 实际上 flatMap是启用全新转换领域的基本运算符。 显然就像函子一样语法顺从性还不足以将某个类称为monad flatMap()运算符必须遵循monad定律但它们非常直观就像flatMap()与标识的关联性一样。 后者要求m(x).flatMap(f)与持有值x任何monad和函数f f(x)相同。 我们不会深入研究monad理论而让我们关注实际含义。 当内部结构并非无关紧要时Monad便会发光例如Promise monad它将在将来具有一定的价值。 您可以从类型系统中猜测Promise在以下程序中的表现吗 首先所有可能花费一些时间才能完成的方法都会返回Promise import java.time.DayOfWeek;PromiseCustomer loadCustomer(int id) {//...
}PromiseBasket readBasket(Customer customer) {//...
}PromiseBigDecimal calculateDiscount(Basket basket, DayOfWeek dow) {//...
} 现在我们可以将这些函数组合起来就好像它们都是使用单子运算符进行了阻塞一样 PromiseBigDecimal discount loadCustomer(42).flatMap(this::readBasket).flatMap(b - calculateDiscount(b, DayOfWeek.FRIDAY)); 这变得很有趣。 flatMap()必须保留monadic类型因为所有中间对象都是Promise 。 不仅仅是保持类型有序-先前的程序突然完全异步 loadCustomer()返回Promise因此不会阻塞。 readBasket()接受Promise拥有将要拥有的一切并应用返回另一个Promise的函数依此类推。 基本上我们建立了一个异步计算管道其中后台完成一个步骤会自动触发下一步。 探索 有两个单子并将它们包含的值组合在一起是很常见的。 但是函子和monad均不允许直接访问其内部这是不纯的。 相反我们必须谨慎地应用转换而不能逃脱monad。 假设您有两个单子并且您想将它们合并 import java.time.LocalDate;
import java.time.Month;MonadMonth month //...
MonadInteger dayOfMonth //...MonadLocalDate date month.flatMap((Month m) -dayOfMonth.map((int d) - LocalDate.of(2016, m, d))); 请花点时间研究前面的伪代码。 我没有使用任何真正的monad实现例如Promise或List来强调核心概念。 我们有两个独立的monad一个是Month类型另一个是Integer类型。 为了从中构建LocalDate 我们必须构建一个嵌套的转换该转换可以访问两个monad的内部。 仔细研究这些类型尤其要确保您了解为什么我们在一个地方使用flatMap在另一个地方使用map() 。 想想如果您还有第三个MonadYear 那么您将如何构造此代码。 这种应用两个参数在本例中为m和d 的函数的模式非常普遍以至于Haskell中有一个特殊的辅助函数称为liftM2 它完全在map和flatMap上实现了这种转换。 在Java伪语法中它看起来像这样 MonadR liftM2(MonadT1 t1, MonadT2 t2, BiFunctionT1, T2, R fun) {return t1.flatMap((T1 tv1) -t2.map((T2 tv2) - fun.apply(tv1, tv2)));
} 您不必为每个monad实现此方法 flatMap()足够了而且它对所有monad都一致地起作用。 当您考虑如何将其与各种monad一起使用时 liftM2非常有用。 例如listM2(list1, list2, function)将对list1和list2 笛卡尔积中的每对可能的项应用function 。 另一方面对于可选选项仅当两个可选选项均为非空时它将应用功能。 更好的是对于Promise monad当两个Promise都完成时将异步执行一个函数。 这意味着我们只是发明了一个简单的同步机制分叉联接算法中的join() 该同步机制包含两个异步步骤。 我们可以轻松地在flatMap()之上构建的另一个有用的运算符是filter(PredicateT) 该运算符接收monad内部的所有内容如果不符合某些谓词则将其完全丢弃。 在某种程度上它类似于map但不是1-to-1映射而是1-to-0-or-1。 同样 filter()对于每个monad具有相同的语义但是取决于我们实际使用的monad其功能相当惊人。 显然它允许从列表中过滤掉某些元素 FListCustomer vips customers.filter(c - c.totalOrders 1_000); 但是它也可以正常工作例如对于可选项目。 在这种情况下如果可选内容不符合某些条件我们可以将非空可选内容转换为空值。 空的可选部分保持不变。 从单子列表到单子列表 另一个来自flatMap()有用运算符是sequence() 。 您只需查看类型签名即可轻松猜测其作用 MonadIterableT sequence(IterableMonadT moands) 通常我们有一堆相同类型的monad而我们想要一个具有该类型列表的monad。 对您来说这听起来似乎很抽象但却非常有用。 想象一下您想通过ID同时从数据库中加载一些客户因此您多次对不同的ID使用loadCustomer(id)方法每次调用都返回PromiseCustomer 。 现在您有了Promise的列表但您真正想要的是客户列表例如要在Web浏览器中显示的客户列表。 sequence() 在RxJava中sequence()称为concat()或merge() 具体取决于用例是为此目的而构建的 FListPromiseCustomer custPromises FList.of(1, 2, 3).map(database::loadCustomer);PromiseFListCustomer customers custPromises.sequence();customers.map((FListCustomer c) - ...); 通过为每个ID调用database.loadCustomer(id) 我们可以在其上map一个表示客户ID的FListInteger 您看到FList是一个函子吗 这导致Promise列表非常不便。 sequence()节省了一天的时间但是再次这不仅是语法糖。 前面的代码是完全非阻塞的。 对于不同种类的monads sequence()仍然有意义但是在不同的计算上下文中。 例如可以将FListFOptionalT更改为FOptionalFListT 。 顺便说一句您可以在flatMap()之上实现sequence() 就像map()一样flatMap() 。 一般而言这只是关于flatMap()和monad有用性的冰山一角。 尽管源于晦涩的类别理论但即使在Java之类的面向对象的编程语言中monad也被证明是极其有用的抽象。 能够组成返回单子函数的函数非常有用以至于数十个无关的类遵循单子行为。 而且一旦将数据封装在monad中通常很难显式地将其取出。 这种操作不是monad行为的一部分并且经常导致非惯用语代码。 例如 PromiseT上的Promise.get()可以从技术上返回T 但是只能通过阻塞来返回而所有基于flatMap()运算符都是非阻塞的。 另一个示例是FOptional.get()可能会失败因为FOptional可能为空。 即使FList.get(idx)从列表偷窥特定元素听起来很别扭因为你可以替换for与循环map()经常。 我希望您现在了解为什么单子如此流行。 即使在像Java这样的面向对象的语言中它们也是非常有用的抽象。 翻译自: https://www.javacodegeeks.com/2016/06/functor-monad-examples-plain-java.htmlfunctor