Javascript 和 Node 代码生成器

有两个 javascript 代码生成器, nodejavascript 。两者之间有两个区别: javascript 代码生成器在被调用时,如果输出是一个一个HTML文件,会同时生成一个基本的HTML文件,生成的代码在 <script> 标签内;另一个区别是在 ffi 上,将在下面解释。

注意:JavaScript 代码生成器使用了新的 BigInt,因此需要 Node.js 10.4 或更高版本。

Javascript FFI 说明符

有三种主要的 javascript ffi 说明符 javascript, nodebrowserjavascript 表示在node 和浏览器上均可用, node 仅在 node 上可用, browser 仅在浏览器上可用。

对于 node 来说,有两种方法来定义一个外部函数:

%foreign "node:lambda: n => process.env[n]"
prim_getEnv : String -> PrimIO (Ptr String)

这里的 lambda 表示我们将定义作为一个 lambda 表达式进行提供。

%foreign "node:lambda:fp=>require('fs').fstatSync(fp.fd, {bigint: false}).size"
prim__fileSize : FilePtr -> PrimIO Int

require 可以用来导入 javascript 模块。

下面是一个完整示例,只有在 browser 的 codegen 是外部函数才可用:

%foreign "browser:lambda:x=>{document.body.innerHTML = x}"
prim__setBodyInnerHTML : String -> PrimIO ()

简短示例

一个有趣的例子是为 setTimeout 函数创建一个外部函数:

%foreign "javascript:lambda:(callback, delay)=>setTimeout(callback, delay)"
prim__setTimeout : (PrimIO ()) -> Int -> PrimIO ()

setTimeout : HasIO io => IO () -> Int -> io ()
setTimeout callback delay = primIO $ prim__setTimeout (toPrim callback) delay

注意:以前版本 的javascript 后端将 Int 视为一个64位有符号的整数,在 javascript 领域由 BigInt 表示。现在情况不是这样了。 Int 现在被视为一个32位有符号的整数,由 Number 表示。这应该有利于 Idris2 和后端之间的互操作。

不过,除非有特别充分的理由,否则建议优先使用其他定长整数类型。这些类型在所有后端的行为应当保持一致。所有精度不超过 32 位的有符号和无符号整数(如 Int8Int16Int32Bits8Bits16Bits32)都由 Number 表示,而 Int64Bits64Integer 则由 BigInt 表示。因此,上述示例可以通过将 Int 替换为 Int32 进行改进:

%foreign "javascript:lambda:(callback, delay)=>setTimeout(callback, delay)"
prim__setTimeout : (PrimIO ()) -> Int32 -> PrimIO ()

setTimeout : HasIO io => IO () -> Int32 -> io ()
setTimeout callback delay = primIO $ prim__setTimeout (toPrim callback) delay

浏览器示例

要构建能在浏览器中使用的JavaScript,必须使用 javascript codegen 选项编译代码。编译器生成 JavaScript 或 HTML 文件。浏览器需要一个 HTML 文件才能加载。此HTML文件可以通过两种方式创建

  • 如果输出文件中包含 .html 后缀,编译器就会生成一个 HTML 文件,其中包括对已生成的 JavaScript 的包装。

  • 如果 没有 给出 .html 后缀,生成的文件只包含JavaScript代码。在这种情况下,需要手动包装。

包装到 HTML 的示例:

<html>
 <head><meta charset='utf-8'></head>
 <body>
  <script type='text/javascript'>
  JS code goes here
  </script>
 </body>
</html>

由于我们的目的是开发在浏览器中运行的东西,自然会产生一些问题:

  • 如何与 HTML 元素交互?

  • 更重要的是,生成的 Idris 代码会在什么时候开始执行?

Idris 生成代码的起点

为你的程序生成的 JavaScript 包含一个入口点。 main 函数被编译成一个 JavaScript 顶层表达式,它将在加载 script 标签时被求值,这就是Idris生成的程序在浏览器中开始的入口点。

与HTML元素的交互

正如简短示例部分所描述的,当 Idris 生成的代码和浏览器/JS生态系统的其他部分发生交互时,必须使用 FFI 。由 FFI 处理的信息被分成两类。第一是Idris FFI 的原语类型,如 Int 。第二类是除原语类型之外所有的。第二类是通过 AnyPtr 访问的。 %foreign 结构应该被用来在 JavaScript 方面给出实现。还有一个 Idris 函数声明,在 Idris 方面给出 Type 声明。语法是 %foreign "browser:lambda:js-lambda-expression" 。在 Idris 方面,当定义 %foreign 时,原语类型和 PrimIO t 类型应该作为参数。这个声明是一个辅助函数,需要在 primIO 函数后面被调用。关于这一点的更多信息可以在 FFI 章节中找到。

JavaScript FFI 示例

console.log

%foreign "browser:lambda: x => console.log(x)"
prim__consoleLog : String -> PrimIO ()

consoleLog : HasIO io => String -> io ()
consoleLog x = primIO $ prim__consoleLog x

在 Idris 中,字符串是一个原语类型,它被表示为一个 JavaScript 字符串。在 Idris 和 JavaScript 之间没有必要进行任何转换。

setInterval

%foreign "browser:lambda: (a,i)=>setInterval(a,i)"
prim__setInterval : PrimIO () -> Int32 -> PrimIO ()

setInterval : (HasIO io) => IO () -> Int32 -> io ()
setInterval a i = primIO $ prim__setInterval (toPrim a) i

JavaScript 中的 setInterval 函数在每 x 毫秒执行给定的函数。我们可以在回调中使用 Idris 生成的函数,只要它们的类型是 IO ()

HTML Dom 元素

让我们把注意力转移到 Dom 元素和事件上。如上所述,任何不是原语类型的东西都应该通过FFI中的 AnyPtr 类型来处理。任何由 JavaScript 函数返回的复杂的东西都应该在 AnyPtr 值中捕获。建议将 AnyPtr 值分成几类。

data DomNode = MkNode AnyPtr

%foreign "browser:lambda: () => document.body"
prim__body : () -> PrimIO AnyPtr

body : HasIO io => io DomNode
body = map MkNode $ primIO $ prim__body ()

我们创建了一个 DomNode 类型,它持有一个 AnyPtrprim__body 函数包装了一个没有参数的 lambda 函数。在 Idris 函数 body 中,我们传递一个额外的 () 参数,我们使用 MkNode 数据构造器将结果包裹在 DomNode 类型中。

JavaScript 返回的原语类型值

作为前面例子的延续,DOM元素的 width 属性可以通过FFI检索。

%foreign "browser:lambda: n=>(n.width)"
prim__width : AnyPtr -> PrimIO Bits32

width : HasIO io => DomNode -> io Bits32
width (MkNode p) = primIO $ prim__width p

处理 JavaScript 事件

data DomEvent = MkEvent AnyPtr

%foreign "browser:lambda: (event, callback, node) => node.addEventListener(event, x=>callback(x)())"
prim__addEventListener : String -> (AnyPtr -> PrimIO ()) -> AnyPtr -> PrimIO ()

addEventListener : HasIO io => String -> DomNode -> (DomEvent -> IO ()) -> io ()
addEventListener event (MkNode n) callback =
  primIO $ prim__addEventListener event (\ptr => toPrim $ callback $ MkEvent ptr) n

本例展示了如何将事件处理器附加到特定的 DOM 元素。在 Idris 端,事件的值同样通过 AnyPtr 进行关联。为了区分 DomNodeDomEvent,我们分别定义了两种不同的类型。此外,该示例还演示了如何在 JavaScript 端使用 Idris 中定义的简单回调函数。

指令

javascript 代码生成器接受三种不同的指令,即生成的代码应该有多紧凑和多晦涩。下面的例子显示了为 putStr 函数生成的代码,这三个指令分别来自 prelude 。(--cg node 被在下面的例子使用,但在生成代码在浏览器中运行时, --cg javascript 的行为是一样的)。

使用 idris2 --cg node --directive pretty (默认情况下,如果没有给出指令),一个基本的美观打印器被用来生成正确缩进的 javascript 代码。

function Prelude_IO_putStr($0, $1) {
 return $0.a2(undefined)($7 => Prelude_IO_prim__putStr($1, $7));
}

使用 idris2 --cg node --directive compact ,每一个顶层函数都在一行中声明,不需要的空格都会被删除:

function Prelude_IO_putStr($0,$1){return $0.a2(undefined)($7=>Prelude_IO_prim__putStr($1,$7));}

最后,通过 idris2 --cg node --directive minimal ,顶层函数名称(除了少数例外,如静态序言『static preamble』中的函数)会被混淆,以减少生成的javascript文件的大小:

function $R3a($0,$1){return $0.a2(undefined)($7=>$R3b($1,$7));}