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有基本的了解。 对函数式编程的一些熟悉也不会受到损害。
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。 让我们仔细看看:
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 u
nit (no argument) and returns an
int.
因此,几乎不必总是写下类型-编译器会为您推断类型。 例如,编译器看到这个() => {1 +
1}为取一个函数au
NIT(无参数),并返回an
int值。
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. Record
s (similar to JavaScript objects) are immutable.
原因中的大多数构造都是不可变的。 List
是不可变的。 Array
是可变的,但大小固定。 向数组添加新元素将返回使用新元素扩展的数组的副本。 Record
(类似于JavaScript对象)是不可变的。
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。 您绝对应该阅读本文档的“ 什么和为什么”部分,因为它为该语言及其功能提供了更多的上下文。
Reason’s official docs are simple and to the point
Reason的官方文档很简单,而且很直截了当
Exploring ReasonML, a book by Dr. Axel Rauschmayer, explores Reason in a more practical way
Axel Rauschmayer博士的书《 Explore ReasonML 》以更实际的方式探索了Reason
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中的不同概念(例如模块,语句,变量绑定和不可变性)如何协同工作。 每当我引入新的概念或语法时,我都会链接到相关的文档和文章。
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.
调度程序由堆组成,负责根据指定的重复规则更新计时器并执行作业。
The API of a priority queue defines:
优先级队列的API定义:
Heap performs insert
and extract
operations in order O(log(n))
where n
is the size of the queue.
堆按O(log(n))
顺序执行insert
和extract
操作,其中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 讲义中概述的伪代码。
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.
这是我们第一次看到ref
。 ref
是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
可以使用新的构造函数进行扩展。 稍后,我们将在堆模块的extract
和head
函数中raise
EmptyQueue
异常。
Exceptions are all of the same type,
exn
. Theexn
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异常都是相同的类型,
exn
。exn
类型是OCaml类型系统中的一种特殊情况。 它与我们在第6章“变体”中遇到的变体类型相似,但它是开放的,这意味着它在任何地方都没有完全定义。 — RealWorldOcaml
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模块的用户可以使用head
和extract
返回的值。 但是,我们不提供定义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
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.
left
和right
返回节点的左,右子节点的索引。
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
需要两个索引a
和b
以及一个数组queue
。 然后,它交换queue
索引a
和b
中的值。
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
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
定义为一个以queue
和index
为参数的函数。 通过声明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.
您提供的参数可用于返回的函数。 由于queue
在fix_up
是固定的, fix_up
我们将其部分应用于key
函数,以使我们的代码更加DRY 。
You can use <case>; when <c
ondition> to specify additional conditions in pattern matching. The value bindings
in the case are available to the expression fo
llowing when (in our e
xample 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
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
属性, heapify
在n
上调用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之间的最高优先级的元素的索引index
, left(index)
,和right(index)
。 如果max_priority_index
不等于index
,我们知道仍然存在违反max heap属性的情况。 我们在index
和max_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).
extract
与queue
匹配的模式(数组不是引用)。
[|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
。
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创建的测试框架,带有内置的模拟库和代码覆盖率报告。
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-jest
给bs-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.json
的sources
中指定它,以便BuckleScript编译器可以使用它们。
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
模块,我们将进行堆排序。
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 value
— e1
is now bound to the value
field of the value returned by extract
.
我们使用let {value: e1} =
以解构返回的值extract
和创建别名e1
的value
- 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 exp
ect with (e1, ...,
e9) to the toEq
ual function.
用|&
g t; 管道操作员,我们可以创建一个复合函数,并将结果函数立即应用于输入。 在这里,我们简单地传递愈伤组织的结果ng exp
ECT的Wi th (e1, ...,
E9)到t he toEq
UAL功能。
Scheduler uses the Heap module to maintain a list of recurrent jobs sorted by their next invocation time.
调度程序使用堆模块维护一个按其下一次调用时间排序的循环作业列表。
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.
recurrence
是Variant类型。 recurrence
类型的任何值都可以是Second
, Minute
或Hour
。 Second
, Minute
和Hour
是recurrence
的构造函数。 您可以像正常函数一样调用构造函数,并获取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
是记录类型 。 period
是recurrence
类型,表示每次执行作业之间的延迟。 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
是代表调度程序的记录类型。 调度程序保留按其下一次调用时间排序的作业queue
。 timer_id
引用queue
第一个作业(将首先调用的作业)的timerId
。
You can invoke JavaScript functions from within Reason. There are different ways of doing this:
您可以从Reason中调用JavaScript函数。 有不同的方法:
you can use BuckleScript bindings if available, such as Js.log
, and Js.Global.setTimeout
您可以使用BuckleScript绑定(如果可用),例如Js.log
和Js.Global.setTimeout
declare an external
such as [@bs.val] external setTimeout
声明一个external
例如[@bs.val] external setTimeout
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 ...]
。
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
。
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 subtract
ing 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
比较两个调用时间。 这是我们用来初始化堆的比较函数。
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
在任何时间点,只有一个计时器在队列中的第一个作业应运行时到期。 当计时器到期时,我们需要进行一些清理。 当计时器到期时,我们应该
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
域来执行指定的任务。
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
。
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
为此,我们将
add
a job to the scheduler to be executed every second. This job increments a ref(int)
counter.
add
作业add
到调度程序以每秒执行一次。 此作业将增加一个ref(int)
计数器。
create a Promise
that gets resolved after 4s
创建一个在4秒后解决的Promise
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, prom
ise will resolve with the value of the counter after 4s. This value is provided as the co
unt to the function passed to the Js.Promise.th
en_.
您可以使用|&
gt; 兑现承诺。 在我们的示例中e, prom
承诺将在4秒后以计数器的值解决。 这个值是为t提供he co
UNT传递给函数到t he Js.Promise.th
EN_。
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)
add
和extract
。
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.add
和Heap.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
函数不需要调用extract
或add
因为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
来修复可能的冲突。
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-scheduler的master
分支中找到。
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) => u
nit
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.
我还鼓励您为Heap
和Scheduler
模块的签名中公开的功能添加测试用例。
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.
解决方案分支中提供了Heap
和Scheduler
模块中所有功能的测试用例以及remove
功能的实现。
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.
我还要感谢Grace , Sami , Freeman和Preetpal ,他们帮助审阅了这篇文章。
翻译自: https://www.freecodecamp.org/news/how-to-get-started-with-reason-cef7ab40660/
reason442