Skip to content

Latest commit

 

History

History
210 lines (141 loc) · 8.66 KB

how-kotlin-jupyter-work.zh-CN.md

File metadata and controls

210 lines (141 loc) · 8.66 KB

Kotlin Jupyter Js 如何实现?

缘起

工作上需要了解盲水印,而这涉及傅里叶变换。我对傅里叶变换一窍不通,想着通过 Kotlin Jupyter 来一步一步用代码来演示傅里叶变换,辅助理解。然而 Kotlin Jupyter 里没有找到 3D 曲面的展示库,像 lets-plot 似乎只支持 2D 图表。

经过尝试,在 Kotlin Jupyter 只能使用 JS 实现 3D 曲面的展示。而 Kotlin Jupyter 里使用 JS 比较繁琐。你需要先将 kotlin 变量转为 JSON 字符串,在HTML()方法写要执行 html 代码,容器标签和<script>标签,

代码如下:

// convert to JSON
var dataList = "[" + bList.map { "[${it.first}, ${it.second}, ${it.third}]" }.joinToString(",\n") + "]";

// render to html
HTML("""
<div id="chartDom" style="width: 600px; height: 600px;"> </div>
<script type="module">
import { init } from "https://unpkg.com/echarts@5.4.3/dist/echarts.esm.min.js"
import "https://unpkg.com/echarts-gl@2.0.9/dist/echarts-gl.min.js"

var chartDom = document.getElementById('chartDom');
var myChart = echarts.init(chartDom);
myChart.setOption({
  // echart option
})
</script>
""")

Don't Repeat Yourself

可以看到上面的例子有好几处模板代码。

  • kotlin 转 Json
  • 创建容器<div>标签
  • 创建<script>标签

ipython 就支持%js直接写js,cell 执行前会拦截有%js标记的代码,转为<script />标签插入的输出 cell。

在 Kotlin Jupyter 我们也通过自定义 line magic,来生成这些模版代码。Kotlin Jupyter 已经提供给了相关的钩子(kotlin-jupyter/docs/libaries.md),我们需要做的就是写一个CodePreprocessor拦截,含有%js的代码,转为HTML函数调用。

比如下面的:

%js
var hello = "hellow jupyter js"

console.log(hello)

需要转换为:

HTML("""
<script type="module">
var hello = "hellow jupyter js"

console.log(hello)
</script>
""")

One More Thing

但是还有更重要的一件事。我们在 Kotlin Jupyter 里写 JS 的目的是为了可视化 Kotlin 的数据,仅仅只是转换代码没有实用价值。我们需要能在 JS 里使用 Kotlin 的变量。

那我们如何在 JS 里使用 kotlin 的数据呢?我的想法是就是虚拟 import。定义@jupyter为虚拟 package,我们可以从这里 import Kotlin 的变量,编译时替换成真实的 kotlin 变量

假设,第一个 cell 定义了一个 Kotlin 变量

val foo = "bar";

后面的cell,直接 import 这个变量然后使用即可

%js
import { foo } from '@jupyter';

console.log('variable from kotlin', foo)

实际的编译结果:

<script type="module">
const foo = "foo"

console.log('variable from kotlin', foo)
</script>

到这里,解决的问题变成了变量从 Kotlin 世界到 JS 世界的转换。对于任意的 Kotlin 变量可以转为 JSON 吗?

根据源码 VariableState.kt#L11 可知:所有的 Kotlin 变量都 Kernel 被保存成Any。显然我们是无法将Any转为 JSON 字符串。

如果对于我们可以缩小支持转类型范围,将 Any 类型的值转为 JSON 就是可以做到。

下面两种场景是比较简单的:

  1. 基本类型/Array/Collection/Map
  2. 使用了 Renderable/DisplayResult 接口

根据 Kotlin/kotlinx.serialization#296 的讨论,在Any?实现toJsonElement方法就可以做到将任意的Collection,Map,Array,String,Boolean,Number 转为 JSON。这已经足够能够支持大多数场景了。

下面的函数就能够递归的将基础类型转为JsonElement,然后将JsonElement转为字符串就很方便了。

fun Any?.toJsonElement(): JsonElement = when(this) {
    null -> JsonNull
    is Collection<*> -> toJsonElement() // call Collection<*>.toJsonElement()
    is String -> JsonPrimitive(this) // end of recursive
    // ... ignore Map<*, *> Array<*>, other primary type
    else -> {
      throw IllegalStateException("Can't serialize unknown type: $this")
    }
}

fun Collection<*>.toJsonElement(): JsonElement {
    return JsonArray(this.map {
      it.toJsonElement() // recursively transform value to JsonElement 
    })
}

完整代码:AnyToJsonElement.kt#L5

但是,这种方式不支持类,对于类的支持需要另一种方式。实现DisplayResult或者Renderable 接口。因为DisplayResulttoJSon 方法的,通过这个方法就能获取到可以 import 的 json 对象。

Kotlin Jupyter JS 变量会判断是否是DisplayResult Renderable 类型,调用toJson方法就能后获取到该变量的 JSON 数据。

when (value) {
  is DisplayResult -> {
    value.toJson()
  }
  is Renderable -> {
    value.render(notebook).toJson()
  }
  // other case
}

From JavaScript To JavaScript

上面提到虚拟 import,需要 import 语句能够被编译成变量声明。我们可以通过正则表达式来将替换 import 语句替换成变量声明,但这不是处理代码的好方式。最好能够将 JS 代码转换成 AST,再操作 AST 进行代码变换。

Kotlin 里并没有一个好的工具了来编译 JS。常见的 JS 编译工具,比如:babel 都是 JS 写的,很难在 JVM 里使用。但是,感谢最近几年前端工具链的锈化,现在已经有了SWC,OXC等 Rust 写的 JS 编译器。可以通过JNI 绑定SWC的动态库来实现 JVM 里编译 JS。Kotlin也可以“原生”支持编译JS

社区似乎没有现成 swc 的 binding。不过,写一个 binding 总比写一个JS 编译器简单。

我实现了 SWC 的 JVM binding swc-binding。基于官方 node binding 改了改,将 napi-rs 换成了 jni。还支持 DSL 的方式描述 AST。

有了 SWC 以后,那我们不光可以支持 jsts/jsx/tsx 也可支持了。

参考下面的流程图:

如果给 Kotlin Kernel 的代码里包含%js magic,JavaScriptMagicCodeProcessor就会将 JS 代码处理成 HTML(""" $jsCode """") 调用以便于能够被 Kotlin Jypyter 正确渲染。

JavaScriptMagicCodeProcessor处理流程如下

第一步,会将 jsx/ts/tsx 转换为正常的 JS,如果是 JS 不会特殊处理

第二步,操作 AST。这一步是 Kotlin Jupyter JS 的核心逻辑,主要做一些代码转换的工作。

  1. import { * } from '@jupyter'; 变量声明语句
  2. jsx/tsx 的默认导出修改为变量声明
  3. 其它操作

第三步,AST 转回代码

最后将 JS 代码包装的成 HTML 结果返回。

React (Jsx to Js)

对于 React, Jupyter 会将默认导出转换变量声明

比如,下面的例子:

export function App() {
  return <span>React</span>
}

会被转换成

const __JupyterCellDefaultExportVariable__ = function App() {
  return <span>React</span>
}
const root = createRoot(cellElement);
root.render(React.createElement(__JupyterCellDefaultExportVariable__))

尾声

至此,kotlin Jupyter 支持 %js magic 的思路就梳理清楚了。

Echart 的例子截图:

实际例子,可以看一下examples/js-magic.ipynb

欢迎大家试用,反馈问题。