Kotlin Jupyter 支持 JavaScript 魔法标签

缘起

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

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

kt 复制代码
// 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 复制代码
%js
var hello = "hellow jupyter js"

console.log(hello)

需要转换为:

kotlin 复制代码
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 变量

kt 复制代码
val foo = "bar";

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

js 复制代码
%js
import { foo } from '@jupyter';

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

实际的编译结果:

html 复制代码
<script type="module">
const foo = "foo"

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

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

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

但是,如果对于我们可以缩小支持转换成 JSON 的类型范围,那么就是可以做到的。

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

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

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

kt 复制代码
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 对象。

kt 复制代码
when (value) {
  is DisplayResult -> {
    value.toJson()
  }
  is Renderable -> {
    value.render(notebook).toJson()
  }
}

From JavaScript To JavaScript

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

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

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

我实现了 SWC 的 binding swc-binding

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

参考下面的流程图:

如果给 Kotlin Kernel 的代码里包含%js magic,JavaScriptMagicCodeProcessor就会将 JS 代码处理成合法的 kotlin 代码。

JavaScriptMagicCodeProcessor处理流程如下

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

第二步,操作 AST

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

第三步,AST 转回代码

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

尾声

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

Echart 的例子截图:

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

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

github: yidafu/kotlin-jupyter-js

相关推荐
Chatopera 研发团队1 天前
机器学习 - 为 Jupyter Notebook 安装新的 Kernel
人工智能·机器学习·jupyter
孤客网络科技工作室1 天前
在 Jupyter Notebook 中使用 Matplotlib 进行交互式可视化的教程
ide·jupyter·matplotlib
好难怎么办2 天前
动手学深度学习-使用d2l导致jupyter内核挂掉
人工智能·深度学习·jupyter
陈晨辰熟稳重2 天前
20241112-Pycharm使用托管的Anaconda的Jupyter Notebook
python·jupyter·pycharm
Byyyi耀2 天前
Jupyter notebook如何加载torch环境
ide·python·jupyter
Byyyi耀2 天前
更改 Jupyter Notebook 中文件的存放位置
ide·python·jupyter
阿斯卡码3 天前
jupyter添加、删除、查看内核
ide·python·jupyter
梓仁沐白5 天前
Windows上安装与使用 Jupyter Notebook
ide·windows·jupyter
伊玛目的门徒6 天前
容器内pip安装Apache Airflow的经历:如何重置初始密码
python·jupyter·pip·工作流·apache airflow
Biomamba生信基地9 天前
jupyter如何切换内核
ide·python·jupyter