reason442_如何开始使用Reason

reason442

In this article, we will build a scheduler in Reason. Along the way, we will see how some of the core features of Reason interact with each other and make it an excellent fit for this project. You can find everything we cover here in the repository.

在本文中,我们将在Reason中构建一个调度程序。 在此过程中,我们将看到Reason的某些核心功能如何相互影响,使其非常适合该项目。 您可以在存储库中找到我们涵盖的所有内容。

Most articles about Reason show how it works in ReasonReact. This makes sense, since Facebook developed Reason. In this article, however, I wanted to show how Reason shines as a language outside of ReasonReact.

关于Reason的大多数文章都显示了ReasonReact是如何工作的。 这是有道理的,因为Facebook开发了Reason。 但是,在这篇文章中,我想展示一下Reason是如何作为ReasonReact之外的一种语言发光的。

This article assumes you have a basic to intermediate understanding of JavaScript. Some familiarity with Functional Programming wouldn’t hurt either.

本文假定您对JavaScript有基本的了解。 对函数式编程的一些熟悉也不会受到损害。

为什么选择理性? (Why choose Reason?)

Reason is a functional language, that encourages immutability, provides an inferred static type system, and compiles down to JavaScript. Let’s take a closer look:

Reason是一种功能语言,它鼓励不变性,提供推断的静态类型系统并编译为JavaScript。 让我们仔细看看:

  1. Reason and OCaml share the same semantics. And so the functional programming constructs available in OCaml such as pattern matching and currying directly translate to Reason.

    原因和OCaml共享相同的语义。 因此,OCaml中可用的功能编程结构(例如模式匹配和currying)直接转换为Reason。
  2. In Reason, almost always you don’t have to write down the types — the compiler infers the types for you. For example, the compiler sees this () => {1 + 1} as a function that takes a unit (no argument) and returns an int.

    因此,几乎不必总是写下类型-编译器会为您推断类型。 例如,编译器看到这个() => {1 + 1}为取一个函数au NIT(无参数),并返回an int值。

  3. Most constructs in Reason are immutable. List is immutable. Array is mutable but has fixed size. Adding a new element to an array returns a copy of the array extended with the new element. Records (similar to JavaScript objects) are immutable.

    原因中的大多数构造都是不可变的。 List是不可变的。 Array是可变的,但大小固定。 向数组添加新元素将返回使用新元素扩展的数组的副本。 Record (类似于JavaScript对象)是不可变的。

  4. BuckleScript compiles Reason down to JavaScript. You can work with JavaScript in your Reason code and use your Reason modules in JavaScript.

    BuckleScript将Reason编译为JavaScript。 您可以在原因代码中使用JavaScript,并在JavaScript中使用原因模块。

Reason brings the benefits of a strongly typed language to a JavaScript at a low cost. You should definitely read the What and Why section of the documentation, as it provides more context into the language and its features.

Reason以低成本将强类型语言的优点带给了JavaScript。 您绝对应该阅读本文档的“ 什么和为什么”部分,因为它为该语言及其功能提供了更多的上下文。

一些帮助您入门的资源 (Some resources to help you get started)

  1. Reason’s official docs are simple and to the point

    Reason的官方文档很简单,而且很直截了当

  2. Exploring ReasonML, a book by Dr. Axel Rauschmayer, explores Reason in a more practical way

    Axel Rauschmayer博士的书《 Explore ReasonML 》以更实际的方式探索了Reason

  3. BuckleScript docs talks in detail about interoperability with JavaScript and OCaml

    BuckleScript文档详细讨论了与JavaScript和OCaml的互操作性

In this article, we will explore how different concepts in Reason such as Modules, Statements, Variable Bindings and Immutability work together. Whenever I introduce a new concept or syntax, I will link to the related docs and articles.

在本文中,我们将探讨Reason中的不同概念(例如模块,语句,变量绑定和不可变性)如何协同工作。 每当我引入新的概念或语法时,我都会链接到相关的文档和文章。

大图 (The big picture)

This tutorial was inspired by Node Schedule, a scheduler for Node.js that uses a single timer at all times. You can learn more about how Node Schedule works here.

本教程的灵感来自于Node Schedule ,它是Node.js的调度程序 ,该调度程序始终使用单个计时器。 您可以在此处了解有关节点计划的更多信息。

Today we are going to create a scheduler in Reason that uses a single timer at all times. We will use our scheduler to execute recurring jobs. This project is just large enough to demonstrate some of the key concepts in Reason.

今天,我们将在Reason中创建一个计划程序,该计划程序始终使用单个计时器。 我们将使用调度程序执行重复作业。 该项目足够大,足以演示Reason中的一些关键概念。

To achieve this, we will define two modules — a Heap and a Scheduler.

为此,我们将定义两个模块-堆和调度程序。

Heap is an implementation of a priority queue. It keeps the jobs in the order they should be executed next. The key of a heap element is the next invocation time of the job.

堆是优先级队列的实现。 它使作业保持应在下一次执行的顺序。 堆元素的关键字是作业的下一次调用时间。

Scheduler is composed of a heap and is responsible for updating the timer and executing the jobs by the specified recurrence rules.

调度程序由堆组成,负责根据指定的重复规则更新计时器并执行作业。

  1. When a job executes, the scheduler will remove the job from the queue, calculates its next invocation time, and inserts the job back to the queue with its updated invocation time.

    执行作业时,调度程序将作业从队列中删除,计算其下一个调用时间,并将作业及其更新后的调用时间插入到队列中。
  2. When a new job is added, the scheduler checks the next invocation time of the root (head / the job that will be executed next). If the new job should be executed before the head, the scheduler updates the timer.

    添加新作业后,调度程序将检查根的下一个调用时间(头/下一个将要执行的作业)。 如果新作业应在头之前执行,则调度程序将更新计时器。

堆模块 (Heap module)

The API of a priority queue defines:

优先级队列的API定义:

  1. Inserting a new element to the queue with a key representing its priority

    使用代表其优先级的键将新元素插入队列
  2. Extracting the element with the highest priority

    提取优先级最高的元素
  3. Size of the queue

    队列大小

Heap performs insert and extract operations in order O(log(n)) where n is the size of the queue.

堆按O(log(n))顺序执行insertextract操作,其中n是队列的大小。

Note: We will talk about algorithm complexity in the last section of the article. If you’re not comfortable with algorithm complexity, you can ignore the last section.

注意:我们将在本文的最后一部分中讨论算法复杂性。 如果您对算法复杂性不满意,可以忽略最后一部分。

If you’re not comfortable with the Heap data structure or need a refresher, I recommend watching the following lecture from MIT OCW 6006 course. In the remaining of this section, we will implement the pseudocode outlined in the lecture notes of 6006.

如果您对Heap数据结构不满意或需要复习,建议您观看MIT OCW 6006课程的以下讲座。 在本节的其余部分,我们将实现6006 讲义中概述的伪代码。

定义堆模块使用的类型 (Defining the types used by the heap module)

heapElement defines a record type. Similar to a JavaScript object, you can access record fields by name. { key: 1, value: "1" } creates a value of type heapElement(int, string).

heapElement定义记录类型。 与JavaScript对象类似,您可以按名称访问记录字段。 { key: 1, value: "1" }创建一个类型为heapElement(int, string)

t('a, 'b) is another record type and represents the Heap. This is the return type of our create function and the last parameter passed to all the other functions in the public API of our heap module.

t('a, 'b)是另一种记录类型,代表堆。 这是我们的create函数的返回类型,也是最后一个参数传递给我们的堆模块的公共API中的所有其他函数。

To maintain the max heap property, Heap only needs to compare the keys of the elements in the array. Hence, we can hide the type of key from the Heap by providing a comparison function compare that returns true when its first argument has a higher priority than the second one.

要维护maxheap属性,Heap只需要比较数组中元素的键。 因此,我们可以通过提供一个比较函数compare来从Heap中隐藏键的类型,当第一个参数的优先级高于第二个参数时,该比较函数返回true。

This is the first time we see ref. ref is Reason’s way for supporting mutations. You can have a ref to a value and update that ref to point to a new value by using the := operator.

这是我们第一次看到refref是Reason支持突变的方式。 您可以使用:=运算符来ref一个值,然后将该ref更新为指向新值。

Arrays in Reason are mutable — You can update a value at a specific index. However, they have a fixed length. To support addition and extraction our heap needs to hold onto a ref to an array of heap elements. If we don’t use a reference here, we will end up having to return a new heap after every addition and extraction. And the modules that depend on the heap need to keep track of the new heap.

原因数组是可变的-您可以在特定索引处更新值。 但是,它们的长度是固定的。 为了支持加法和提取,我们的堆需要保持对堆元素数组的ref 。 如果我们在这里不使用引用,那么每次添加和提取之后,我们最终都必须返回一个新堆。 并且依赖于堆的模块需要跟踪新堆。

exception can be extended with new constructors. We will raise EmptyQueue exception later in the extract and head functions in the heap module.

exception可以使用新的构造函数进行扩展。 稍后,我们将在堆模块的extracthead函数中raise EmptyQueue异常。

Exceptions are all of the same type, exn. The exn type is something of a special case in the OCaml type system. It is similar to the variant types we encountered in Chapter 6, Variants, except that it is open, meaning that it's not fully defined in any one place. — RealWorldOcaml

异常都是相同的类型, exnexn类型是OCaml类型系统中的一种特殊情况。 它与我们在第6章“变体”中遇到的变体类型相似,但它是开放的,这意味着它在任何地方都没有完全定义。 — RealWorldOcaml

签名 (Signature)

By default, all the bindings (variable assignments) in a module are accessible everywhere even outside the module where they are defined. signature is the mechanism by which you can hide the implementation specific logic and define an API for a module. You can define a signature in a file with the same name as the module ending with .rei suffix. For example you can define the signature for the Heap.re in the Heap.rei file.

默认情况下, 模块中的所有绑定(变量分配)都可以在任何地方访问,即使在定义它们的模块之外也是如此。 signature是一种机制,通过它可以隐藏实现特定的逻辑并为模块定义API。 您可以在文件中定义签名,其名称与以结尾的模块相同。 rei后缀。 例如,您可以在Heap.rei文件中定义Heap.re的签名。

Here, we are exposing the definition of heapElement so the users of the Heap module can use the value returned by head and extract. But we are not providing the definition for t our heap type. This makes t an abstract type which ensures that only functions within the Heap module can consume a heap and transform it.

在这里,我们公开了heapElement的定义,以便Heap模块的用户可以使用headextract返回的值。 但是,我们不提供定义t我们的堆型。 这使得t抽象类型 ,可确保只有Heap模块中的函数才能使用堆并对其进行转换。

Every function except create takes as argument a heap. create takes a comparison function and creates an empty Heap.t that can be consumed by the other functions in the Heap module.

create之外的每个函数都将堆作为参数。 create需要一个比较函数并创建一个空的Heap.t ,Heap模块中的其他函数可以使用该Heap.t

辅助功能 (Helper functions)

parent is a function that takes a single argument — index. It returns None when the index is 0. index 0 indicates the root of the tree, and the root of a tree doesn’t have a parent.

parent是带有单个参数(索引)的函数。 当索引为0时,它返回None 。 索引0表示树的根,树的根没有父级。

left and right return the index of the left and the right child of a node.

leftright返回节点的左,右子节点的索引。

swap takes two indexes a and b and an array queue. It then swaps the values in the index a and b of the queue.

swap需要两个索引ab以及一个数组queue 。 然后,它交换queue索引ab中的值。

key simply returns the key field of a heapElement at the specified index in the queue.

key只是返回队列中指定索引处的heapElement的键字段。

size returns the length of the queue

size返回队列的长度

(Add)

add is one of the primary functions we exposed in the heap signature. It takes a value and a key representing the priority of the value to insert into the queue. We will use this function later in the Scheduler module to add new jobs to our execution queue.

add是我们在heap签名中公开的主要功能之一。 它需要一个值和一个代表该值优先级的键插入队列。 稍后,我们将在Scheduler模块中使用此功能将新作业添加到我们的执行队列中。

let rec lets us define recursive functions. With rec you can refer to the function name inside the function body.

let rec让我们定义递归函数。 使用rec可以在函数体内引用函数名称。

We defined key as a function that takes a queue and index as arguments. With the declaration let key = key(queue) we are shadowing key by partially applying the helper function key we defined previously.

我们将key定义为一个以queueindex为参数的函数。 通过声明let key = key(queue)我们通过部分应用我们先前定义的辅助功能key遮盖 key

When you provide a subset of the arguments to a function, it returns a new function that takes the remaining arguments as input — this is known as currying.

当您为函数提供参数的子集时,它将返回一个新函数,该函数将其余参数作为输入-称为currying

The arguments you provided are available to the returned function. Since queue is fixed in fix_up, we partially apply it to the key function to make our code more DRY.

您提供的参数可用于返回的函数。 由于queuefix_up是固定的, fix_up我们将其部分应用于key函数,以使我们的代码更加DRY

You can use <case>; when <condition> to specify additional conditions in pattern matching. The value bindings in the case are available to the expression following when (in our example p_ind is available in compare(key(index), key(p_ind)). Only when the condition is satisfied we execute the associated statement after the =>.

您可以使用<cas e> ; when <c <cas e> ; when <c ondition>在模式匹配中指定其他条件时。 值绑定ings的情况下可向expressio n fo时llowing(在our e xample p_ind是vailable in compare(key(index),钥匙(p_ind))。只有当条件满足时,我们执行相关的statemen =>之后的t

add concatenates a new element to the end of the queue. If the new element has higher priority than its parent, it is violating the max heap property. fix_up is a recursive function that restores the max heap property by moving the new element up in the tree (pairwise swapping with its parent) until it reaches the root of the tree or its priority is lower than its parent.

add将一个新元素连接到队列的末尾。 如果新元素的优先级高于其父元素,则违反了最大堆属性。 fix_up是一个递归函数,它通过在树中上移新元素(与父元素成对交换),直到到达树的根或其优先级低于其父元素,来还原最大堆属性。

fix_last is just wrapper around fix_up and calls it with the index of the last element in the queue.

fix_last只是包装围绕fix_up并在队列中的最后一个元素的索引调用它。

heap.queue^ is how we access the value ref references.

heap.queue^是我们访问值ref引用的方式。

[||] is the array literal syntax for an empty array.

[||]是空数组的数组文字语法。

提取 (Extract)

extract removes the element with the highest priority (in our case, the element with the smallest key) from the queue and returns it. extract removes the head of the queue by first swapping it with the last element in the array. This introduces a single violation of the max heap property at the root/head of the queue.

extract从队列中删除优先级最高的元素(在我们的示例中为键最小的元素)并返回它。 extract首先通过与数组中的最后一个元素交换来删除队列的头部。 这在队列的根/头处引入了对最大堆属性的单次违反。

As described in the lecture, heapify — also known as sift-down— fixes a single violation. Assuming the left and right subtrees of node n satisfy the max heap property, calling heapify on n fixes the violation.

如讲座中所述, heapify (也称为sift-down )可修复单个冲突。 假设节点n的左子树和右子树满足heapify属性, heapifyn上调用heapify可解决此冲突。

Each time heapify is called, it finds the max_priority_index index of the highest priority element between the heapElements at the index, left(index), and the right(index). If the max_priority_index is not equal to the index, we know there is still a violation of the max heap property. We swap the elements at the index and max_priority_index to fix the violation at index. We recursively call heapify with the max_priority_index to fix the possible violation we might create by swapping the two elements.

每次heapify被调用时,它找到max_priority_index的在heapElements之间的最高优先级的元素的索引indexleft(index) ,和right(index) 。 如果max_priority_index不等于index ,我们知道仍然存在违反max heap属性的情况。 我们在indexmax_priority_index处交换元素以修复index处的冲突。 我们以max_priority_index递归调用heapify ,以解决可能通过交换两个元素而造成的违反情况。

index is an int representing the root of a subtree that violates the max heap property, but its subtrees satisfy the property. compare is the comparison function defined with the heap. queue is an array that holds the heap elements.

index是一个int表示违反最大堆属性的子树的根,但其子树满足该属性。 compare是用堆定义的比较函数。 queue是保存堆元素的数组。

if statements in Reason like the other expressions evaluate to a value. Here the if statements evaluate to an int that represents which index was smaller in the comparison.

与其他表达式一样,Reason中的if 语句求值。 在这里, if语句的计算结果为一个int ,该int表示比较中哪个索引较小。

extract pattern matches against queue (the array not the reference).

extractqueue匹配的模式(数组不是引用)。

[|head|] only matches an array with a single element.

[|head|]仅将具有单个元素的数组匹配。

When the queue is empty [||] we raise the EmptyQueue exception we defined previously. But why? Why don’t we return None instead? Well this is a matter of preference. I prefer to raise an exception, because when I use this function, I will get a heapElement and not a option(heapElement). This saves me pattern matching against the returned value of the extract. The caveat is that you need to be careful when you use this function, making sure the queue is never empty.

当队列为空[||]我们引发先前定义的EmptyQueue异常。 但为什么? 为什么我们不返回None呢? 好吧,这是一个优先事项。 我更喜欢raise一个异常,因为当我使用此函数时,将得到一个heapElement而不是option(heapElement) 。 这可以节省与extract的返回值匹配的模式。 需要注意的是,使用此功能时需要小心,确保queue永远不会为空。

When we have more than one element, we swap the first and the last element of the queue, remove the last element and call heapify on the first element (the root of the tree).

当我们有多个元素时,我们交换队列的第一个和最后一个元素,删除最后一个元素,并在第一个元素(树的根)上调用heapify

测试中 (Testing)

We use bs-jest — BuckleScript bindings for Jest — to write tests. Jest is a testing framework created by Facebook that comes with Built-in mocking library and code coverage reports.

我们使用bs-jest ( Jest BuckleScript绑定)编写测试。 Jest是Facebook创建的测试框架,带有内置的模拟库和代码覆盖率报告。

  1. https://github.com/glennsl/bs-jest

    https://github.com/glennsl/bs-jest

  2. https://facebook.github.io/jest/docs/en/getting-started.html

    https://facebook.github.io/jest/docs/en/getting-started.html

Follow the instructions in bs-jest to set up Jest.

按照bs-jest中的说明设置Jest

Make sure to add @glennsl/bs-jest to bs-dev-dependencies in your bsconfig.json. Otherwise BuckleScript won’t find the Jest module and your build will fail.

确保添加@glennsl/bs-jestbs-dev-dependencies于你的bsconfig.json 。 否则,BuckleScript将找不到Jest模块,并且您的构建将失败。

If you’re writing your test cases in a directory other than src you have to specify it in the sources in the bsconfig.json for the BuckleScript compiler to pick them up.

如果要在src以外的目录中编写测试用例,则必须在bsconfig.jsonsources中指定它,以便BuckleScript编译器可以使用它们。

测试同步功能 (Testing synchronous functions)

With the Heap module in place and Jest installed, we are ready to write our first test case.

安装了Heap模块并安装了Jest ,我们就可以编写第一个测试用例了。

To test our Heap module, we will do a heap sort.

为了测试我们的Heap模块,我们将进行堆排序。

  1. create a heap

    创建一个堆
  2. insert elements into the heap

    将元素插入堆
  3. use the extract operation to remove the elements in the ascending order

    使用extract操作以升序删除元素

open Jest opens the module so we can refer to the bindings available in the Jest module without prepending them with Jest.. For example, instead of writing Jest.expect we can just write expect.

open Jest将打开模块,因此我们可以引用Jest模块中可用的绑定,而无需在它们之前添加Jest. 。 例如,而不是写Jest.expect我们可以只写expect

We use let {value: e1} = to destructure the value returned by extract and create an alias e1 for valuee1 is now bound to the value field of the value returned by extract.

我们使用let {value: e1} =以解构返回的值extract和创建别名e1value - e1被绑定到value返回的值的字段extract

With the|&gt; pipe operator we can create a composite function and apply the resulting function immediately on an input. Here we simply pass the result of calling expect with (e1, ..., e9) to the toEqual function.

|& g t; 管道操作员,我们可以创建一个复合函数,并将结果函数立即应用于输入。 在这里,我们简单地传递愈伤组织的结果ng exp ECT的Wi th (e1, ..., E9)到t he toEq UAL功能。

调度程序模块 (Scheduler module)

Scheduler uses the Heap module to maintain a list of recurrent jobs sorted by their next invocation time.

调度程序使用堆模块维护一个按其下一次调用时间排序的循环作业列表。

让我们定义调度程序模块中使用的类型 (Let’s define the types used in the Scheduler module)

recurrence is a Variant type. Any value of the recurrence type can either be a Second, Minute, or an Hour. Second, Minute and Hour are the constructors for the recurrence. You can invoke a constructor like a normal function and get back a value of the Variant type. In our case, if you call Second with an int you get back a value of type recurrence. You can pattern match this value with Second(number_of_seconds) to access the argument that was passed to the Second constructor.

recurrenceVariant类型。 recurrence类型的任何值都可以是SecondMinuteHourSecondMinuteHourrecurrence的构造函数。 您可以像正常函数一样调用构造函数,并获取Variant类型的值。 在我们的例子中,如果您使用int调用Second ,则将返回类型为recurrence的值。 您可以将此值与Second(number_of_seconds)进行模式匹配,以访问传递给Second构造函数的参数。

job is a record type. period is of type recurrence and indicates the delay between each execution of a job. invoke is a function that takes unit (no argument) and returns unit (no result). invoke is the function that gets executed when the job runs.

job记录类型periodrecurrence类型,表示每次执行作业之间的延迟。 invoke是一个接受unit (无参数)并返回unit (无结果)的函数。 invoke是作业运行时执行的功能。

t is a record type representing the scheduler. A scheduler holds onto a queue of jobs sorted by their next invocation time. timer_id references the timerId for the first job in the queue — the job that will be invoked first.

t是代表调度程序的记录类型。 调度程序保留按其下一次调用时间排序的作业queuetimer_id引用queue第一个作业(将首先调用的作业)的timerId

互操作 (Interop)

You can invoke JavaScript functions from within Reason. There are different ways of doing this:

您可以从Reason中调用JavaScript函数。 有不同的方法:

  1. you can use BuckleScript bindings if available, such as Js.log, and Js.Global.setTimeout

    您可以使用BuckleScript绑定(如果可用),例如Js.logJs.Global.setTimeout

  2. declare an external such as [@bs.val] external setTimeout

    声明一个external例如[@bs.val] external setTimeout

  3. execute raw JavaScript code with [%raw ...]

    [%raw ...]执行原始JavaScript代码

Bindings for most JavaScript functions is provided by the BuckleScript. For example, Js.Date.getTime takes a Js.Date.t — a date value — and returns the number of milliseconds since epoch. Js.Date.getTime is the binding for the getTime method of the the JavaScript Date object. Js.Date.getTime returns a float value.

BuckleScript提供了大多数JavaScript函数的绑定。 例如, Js.Date.getTime接受一个Js.Date.t (一个date值),并返回自纪元以来的毫秒数。 Js.Date.getTime是JavaScript Date对象的getTime方法的绑定。 Js.Date.getTime返回一个float值。

Using bucklescript bindings is exactly the same as using user-defined modules. You can read more about the available bindings here. For the rest of this section we will focus on external and [%raw ...].

使用扣脚本绑定与使用用户定义的模块完全相同。 您可以在此处阅读有关可用绑定的更多信息。 在本节的其余部分,我们将重点介绍external[%raw ...]

外部 (external)

With external you can bind a variable to a JavaScript function. Here for example we are binding setTimeout variable to JavaScript’s setTimeout global function.

使用external您可以将变量绑定到JavaScript函数。 例如,在这里我们将setTimeout变量绑定到JavaScript的setTimeout全局函数。

setTimeout returns a float, an identifier that we can pass to clearTimeout to cancel the timer. The only function that uses the value returned by the setTimeout is clearTimeout. So we can define the value returned by setTimeout to have an abstract type. This ensures that only a value returned by setTimeout can be passed to clearTimeout.

setTimeout返回一个float ,我们可以将其传递给clearTimeout来取消计时器的标识符。 使用setTimeout返回的值的唯一函数是clearTimeout 。 因此,我们可以将setTimeout返回的值定义为抽象类型 。 这样可以确保只有setTimeout返回的值可以传递给clearTimeout

[%生的 …] ([%raw …])

new Date.getTime() in JavaScript returns an integer Number. Numbers in JavaScript are 64bit long. int in Reason are only 32bit long. This is a problem!

JavaScript中的new Date.getTime()返回整数。 JavaScript中的数字长64位int 仅32bit长 。 这是个问题!

In Reason, we can work with the returned value of new Date.getTime() by expecting it to be Float. This is actually the expected return type of Js.Date.getTime provided by BuckleScript.

在Reason中,我们可以期望new Date.getTime()的返回值是Float 。 这实际上是Js.Date.getTime提供的Js.Date.getTime的预期返回类型。

Instead, let’s use [%raw ...] and create an abstract type long similar to what we did for setTimeout. In doing this, we are hiding the implementation of long. Our Reason code can pass values of type long around, but it can’t really operate on them. For this we are defining a set of helper bindings that take values of type long and delegate the computation to raw JavaScript expressions.

相反,让我们使用[%raw ...]并创建一个抽象类型long类似于我们做了setTimeout 。 这样,我们隐藏了long的实现。 我们的原因代码可以long传递类型的值,但实际上不能对其进行操作。 为此,我们定义了一组辅助绑定,这些绑定采用long类型的值并将计算委托给原始JavaScript表达式。

We can define a JavaScript expression with [%raw ...]. Here we are defining an abstract type long and a set of functions that consume and return values of type long. The type of all the expressions is specified in the let bindings.

我们可以使用[%raw ...]定义一个JavaScript表达式。 在这里,我们定义了一个long 抽象类型 long以及一组使用和返回long类型值的函数。 所有表达式的类型在let绑定中指定。

time_now returns the number of milliseconds since epoch.

time_now返回自纪元以来的毫秒数。

We use sum to calculate the next invocation time of a job, by passing in the result of time_now and an int representing how many milliseconds from now the job should be executed.

我们使用sum来计算作业的下一个调用时间,方法是传入time_now的结果和一个int ,该int表示从现在开始应执行多少毫秒。

We can compute how long from now a job will be invoked by subtracting the invocation time of a job from time_now. The result of subtract is passed to the setTimeout.

通过从time_now subtract作业的调用时间,我们可以计算从现在开始将调用多长时间。 subtract的结果传递到setTimeout

has_higher_priority compares two invocation times. This is the comparison function we use to initialize our Heap.

has_higher_priority比较两个调用时间。 这是我们用来初始化堆的比较函数。

调用方式 (Invocation)

At any point in time, we only have a single timer that expires when the first job in the queue should run. When the timer expires, we need to do some cleanup. When the timer expires, we should

在任何时间点,只有一个计时器在队列中的第一个作业应运行时到期。 当计时器到期时,我们需要进行一些清理。 当计时器到期时,我们应该

  1. extract the first job from the queue

    从队列中提取第一个作业
  2. calculate its next invocation time (a new key for the job)

    计算其下一次调用时间(作业的新密钥)
  3. insert the job back into the queue with its updated key

    使用更新后的密钥将作业重新插入队列
  4. look at the head of the queue to find the job that should be executed next and

    查看队列的开头以查找下一步应执行的作业,然后
  5. create a new timer for this job

    为此工作创建一个新的计时器

wait takes a period — a value of type recurrence — and returns an int representing how many milli-seconds a job has to wait before getting executed again. We pass the value returned by wait to the setTimeout.

wait需要一个时间段(一个recurrence类型的值),并返回一个整数,该整数表示作业在再次执行之前必须等待的毫秒数。 我们将wait返回的值传递给setTimeout

next_invocation calculates the next invocation time of a job. time_now returns a long value. sum takes in a long and an int value and returns a long value. sum adds the two number by calling the JavaScript + operator on its arguments.

next_invocation计算作业的下一次调用时间。 time_now返回一个long值。 sum接受一个long和一个int值,并返回一个long值。 sum通过在参数上调用JavaScript +运算符将两个数字sum

execute is a recursive function that is responsible for executing the job and doing the cleanup. It captures the scheduler in a closure and returns a function that can be invoked when the timer expires.

execute是一个递归函数,负责执行作业和进行清理。 它在一个闭包中捕获调度程序,并返回一个可在计时器到期时调用的函数。

In the first three lines, we remove the job with the highest priority (lowest key or closest invocation time) and insert it back into the queue with its next invocation time.

在前三行中,我们删除优先级最高的作业(最低键或最短的调用时间),并将其下一个调用时间插入到队列中。

We then go on to create a new timer for the job at the head of the queue (the next job that should be executed after this invocation). We update the timer_id reference to point to the new timerId.

然后,我们继续在队列的开头为作业创建新的计时器(此调用后应执行的下一个作业)。 我们更新timer_id引用以指向新的timerId

Finally, we call the invoke field of the job to perform the specified task.

最后,我们调用作业的invoke域来执行指定的任务。

新增工作 (Add a new job)

When the queue is empty, adding a new job is simple. We create a timer that expires at the next invocation time of the job.

queue为空时,添加新作业很简单。 我们创建一个计时器,该计时器在作业的下一次调用时间到期。

The more interesting case is when the queue is not empty! We can have two situations here. Either the head of the queue has a key greater than the next invocation time of the job or not.

更有趣的情况是队列不为空! 我们在这里可以有两种情况。 queue头的密钥是否大于作业的下一次调用时间。

The first case is when the head of the queue has a key less than or equal to the next invocation time of the job. This is the case when the new job needs to be executed before the current timer. In this case, we need to cancel the timer by calling clearTimeout with the timer_id and create a new timer that will expire at the next invocation time of the new job.

第一种情况是queue的头中的键小于或等于作业的下一个调用时间。 当需要在当前计时器之前执行新作业时,就是这种情况。 在这种情况下,我们需要通过使用timer_id调用clearTimeout来取消计时器,并创建一个新计时器,该计时器将在新作业的下一次调用时间到期。

In the other case, because the new job needs to be executed after the current timer expires, we can just insert the new job in the queue.

在另一种情况下,由于新作业需要在当前计时器到期后执行,因此我们只需将新作业插入queue

测试异步功能 (Testing asynchronous functions)

All the functions in the heap module are synchronous. For example, when you call add, you are blocked until a new heapElement has been added to the queue. When add returns, you know that the heap has been extended with the new element.

堆模块中的所有功能都是同步的 。 例如,当您调用add ,您将被阻塞,直到将新的heapElement添加到队列中为止。 当add返回时,您知道堆已使用新元素进行了扩展。

The functions in the scheduler, on the other hand, have asynchronous side effects. When you add a new job to the scheduler, the scheduler adds the job to its queue and returns. Later, according to the recurrence rule the job gets invoked. Your code doesn’t wait for the job to get invoked, and continues executing.

另一方面,调度程序中的函数具有异步副作用。 当您add新作业add到调度程序时,调度程序将作业添加到其队列中并返回。 后来,根据recurrence规则,该作业被调用。 您的代码不会等待作业被调用,而是继续执行。

Now, lets write a test case to ensure that when a job is added to the scheduler, it gets invoked according to its recurrence rule.

现在,让我们编写一个测试用例,以确保将作业添加到调度程序后,它会根据其重复规则被调用。

To do this we will

为此,我们将

  1. add a job to the scheduler to be executed every second. This job increments a ref(int) counter.

    add作业add到调度程序以每秒执行一次。 此作业将增加一个ref(int)计数器。

  2. create a Promise that gets resolved after 4s

    创建一个在4秒后解决的Promise

  3. return a Jest.assertion promise that expects the counter to have been incremented 4 times.

    返回一个Jest.assertion承诺,该承诺期望计数器已递增4次。

We can use testPromise to test promises. testPromise expects a Js.Promise.t(Jest.assertion). Look at the last line of the test case.

我们可以使用testPromise来测试promise。 testPromise需要一个Js.Promise.t(Jest.assertion) 。 查看测试用例的最后一行。

Scheduler.Second(1) indicates we want our job to execute every second.

Scheduler.Second(1)表示我们希望作业每秒钟执行一次。

counter is a ref and everytime invoke is called, it gets incremented.

counter是一个ref ,每次invoke ,它都会递增。

promise is a Js.Promise.t that will get resolved after 4s. Notice that we are waiting for 4.1s to make sure the last call to the invoke has finished executing. Otherwise, we might resolve the promise when we have only incremented the counter three times.

promise是一个Js.Promise.t ,它将在4秒后解决。 请注意,我们正在等待4.1s以确保对调用的最后一次invoke已完成执行。 否则,当我们仅将计数器增加3次时,我们可能会解决诺言。

You can use |&gt; to chain promises. In our example, promise will resolve with the value of the counter after 4s. This value is provided as the count to the function passed to the Js.Promise.then_.

您可以使用|& gt; 兑现承诺。 在我们的示例中e, prom承诺将在4秒后以计数器的值解决。 这个值是为t提供he co UNT传递给函数到t he Js.Promise.th EN_。

优化 (Optimize)

We implemented our Heap and Scheduler modules similar to what we would have done in JavaScript. In doing so, we have reduced the performance of the functions operating on the heap such as add and extract to O(n).

我们实现了堆和调度程序模块,类似于我们在JavaScript中所做的。 这样,我们降低了在堆上运行的函数的性能,例如对O(n) addextract

We know Array in Reason has a fixed length. Everytime we add a new job or delete one, the size of our Array will change and therefore a new copy will be created. We can fix this by creating a dynamic array module that implements table doubling.

我们知道Array in Reason具有固定的长度。 每当我们添加或删除新作业时,阵列的大小都会更改,因此将创建一个新副本。 我们可以通过创建实现表加倍的动态数组模块来解决此问题。

I have created a version of Heap and Dynamic Array if you’re interested in the implementation, however, I think this would be outside the scope of this article. So for now we focus on optimizing the Scheduler by calling operations that cost O(n) less frequently.

如果您对实现感兴趣,我已经创建了一个版本的堆和动态数组,但是,我认为这不在本文讨论范围之内。 因此,目前我们专注于通过调用开销较小的O(n)操作来优化调度程序。

There are two places in the Scheduler where we call Heap.add and Heap.extract — when adding a new job and when executing a job.

在计划程序中,有两个地方我们称为Heap.addHeap.extract添加新作业和执行作业时。

We can’t help Scheduler.add but we can fix the performance of Scheduler.execute. The execute function doesn’t need to call extract or add since the size of our queue before and after execute should be the same.

我们无法帮助Scheduler.add但可以修复Scheduler.execute的性能。 execute函数不需要调用extractadd因为execute前后队列的大小应该相同。

Let’s introduce a new function to our Heap Signature. decrease_root_priority reduces the priority of the root of the Heap. We can use this new function to update the root key to its next invocation time without first extracting the head of the queue and adding it back with its updated invocation time.

让我们为堆签名引入一个新功能。 decrease_root_priority降低堆根的优先级。 我们可以使用此新功能将根密钥更新为下一个调用时间,而无需首先提取队列的头并将其与更新后的调用时间一起添加回去。

decrease_root_priority takes the new priority for the root, checks to make sure the new priority is less than the current priority of the root, and delegates the actual work to a helper function update_priority.

decrease_root_priority采用根的新优先级,检查以确保新优先级小于根的当前优先级,并将实际工作委托给辅助函数update_priority

update_priority can decrease or increase the priority of any element in a Heap in O(log(n)). It checks whether the new priority violates the max heap property with respect to the children of a node or its parent. When we increase the priority of a node, we might be violating the max heap property of the node with respect to its parent and so we fix_up. When we decrease the priority of a node, we might be violating the max heap property with respect to its children and so we call heapify to fix the possible violation.

update_priority可以降低或增加O(log(n)) Heap中任何元素的优先级。 它检查关于节点的子节点或其父节点的新优先级是否违反了最大堆属性。 当增加节点的优先级时,我们可能会违反其父节点的最大堆属性,因此我们使用fix_up 。 当我们降低节点的优先级时,我们可能违反了关于其子节点的最大堆属性,因此我们调用heapify来修复可能的冲突。

下一步 (Next steps)

This article is by far not a complete overview of the features of Reason. We have seen many of the language constructs, but haven’t explored them in detail. There are also features that have been left out, such as functors and objects. I strongly recommend you to read the documentation or Exploring ReasonML and functional programming to know what’s available to you before jumping to coding.

到目前为止,本文不是对Reason功能的完整概述。 我们已经看到了许多语言构造,但是没有详细探讨它们。 还有一些遗漏的功能,例如函子和对象。 我强烈建议您在跳转到编码之前先阅读文档探究ReasonML和函数式编程,以了解可用的内容。

The complete source code for what we covered today is available in the master branch of the https://github.com/Artris/reason-scheduler

我们今天介绍的完整源代码可在https://github.com/Artris/reason-schedulermaster分支中找到。

If you want to practice, I encourage you to add remove functionality to the scheduler. In specific, extend the signature of the Scheduler with

如果您想练习,建议您向计划程序中添加remove功能。 具体而言,延长的签名Scheduler

  • type jobId and

    type jobId

  • let remove = (t, jobId) => unit

    let remove = (t, jobId) => u nit

I also encourage you to add test cases for the functions exposed in the signature of the Heap and Scheduler modules.

我还鼓励您为HeapScheduler模块的签名中公开的功能添加测试用例。

The test cases for all the functions in the Heap and Scheduler module as well as an implementation for the remove functionality is available in the solutions branch.

解决方案分支中提供了HeapScheduler模块中所有功能的测试用例以及remove功能的实现。

归因 (Attribution)

I would like to thank the Reason/BuckleScript community for providing detailed documentation. And Dr. Axel Rauschmayer for Exploring ReasonML book and many interesting articles on Reason.

我要感谢Reason / BuckleScript社区提供的详细文档。 还有Axel Rauschmayer博士的《 ReasonML》一书和许多有关Reason的有趣文章。

Code snippets were generated using carbon.now.sh.

代码片段是使用carbon.now.sh生成的。

I’d also like to thank Grace, Sami, Freeman, and Preetpal who helped review this article.

我还要感谢GraceSamiFreemanPreetpal ,他们帮助审阅了这篇文章。

翻译自: https://www.freecodecamp.org/news/how-to-get-started-with-reason-cef7ab40660/

reason442

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值