使用 promise 的语法编写同步代码有什么好处吗? [英] Would there be any benefit to writing synchronous code using the syntax of promises

查看:63
本文介绍了使用 promise 的语法编写同步代码有什么好处吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

有同步promise这样的概念吗?使用 promise 的语法编写同步代码有什么好处吗?

尝试{富();酒吧(a,b);巴姆();}赶上(e){句柄错误(e);}

...可以这样写(但使用 then 的同步版本);

foo().then(bar.bind(a, b)).然后(砰).fail(handleError)

解决方案

有同步承诺这样的概念吗?

本杰明是完全正确的.Promise 是一种 monad.然而,它们并不是唯一的类型.

如果您还不知道它,那么您可能想知道 monad 是什么.网上有很多关于 monad 的解释.然而,他们中的大多数人都受到 monad 教程谬误.

简而言之,这个谬误是大多数理解 monad 的人并不真正知道如何向其他人解释这个概念.简单来说,单子是一个抽象的概念,人类很难掌握抽象的概念.然而,人类很容易理解具体的概念.

因此,让我们从一个具体的概念开始征服以了解 monad.正如我所说,单子是一个抽象的概念.这意味着 monad 是一个 接口,没有 implementation(即它定义了某些操作并指定了这些操作应该做什么,但没有指定必须如何完成).>

现在,有不同类型的 monad.每种类型的 monad 都是具体的(即它定义了 monad 实现http://en.wikipedia.org/wiki/Interface_%28computing%29" rel="nofollow noreferrer">接口).Promise 是一种 monad.因此,promise 是 monad 的一个具体例子.因此,如果我们研究 promise,那么我们就可以开始理解 monad.

那么我们从哪里开始呢?幸运的是,用户 spike 在他的 评论你的问题:

<块引用>

我能想到的一个例子是将 promise 与同步代码链接在一起.在寻找这个问题的答案时:基于场景动态生成 AJAX 请求我在承诺中包装了一个同步调用,以便能够将它们与其他承诺联系起来.

那么让我们看看他的代码:

var run = function() {获取场景().then(mapToInstruction).then(waitForTimeout).then(callApi).then(handleResults).然后(运行);};

这里run函数返回一个promise,它由getScenariomapToInstructionwaitForTimeout返回的promise组成>、callApihandleResultsrun 本身链接在一起.

现在,在我们继续之前,我想向您介绍一种新的符号来可视化这些函数的作用:

run :: Unit ->推迟一个getScenario :: 单元 ->延迟数据mapToInstruction :: 数据 ->延期教学waitForTimeout :: 指令 ->延期教学callApi :: 指令 ->延迟数据handleResults :: 数据 ->延期单位

所以这里是细分:

  1. :: 符号表示“是类型”-> 符号表示“to”.因此,例如 run :: Unit ->Deferred a 读作 runUnitDeferred a 的类型.
  2. 这意味着run 是一个函数,它接受一个Unit 值(即没有参数)并返回一个Deferred a 类型的值.
  3. 这里,a 表示任何类型.我们不知道 a 是什么类型,也不关心 a 是什么类型.因此,它可以是任何类型.
  4. 这里,Deferred 是一种承诺数据类型(具有不同的名称),而 Deferred a 意味着当承诺被解析时,它会产生一个 类型的值一个.

我们可以从上面的可视化中了解到几件事:

  1. 每个函数都接受一些值并返回一个承诺.
  2. 每个 promise 返回的已解析值成为下一个函数的输入:

    run :: Unit ->推迟一个getScenario :: 单元 ->延迟数据getScenario :: 单元 ->延迟数据mapToInstruction :: 数据 ->延期教学mapToInstruction :: 数据 ->延期教学waitForTimeout :: 指令 ->延期教学waitForTimeout :: 指令 ->延期教学callApi :: 指令 ->延迟数据callApi :: 指令 ->延迟数据handleResults :: 数据 ->延期单位handleResults :: 数据 ->延期单位运行::单元->推迟一个

  3. 下一个函数在前一个 promise 被解析之前无法执行,因为它必须使用前一个 promise 的解析值.

现在,正如我之前提到的,monad 是一个 接口,它定义了某些操作.monad 接口提供的操作之一是链接 monad 的操作.在承诺的情况下,这是 then 方法.例如:

getScenario().then(mapToInstruction)

我们知道:

getScenario :: Unit ->延迟数据mapToInstruction :: 数据 ->延期教学

因此:

getScenario() :: Deferred Data -- 因为当调用时,getScenario-- 返回延迟数据值

我们也知道:

getScenario().then(mapToInstruction) :: 延迟指令

因此,我们可以推断:

then :: 推迟一个 ->(a -> 延期 b) ->递延 b

换句话说,then 是一个函数,它接受两个参数(Deferred a 类型的值和 类型的函数a -> Deferred b) 并返回 Deferred b 类型的值.” 因此:

then :: 推迟一个 ->(a -> 延期 b) ->递延 bgetScenario() :: 延迟数据-- 因此,由于 a = 数据getScenario().then :: (Data -> Deferred b) ->递延 bmapToInstruction :: 数据 ->延期教学-- 因此,因为 b = 指令getScenario().then(mapInstruction) :: 延迟指令

所以我们得到了我们的第一个 monad 操作:

then :: 推迟一个 ->(a -> 延期 b) ->递延 b

但是,这个操作是具体的.它特定于承诺.我们想要一个可以适用于任何 monad 的抽象操作.因此,我们对函数进行泛化,使其适用于任何 monad:

bind :: Monad m =>m a ->(a -> m b) ->米

请注意,此 bind 函数与 Function.prototype.bind.这个 bind 函数是 then 函数的泛化.然后 then 函数特定于 Promise.但是,bind 函数是通用的.它适用于任何 monad m.

粗箭头 => 表示 有界量化.如果 ab 可以是任何类型,那么 m 可以是任何类型,实现了 monad 接口.我们不关心 m 是什么类型,只要它实现了 monad 接口.

这是我们在 JavaScript 中实现和使用 bind 函数的方式:

function bind(m, f) {返回 m.then(f);}绑定(getScenario(),mapToInstruction);

这是如何通用的?好吧,我可以创建一个新的数据类型来实现 then 函数:

//标识 :: a ->身份一功能标识(值){this.value = 值;}//然后 :: 标识 a ->(a -> 身份 b) ->身份bIdentity.prototype.then = 函数 (f) {返回 f(this.value);};//一 :: 身份证号码var one = new Identity(1);//是 :: 标识布尔值var yes = bind(one, isOdd);//isOdd :: Number ->标识布尔值函数 isOdd(n) {返回新的身份(n % 2 === 1);}

代替 bind(one, isOdd) 我可以轻松地编写 one.then(isOdd)(实际上更容易阅读).

Identity 数据类型,和 promises 一样,也是一种 monad.事实上,它是所有 monad 中最简单的.它被称为 Identity 因为它不会对其输入类型做任何事情.它保持原样.

不同的 monad 有不同的效果,这使得它们很有用.例如,promise 具有管理异步性的效果.但是 Identity monad 没有效果.它是一种vanilla 数据类型.

无论如何,继续……我们发现了 monad 的一种操作,bind 函数.还有一个操作有待发现.事实上,用户 spike 在他前面提到的评论中提到了这一点:

<块引用>

我将同步调用封装在一个 promise 中,以便能够将它们与其他 promise 链接起来.

你看,问题是then函数的第二个参数必须是一个返回promise的函数:

then :: 推迟一个 ->(a -> 延期 b) ->递延 b|_______________||-- 第二个参数是一个函数-- 返回一个承诺

这意味着第二个参数必须是异步的(因为它返回一个承诺).然而,有时我们可能希望用 then 链接一个同步函数.为此,我们将同步函数的返回值包装在一个 Promise 中.例如,这就是 spike 所做的:

//mapToInstruction :: Data ->延期教学//前一个promise的结果传入//下一步,因为我们正在链接.所以数据将包含//getScenario 的结果var mapToInstruction = 函数(数据){//我们将它映射到一个新的指令对象上var 指令 = {方法:data.endpoints[0].method,类型:data.endpoints[0].type,端点:data.endpoints[0].endPoint,频率:data.base.frequency};console.log('收到指令:');控制台日志(指令);//现在我们从这个创建一个 promise//指令,以便我们可以链接它var deferred = $.Deferred();deferred.resolve(指令);返回 deferred.promise();};

如您所见,mapToInstruction 函数的返回值是instruction.但是,我们需要将它包装在一个 promise 对象中,这就是我们这样做的原因:

//现在我们从这个创建一个 promise//指令,以便我们可以链接它var deferred = $.Deferred();deferred.resolve(指令);返回 deferred.promise();

事实上,他在handleResults函数中也做了同样的事情:

//handleResults :: Data ->延期单位var handleResults = 函数(数据){console.log("处理数据...");var deferred = $.Deferred();deferred.resolve();返回 deferred.promise();};

最好将这三行放在一个单独的函数中,这样我们就不必重复自己了:

//单位 :: a ->推迟一个功能单元(值){var deferred = $.Deferred();deferred.resolve(value);返回 deferred.promise();}

使用这个unit函数,我们可以重写mapToInstructionhandleResults如下:

//mapToInstruction :: Data ->延期教学//前一个promise的结果传入//下一步,因为我们正在链接.所以数据将包含//getScenario 的结果var mapToInstruction = 函数(数据){//我们将它映射到一个新的指令对象上var 指令 = {方法:data.endpoints[0].method,类型:data.endpoints[0].type,端点:data.endpoints[0].endPoint,频率:data.base.frequency};console.log('收到指令:');控制台日志(指令);返回单元(说明);};//handleResults :: 数据 ->延期单位var handleResults = 函数(数据){console.log("处理数据...");返回单元();};

事实上,unit 函数是 monad 接口中第二个缺失的操作.概括后,可以将其可视化如下:

unit :: Monad m =>->嘛

它所做的一切都是将一个值包装在一个 monad 数据类型中.这允许您将常规值和函数提升到一元上下文中.例如,promise 提供了一个异步上下文,而 unit 允许您将同步函数提升到这个异步上下文中.同样,其他 monad 也提供其他效果.

unit 与函数组合在一起,可以将函数提升到一元上下文中.例如,考虑我们之前定义的 isOdd 函数:

//isOdd :: Number ->标识布尔值函数 isOdd(n) {返回新的身份(n % 2 === 1);}

将其定义如下会更好(虽然速度较慢):

//奇数 :: Number ->布尔值函数奇数(n){返回 n % 2 === 1;}//单位 :: a ->身份一功能单元(值){返回新的身份(值);}//isOdd :: Number ->标识布尔值函数 idOdd(n) {返回单位(奇数(n));}

如果我们使用 compose 函数会更好看:

//compose :: (b -> c) ->(a -> b) ->->C//|______||______|//||函数 compose( f, g) {//compose(f, g) :: a ->C//|返回函数 ( x) {返回 f(g(x));};}var isOdd = compose(unit,odd);

我之前提到过,monad 是一个接口,没有实现(即它定义了某些操作并指定了这些操作应该做什么,而不指定必须如何做).因此,monad 是一个接口:

  1. 定义某些操作.
  2. 指定这些操作应该做什么.

我们现在知道monad的两个操作是:

bind :: Monad m =>m a ->(a -> m b) ->米单位 :: Monad m =>->嘛

现在,我们将看看这些操作应该做什么或它们应该如何表现(即我们将看看管理 monad 的法则):

//给定://x :: a//f :: Monad m =>->米//h :: Monad m =>嘛//g :: Monad m =>b->米//我们有以下三个定律://1. 左身份绑定(单位(x),f)=== f(x)单位(x).then(f) === f(x)//2. 正确的身份绑定(h,单位)===hh.then(unit) === h//3. 关联性bind(bind(h, f), g) === bind(h, function (x) { return bind(f(x), g); })h.then(f).then(g) === h.then(function (x) { return f(x).then(g); })

给定一个数据类型,我们可以为它定义thenunit 函数,这些函数违反了这些规则.在那种情况下,thenunit 的那些特定实现是不正确的.

例如,数组是一种表示非确定性计算的 monad.让我们为数组定义一个错误的 unit 函数(数组的 bind 函数是正确的):

//单位 :: a ->数组 a功能单元(x){返回 [x, x];}//concat :: Array (Array a) ->数组 a函数连接(h){返回 h.concat.apply([], h);}//绑定 :: 数组 a ->(a -> 数组 b) ->数组 b函数绑定(h,f){返回 concat(h.map(f));}

数组unit 的错误定义违反了第二定律(正确的身份):

//2. 正确的身份绑定(h,单位)===h//证明var h = [1,2,3];var lhs = bind(h, unit) = [1,1,2,2,3,3];var rhs = h = [1,2,3];lhs !== rhs;

数组unit的正确定义是:

//单位 :: a ->数组 a功能单元(x){返回 [x];}

一个有趣的特性是数组bind 函数是根据concatmap 实现的.然而,数组并不是唯一拥有这个属性的 monad.每个 monad bind 函数都可以根据 concatmap 的通用 monadic 版本来实现:

concat :: Array (Array a) ->数组 a加入 :: Monad m =>m (m a) ->嘛地图:: (a -> b) ->数组 a ->数组 bfmap :: 函子 f =>(a -> b) ->f a ->fb

如果您对 functor 是什么感到困惑,请不要担心.函子只是一种实现 fmap 函数的数据类型.根据定义,每个 monad 也是一个函子.

我不会深入了解 monad 法则的细节,以及 fmapjoin 如何等同于 bind.您可以在 维基百科页面上阅读有关它们的信息.

附带说明,根据 JavaScript Fantasy Land 规范单元 函数称为ofbind 函数称为chain.这将允许您编写如下代码:

Identity.of(1).chain(isOdd);

无论如何,回到你的主要问题:

<块引用>

使用 promise 的语法编写同步代码有什么好处吗?

是的,使用 promise 的语法(即 monadic 代码)编写同步代码可以获得很大的好处.许多数据类型都是 monad,使用 monad 接口,您可以对不同类型的顺序计算进行建模,例如异步计算、非确定性计算、失败计算、状态计算、记录计算等.我最喜欢的使用 monad 的例子之一是使用免费的monads来创建语言解释器.

Monads 是函数式编程语言的一个特性.使用 monad 可以促进代码重用.从这个意义上说,它绝对是好的.然而,这是有代价的.功能代码比过程代码慢几个数量级.如果这对您来说不是问题,那么您绝对应该考虑编写 monadic 代码.

一些更流行的 monad 是数组(用于非确定性计算),Maybe monad(用于可能失败的计算,类似于浮点数中的 NaN) 和 monadic 解析器组合器.

<块引用>

尝试{富();酒吧(a,b);巴姆();}赶上(e){句柄错误(e);}

...可以这样写(但使用 then 的同步版本);

foo().then(bar.bind(a, b)).然后(砰).fail(handleError)

是的,您绝对可以编写这样的代码.请注意,我没有提到关于 fail 方法的任何内容.原因是您根本不需要特殊的 fail 方法.

例如,让我们为可能失败的计算创建一个 monad:

function CanFail() {}//失败 :: f ->可以失败功能失败(错误){this.error = 错误}Fail.prototype = new CanFail;//好的 :: a ->可以失败功能好的(价值){this.value = 值;}好的.prototype = new CanFail;//然后 :: CanFail f a ->(a -> CanFail f b) ->CanFail f bCanFail.prototype.then = 函数 (f) {返回这个 instanceof 好吗?f(this.value) : 这个;};

然后我们定义foobarbamhandleError:

//foo :: Unit ->CanFail 数字布尔值函数 foo() {if (someError) return new Fail(1);否则返回新的好(真);}//bar :: String ->字符串 ->布尔值 ->CanFail 数字字符串功能栏(a,b){返回函数 (c) {if (typeof c !== "boolean") return new Fail(2);否则返回新的好的(c?a:b);};}//bam :: 字符串 ->CanFail 数字字符串功能 bam(s) {if (typeof s !== "string") return new Fail(3);否则返回新的 OK(s + "!");}//handleError :: Number ->单元函数句柄错误(n){开关 (n) {情况 1:警报(未知错误");休息;情况 2:警报(预期布尔值");休息;情况 3:警报(预期字符串");休息;}}

最后,我们可以这样使用它:

//结果 :: CanFail Number Stringvar 结果 = foo().then(bar("你好", "世界")).然后(砰);if (result instanceof OK)警报(结果.值);否则 handleError(result.error);

我描述的 CanFail monad 实际上是函数式编程语言中的 Either monad.希望有所帮助.

Is there such a concept as a synchronous promise? Would there be any benefit to writing synchronous code using the syntax of promises?

try {
  foo();
  bar(a, b);
  bam();
} catch(e) {
  handleError(e);
}

...could be written something like (but using a synchronous version of then);

foo()
  .then(bar.bind(a, b))
  .then(bam)
  .fail(handleError)

解决方案

Is there such a concept as a synchronous promise?

Benjamin is absolutely right. Promises are a type of monad. However, they are not the only type.

If you're not already aware of it then you're probably wondering what a monad is. There are lots of explanations of monads available online. However, most of them suffer from the monad tutorial fallacy.

In brief, the fallacy is that most people who understand monads don't really know how to explain the concept to others. In simple terms, monads are an abstract concept and human beings find it difficult to grasp abstract concepts. However, it's easy for humans to grok concrete concepts.

So let's begin our conquest to understand monads starting with a concrete concept. As I said, monads are an abstract concept. This means that a monad is an interface without an implementation (i.e. it defines certain operations and specifies what those operations should do, without specifying how it must be done).

Now, there are different types of monads. Each type of monad is concrete (i.e. it defines an implementation of the monad interface). Promises are a type of monad. Hence, promises are a concrete example of a monad. Thus, if we study promises then we can begin to understand monads.

So where do we begin? Fortunately, the user spike gave us a good starting point in his comment to your question:

One instance I can think of is chaining promises together with sync code. While finding an answer for this question: Generating AJAX Request Dynamically Based on Scenario I wrapped a synchronous call in a promise in order to be able to chain them with other promises.

So let's look at his code:

var run = function() {
    getScenario()
    .then(mapToInstruction)
    .then(waitForTimeout)
    .then(callApi)
    .then(handleResults)
    .then(run);
};

Here the run function returns a promise which is composed of the promises returned by getScenario, mapToInstruction, waitForTimeout, callApi, handleResults and run itself chained together.

Now, before we proceed I want to introduce to you a new notation to visualize what these functions are doing:

run              :: Unit        -> Deferred a
getScenario      :: Unit        -> Deferred Data
mapToInstruction :: Data        -> Deferred Instruction
waitForTimeout   :: Instruction -> Deferred Instruction
callApi          :: Instruction -> Deferred Data
handleResults    :: Data        -> Deferred Unit

So here's the breakdown:

  1. The :: symbol means “is of the type” and the -> symbol means “to”. Hence for example, run :: Unit -> Deferred a reads as run is of the type Unit to Deferred a.
  2. That means that run is a function which takes a Unit value (i.e. no arguments) and returns a value of type Deferred a.
  3. Here, a means any type. We don't know what type a is and we don't care what type a is. Hence, it can be any type whatsoever.
  4. Here, Deferred is a promise data type (with a different name) and Deferred a means that when the promise is resolved it yields a value of type a.

There are several things we can learn from the above visualization:

  1. Each function takes some value and returns a promise.
  2. The resolved value returned by each promise becomes the input to the next function:

    run              :: Unit -> Deferred a
    getScenario      ::                  Unit -> Deferred Data
    
    getScenario      :: Unit -> Deferred Data
    mapToInstruction ::                  Data -> Deferred Instruction
    
    mapToInstruction :: Data -> Deferred Instruction
    waitForTimeout   ::                  Instruction -> Deferred Instruction
    
    waitForTimeout   :: Instruction -> Deferred Instruction
    callApi          ::                         Instruction -> Deferred Data
    
    callApi          :: Instruction -> Deferred Data
    handleResults    ::                         Data -> Deferred Unit
    
    handleResults    :: Data -> Deferred Unit
    run              ::                  Unit -> Deferred a
    

  3. The next function cannot execute until the previous promise is resolved because it has to make use of the resolved value of the previous promise.

Now, as I mentioned earlier a monad is an interface which defines certain operations. One of the operations that the monad interface provides is the operation of chaining monads. In case of promises this is the then method. For example:

getScenario().then(mapToInstruction)

We know that:

getScenario      :: Unit -> Deferred Data
mapToInstruction :: Data -> Deferred Instruction

Hence:

getScenario()    :: Deferred Data -- because when called, getScenario
                                  -- returns a Deferred Data value

We also know that:

getScenario().then(mapToInstruction) :: Deferred Instruction

Thus, we can deduce:

then :: Deferred a -> (a -> Deferred b) -> Deferred b

In words, then is a function which takes two arguments (a value of the type Deferred a and a function of the type a -> Deferred b) and returns a value of type Deferred b.” Hence:

then          :: Deferred a    -> (a -> Deferred b) -> Deferred b
getScenario() :: Deferred Data

-- Therefore, since a = Data

getScenario().then :: (Data -> Deferred b)          -> Deferred b
mapToInstruction   ::  Data -> Deferred Instruction

-- Therefor, since b = Instruction

getScenario().then(mapInstruction) :: Deferred Instruction

So we got our first monad operation:

then :: Deferred a -> (a -> Deferred b) -> Deferred b

However, this operation is concrete. It is specific to promises. We want an abstract operation that can work for any monad. Hence, we generalize the function so that it can work for any monad:

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

Note that this bind function has nothing to do with Function.prototype.bind. This bind function is a generalization of the then function. Then then function is specific to promises. However, the bind function is generic. It can work for any monad m.

The fat arrow => means bounded quantification. If a and b can be of any type whatsoever then m can be of any type whatsoever which implements the monad interface. We don't care what type m is as long as it implements the monad interface.

This is how we would implement and use the bind function in JavaScript:

function bind(m, f) {
    return m.then(f);
}

bind(getScenario(), mapToInstruction);

How is this generic? Well, I could create a new data type which implements the then function:

// Identity :: a -> Identity a

function Identity(value) {
    this.value = value;
}

// then :: Identity a -> (a -> Identity b) -> Identity b

Identity.prototype.then = function (f) {
    return f(this.value);
};

// one :: Identity Number

var one = new Identity(1);

// yes :: Identity Boolean

var yes = bind(one, isOdd);

// isOdd :: Number -> Identity Boolean

function isOdd(n) {
    return new Identity(n % 2 === 1);
}

Instead of bind(one, isOdd) I could just have easily written one.then(isOdd) (which is actually much easier to read).

The Identity data type, like promises, is also a type of monad. In fact, it is the simplest of all monads. It's called Identity because it doesn't do anything to its input type. It keeps it as it is.

Different monads have different effects which make them useful. For example, promises have the effect of managing asynchronicity. The Identity monad however has no effect. It is a vanilla data type.

Anyway, continuing... we discovered one operation of monads, the bind function. There is one more operation that is left to be discovered. In fact, the user spike alluded to it in his aforementioned comment:

I wrapped a synchronous call in a promise in order to be able to chain them with other promises.

You see, the problem is that the second argument of the then function must be a function which returns a promise:

then :: Deferred a -> (a -> Deferred b) -> Deferred b
                      |_______________|
                              |
                    -- second argument is a function
                    -- that returns a promise

This implies that the second argument must be asynchronous (since it returns a promise). However, sometimes we may wish to chain a synchronous function with then. To do so, we wrap the return value of the synchronous function in a promise. For example, this is what spike did:

// mapToInstruction :: Data -> Deferred Instruction

// The result of the previous promise is passed into the 
// next as we're chaining. So the data will contain the 
// result of getScenario
var mapToInstruction = function (data) {
    // We map it onto a new instruction object
    var instruction = {
        method: data.endpoints[0].method,
        type: data.endpoints[0].type,
        endpoint: data.endpoints[0].endPoint,
        frequency: data.base.frequency
    };

    console.log('Instructions recieved:');
    console.log(instruction);

    // And now we create a promise from this
    // instruction so we can chain it
    var deferred = $.Deferred();
    deferred.resolve(instruction);
    return deferred.promise();
};

As you can see, the return value of the mapToInstruction function is instruction. However, we need to wrap it in a promise object which is why we do this:

// And now we create a promise from this
// instruction so we can chain it
var deferred = $.Deferred();
deferred.resolve(instruction);
return deferred.promise();

In fact, he does the same thing in the handleResults function as well:

// handleResults :: Data -> Deferred Unit

var handleResults = function(data) {
    console.log("Handling data ...");
    var deferred = $.Deferred();
    deferred.resolve();
    return deferred.promise();
};

It would be nice to put these three lines into a separate function so that we don't have to repeat ourselves:

// unit :: a -> Deferred a

function unit(value) {
    var deferred = $.Deferred();
    deferred.resolve(value);
    return deferred.promise();
}

Using this unit function we can rewrite mapToInstruction and handleResults as follows:

// mapToInstruction :: Data -> Deferred Instruction

// The result of the previous promise is passed into the 
// next as we're chaining. So the data will contain the 
// result of getScenario
var mapToInstruction = function (data) {
    // We map it onto a new instruction object
    var instruction = {
        method: data.endpoints[0].method,
        type: data.endpoints[0].type,
        endpoint: data.endpoints[0].endPoint,
        frequency: data.base.frequency
    };

    console.log('Instructions recieved:');
    console.log(instruction);

    return unit(instruction);
};

// handleResults :: Data -> Deferred Unit

var handleResults = function(data) {
    console.log("Handling data ...");
    return unit();
};

In fact, as it turns out the unit function is the second missing operation of the monad interface. When generalized, it can be visualized as follows:

unit :: Monad m => a -> m a

All it does it wrap a value in a monad data type. This allows you to lift regular values and functions into a monadic context. For example, promises provide an asynchronous context and unit allows you to lift synchronous functions into this asynchronous context. Similarly, other monads provide other effects.

Composing unit with a function allows you to lift the function into a monadic context. For example, consider the isOdd function we defined before:

// isOdd :: Number -> Identity Boolean

function isOdd(n) {
    return new Identity(n % 2 === 1);
}

It would be nicer (albeit slower) to define it as follows instead:

// odd :: Number -> Boolean

function odd(n) {
    return n % 2 === 1;
}

// unit :: a -> Identity a

function unit(value) {
    return new Identity(value);
}

// isOdd :: Number -> Identity Boolean

function idOdd(n) {
    return unit(odd(n));
}

It would look even nicer if we used a compose function:

// compose :: (b -> c) -> (a -> b) -> a -> c
//            |______|    |______|
//                |           |
function compose( f,          g) {

    // compose(f, g) :: a -> c
    //                  |
    return function (   x) {
        return f(g(x));
    };
}

var isOdd = compose(unit, odd);

I mentioned earlier that a monad is an interface without an implementation (i.e. it defines certain operations and specifies what those operations should do, without specifying how it must be done). Hence, a monad is an interface that:

  1. Defines certain operations.
  2. Specifies what those operations should do.

We now know that the two operations of a monad are:

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

unit :: Monad m => a -> m a

Now, we'll look at what these operations should do or how they should behave (i.e. we will look at the laws that govern a monad):

// Given:

// x :: a
// f :: Monad m => a -> m b
// h :: Monad m => m a
// g :: Monad m => b -> m c

// we have the following three laws:

// 1. Left identity

bind(unit(x), f)    === f(x)

unit(x).then(f)     === f(x)

// 2. Right identity

bind(h, unit)       === h

h.then(unit)        === h

// 3. Associativity

bind(bind(h, f), g) === bind(h, function (x) { return bind(f(x), g); })

h.then(f).then(g)   === h.then(function (x) { return f(x).then(g); })

Given a data type we can define then and unit functions for it that violate these laws. In that case those particular implementations of then and unit are incorrect.

For example, arrays are a type of monad that represent non-deterministic computation. Let's define an incorrect unit function for arrays (the bind function for arrays is correct):

// unit :: a -> Array a

function unit(x) {
    return [x, x];
}

// concat :: Array (Array a) -> Array a

function concat(h) {
    return h.concat.apply([], h);
}

// bind :: Array a -> (a -> Array b) -> Array b

function bind(h, f) {
    return concat(h.map(f));
}

This incorrect definition of unit for arrays disobeys the second law (right identity):

// 2. Right identity

bind(h, unit) === h

// proof

var h   = [1,2,3];

var lhs = bind(h, unit) = [1,1,2,2,3,3];

var rhs = h = [1,2,3];

lhs !== rhs;

The correct definition of unit for arrays would be:

// unit :: a -> Array a

function unit(x) {
    return [x];
}

An interesting property to note is that the array bind function was implemented in terms of concat and map. However, arrays are not the only monad that possess this property. Every monad bind function can be implemented in terms of generalized monadic versions of concat and map:

concat :: Array (Array a) -> Array a

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

map    :: (a -> b) -> Array a -> Array b

fmap   :: Functor f => (a -> b) -> f a -> f b

If you're confused about what a functor is then don't worry. A functor is just a data type that implements the fmap function. By definition, every monad is also a functor.

I won't get into the details of the monad laws and how fmap and join together are equivalent to bind. You can read about them on the Wikipedia page.

On a side note, according to the JavaScript Fantasy Land Specification the unit function is called of and the bind function is called chain. This would allow you to write code like:

Identity.of(1).chain(isOdd);

Anyway, back to your main question:

Would there be any benefit to writing synchronous code using the syntax of promises?

Yes, there are great benefits to be gained when writing synchronous code using the syntax of promises (i.e. monadic code). Many data types are monads and using the monad interface you can model different types of sequential computations like asynchronous computations, non-deterministic computations, computations with failure, computations with state, computations with logging, etc. One of my favourite examples of using monads is to use free monads to create language interpreters.

Monads are a feature of functional programming languages. Using monads promotes code reuse. In that sense it is definitely good. However, it comes at a penalty. Functional code is orders of magnitude slower than procedural code. If that's not an issue for you then you should definitely consider writing monadic code.

Some of the more popular monads are arrays (for non-deterministic computation), the Maybe monad (for computations that can fail, similar to NaN in floating point numbers) and monadic parser combinators.

try {
  foo();
  bar(a, b);
  bam();
} catch(e) {
  handleError(e);
}

...could be written something like (but using a synchronous version of then);

foo()
  .then(bar.bind(a, b))
  .then(bam)
  .fail(handleError)

Yes, you can definitely write code like that. Notice that I didn't mention anything about the fail method. The reason is that you don't need a special fail method at all.

For example, let's create a monad for computations that can fail:

function CanFail() {}

// Fail :: f -> CanFail f a

function Fail(error) {
    this.error = error
}

Fail.prototype = new CanFail;

// Okay :: a -> CanFail f a

function Okay(value) {
    this.value = value;
}

Okay.prototype = new CanFail;

// then :: CanFail f a -> (a -> CanFail f b) -> CanFail f b

CanFail.prototype.then = function (f) {
    return this instanceof Okay ? f(this.value) : this;
};

Then we define foo, bar, bam and handleError:

// foo :: Unit -> CanFail Number Boolean

function foo() {
    if (someError) return new Fail(1);
    else return new Okay(true);
}

// bar :: String -> String -> Boolean -> CanFail Number String

function bar(a, b) {
    return function (c) {
        if (typeof c !== "boolean") return new Fail(2);
        else return new Okay(c ? a : b);
    };
}

// bam :: String -> CanFail Number String

function bam(s) {
    if (typeof s !== "string") return new Fail(3);
    else return new Okay(s + "!");
}

// handleError :: Number -> Unit

function handleError(n) {
    switch (n) {
    case 1: alert("unknown error");    break;
    case 2: alert("expected boolean"); break;
    case 3: alert("expected string");  break;
    }
}

Finally, we can use it as follows:

// result :: CanFail Number String

var result = foo()
            .then(bar("Hello", "World"))
            .then(bam);

if (result instanceof Okay)
    alert(result.value);
else handleError(result.error);

The CanFail monad that I described is actually the Either monad in functional programming languages. Hope that helps.

这篇关于使用 promise 的语法编写同步代码有什么好处吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆