-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
yidafu
committed
Jan 22, 2024
1 parent
499354b
commit 5b2e123
Showing
1 changed file
with
172 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<title>docs/how-kotlin-jupyter-work.zh-CN.md</title> | ||
<link rel="stylesheet" href="theme.css"> | ||
</head> | ||
<body><div class="markdown-body"> | ||
<h1 id="kotlin-jupyter-js-">Kotlin Jupyter Js 如何实现?</h1> | ||
<h2 id="-">缘起</h2> | ||
<p>工作上需要了解盲水印,而这涉及傅里叶变换。我对傅里叶变换一窍不通,想着通过 Kotlin Jupyter 来一步一步用代码来演示傅里叶变换,辅助理解。然而 Kotlin Jupyter 里没有找到 3D 曲面的展示库,像 lets-plot 似乎只支持 2D 图表。</p> | ||
<p>经过尝试,在 Kotlin Jupyter 只能使用 JS 实现 3D 曲面的展示。而 Kotlin Jupyter 里使用 JS 比较繁琐。你需要先将 kotlin 变量转为 JSON 字符串,在<code>HTML()</code>方法写要执行 html 代码,容器标签和<code><script></code>标签,</p> | ||
<p>代码如下:</p> | ||
<pre><code class="lang-kotlin"><div class="highlight"><pre><span class="c1">// convert to JSON</span> | ||
<span class="k">var</span> <span class="py">dataList</span> <span class="p">=</span> <span class="s">"["</span> <span class="p">+</span> <span class="n">bList</span><span class="p">.</span><span class="n">map</span> <span class="p">{</span> <span class="s">"[${it.first}, ${it.second}, ${it.third}]"</span> <span class="p">}.</span><span class="n">joinToString</span><span class="p">(</span><span class="s">",\n"</span><span class="p">)</span> <span class="p">+</span> <span class="s">"]"</span><span class="p">;</span> | ||
|
||
<span class="c1">// render to html</span> | ||
<span class="n">HTML</span><span class="p">(</span><span class="s">"""</span> | ||
<span class="p"><</span><span class="n">div</span> <span class="n">id</span><span class="p">=</span><span class="s">"chartDom"</span> <span class="n">style</span><span class="p">=</span><span class="s">"width: 600px; height: 600px;"</span><span class="p">></span> <span class="p"></</span><span class="n">div</span><span class="p">></span> | ||
<span class="p"><</span><span class="n">script</span> <span class="k">type</span><span class="p">=</span><span class="s">"module"</span><span class="p">></span> | ||
<span class="k">import</span> <span class="nn">{</span> <span class="n">init</span> <span class="p">}</span> <span class="n">from</span> <span class="s">"https://unpkg.com/echarts@5.4.3/dist/echarts.esm.min.js"</span> | ||
<span class="k">import</span> <span class="nn">"https://unpkg.com/echarts-gl@2.0.9/dist/echarts-gl.min.js"</span> | ||
|
||
<span class="k">var</span> <span class="py">chartDom</span> <span class="p">=</span> <span class="n">document</span><span class="p">.</span><span class="n">getElementById</span><span class="p">(</span><span class="err">'</span><span class="n">chartDom</span><span class="err">'</span><span class="p">);</span> | ||
<span class="k">var</span> <span class="py">myChart</span> <span class="p">=</span> <span class="n">echarts</span><span class="p">.</span><span class="n">init</span><span class="p">(</span><span class="n">chartDom</span><span class="p">);</span> | ||
<span class="n">myChart</span><span class="p">.</span><span class="n">setOption</span><span class="p">({</span> | ||
<span class="c1">// echart option</span> | ||
<span class="p">})</span> | ||
<span class="p"></</span><span class="n">script</span><span class="p">></span> | ||
<span class="s">""")</span> | ||
</pre></div> | ||
|
||
</code></pre> | ||
<h2 id="don-t-repeat-yourself">Don't Repeat Yourself</h2> | ||
<p>可以看到上面的例子有好几处模板代码。</p> | ||
<ul> | ||
<li>kotlin 转 Json</li> | ||
<li>创建容器<code><div></code>标签</li> | ||
<li>创建<code><script></code>标签</li> | ||
</ul> | ||
<p>像 <code>ipython</code> 就支持<code>%js</code>直接写js,cell 执行前会拦截有<code>%js</code>标记的代码,转为<code><script /></code>标签插入的输出 cell。</p> | ||
<p>在 Kotlin Jupyter 我们也通过自定义 line magic,来生成这些模版代码。Kotlin Jupyter 已经提供给了相关的钩子(<a href="https://github.com/Kotlin/kotlin-jupyter/blob/master/docs/libraries.md">kotlin-jupyter/docs/libaries.md</a>),我们需要做的就是写一个<code>CodePreprocessor</code>拦截,含有<code>%js</code>的代码,转为<code>HTML</code>函数调用。</p> | ||
<p>比如下面的:</p> | ||
<pre><code class="lang-js"><div class="highlight"><pre><span class="o">%</span><span class="nx">js</span> | ||
<span class="kd">var</span> <span class="nx">hello</span> <span class="o">=</span> <span class="s2">"hellow jupyter js"</span> | ||
|
||
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">hello</span><span class="p">)</span> | ||
</pre></div> | ||
|
||
</code></pre> | ||
<p>需要转换为:</p> | ||
<pre><code class="lang-kotlin"><div class="highlight"><pre><span class="n">HTML</span><span class="p">(</span><span class="s">"""</span> | ||
<span class="p"><</span><span class="n">script</span> <span class="k">type</span><span class="p">=</span><span class="s">"module"</span><span class="p">></span> | ||
<span class="k">var</span> <span class="py">hello</span> <span class="p">=</span> <span class="s">"hellow jupyter js"</span> | ||
|
||
<span class="n">console</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="n">hello</span><span class="p">)</span> | ||
<span class="p"></</span><span class="n">script</span><span class="p">></span> | ||
<span class="s">""")</span> | ||
</pre></div> | ||
|
||
</code></pre> | ||
<h2 id="one-more-thing">One More Thing</h2> | ||
<p>但是还有更重要的一件事。我们在 Kotlin Jupyter 里写 JS 的目的是为了可视化 Kotlin 的数据,仅仅只是转换代码没有实用价值。我们需要能在 JS 里使用 Kotlin 的变量。</p> | ||
<p>那我们如何在 JS 里使用 kotlin 的数据呢?我的想法是就是虚拟 import。定义<code>@jupyter</code>为虚拟 package,我们可以从这里 <code>import</code> Kotlin 的变量,编译时替换成真实的 kotlin 变量</p> | ||
<p>假设,第一个 cell 定义了一个 Kotlin 变量</p> | ||
<pre><code class="lang-kotlin"><div class="highlight"><pre><span class="k">val</span> <span class="py">foo</span> <span class="p">=</span> <span class="s">"bar"</span><span class="p">;</span> | ||
</pre></div> | ||
|
||
</code></pre> | ||
<p>后面的cell,直接 import 这个变量然后使用即可</p> | ||
<pre><code class="lang-js"><div class="highlight"><pre><span class="o">%</span><span class="nx">js</span> | ||
<span class="kr">import</span> <span class="p">{</span> <span class="nx">foo</span> <span class="p">}</span> <span class="nx">from</span> <span class="s1">'@jupyter'</span><span class="p">;</span> | ||
|
||
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'variable from kotlin'</span><span class="p">,</span> <span class="nx">foo</span><span class="p">)</span> | ||
</pre></div> | ||
|
||
</code></pre> | ||
<p>实际的编译结果:</p> | ||
<pre><code class="lang-html"><div class="highlight"><pre><span class="nt"><script </span><span class="na">type=</span><span class="s">"module"</span><span class="nt">></span> | ||
<span class="kr">const</span> <span class="nx">foo</span> <span class="o">=</span> <span class="s2">"foo"</span> | ||
|
||
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'variable from kotlin'</span><span class="p">,</span> <span class="nx">foo</span><span class="p">)</span> | ||
<span class="nt"></script></span> | ||
</pre></div> | ||
|
||
</code></pre> | ||
<p>到这里,解决的问题变成了变量从 Kotlin 世界到 JS 世界的转换。对于任意的 Kotlin 变量可以转为 JSON 吗?</p> | ||
<p>根据源码 <a href="https://github.com/Kotlin/kotlin-jupyter/blob/94794065fd0a616b757a8cabf4574bb63344facb/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/VariableState.kt#L11">VariableState.kt#L11</a> 可知:所有的 Kotlin 变量都 Kernel 被保存成<code>Any</code>。显然我们是无法将<code>Any</code>转为 JSON 字符串。</p> | ||
<p>如果对于我们可以缩小支持转类型范围,将 Any 类型的值转为 JSON 就是可以做到。</p> | ||
<p>下面两种场景是比较简单的:</p> | ||
<ol> | ||
<li>基本类型/Array/Collection/Map</li> | ||
<li>使用了 <code>Renderable</code>/<code>DisplayResult</code> 接口</li> | ||
</ol> | ||
<p>根据 <a href="https://github.com/Kotlin/kotlinx.serialization/issues/296">Kotlin/kotlinx.serialization#296</a> 的讨论,在<code>Any?</code>实现<code>toJsonElement</code>方法就可以做到将任意的<code>Collection</code>,<code>Map</code>,<code>Array</code>,<code>String</code>,<code>Boolean</code>,<code>Number</code> 转为 JSON。这已经足够能够支持大多数场景了。</p> | ||
<p>下面的函数就能够递归的将基础类型转为<code>JsonElement</code>,然后将<code>JsonElement</code>转为字符串就很方便了。</p> | ||
<pre><code class="lang-kotlin"><div class="highlight"><pre><span class="k">fun</span> <span class="nf">Any</span><span class="o">?.</span><span class="n">toJsonElement</span><span class="p">():</span> <span class="n">JsonElement</span> <span class="p">=</span> <span class="k">when</span><span class="p">(</span><span class="k">this</span><span class="p">)</span> <span class="p">{</span> | ||
<span class="k">null</span> <span class="p">-></span> <span class="n">JsonNull</span> | ||
<span class="k">is</span> <span class="n">Collection</span><span class="p"><*></span> <span class="p">-></span> <span class="n">toJsonElement</span><span class="p">()</span> <span class="c1">// call Collection<*>.toJsonElement()</span> | ||
<span class="k">is</span> <span class="n">String</span> <span class="p">-></span> <span class="n">JsonPrimitive</span><span class="p">(</span><span class="k">this</span><span class="p">)</span> <span class="c1">// end of recursive</span> | ||
<span class="c1">// ... ignore Map<*, *> Array<*>, other primary type</span> | ||
<span class="k">else</span> <span class="p">-></span> <span class="p">{</span> | ||
<span class="k">throw</span> <span class="n">IllegalStateException</span><span class="p">(</span><span class="s">"Can't serialize unknown type: $this"</span><span class="p">)</span> | ||
<span class="p">}</span> | ||
<span class="p">}</span> | ||
|
||
<span class="k">fun</span> <span class="nf">Collection</span><span class="p"><*>.</span><span class="n">toJsonElement</span><span class="p">():</span> <span class="n">JsonElement</span> <span class="p">{</span> | ||
<span class="k">return</span> <span class="n">JsonArray</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">map</span> <span class="p">{</span> | ||
<span class="n">it</span><span class="p">.</span><span class="n">toJsonElement</span><span class="p">()</span> <span class="c1">// recursively transform value to JsonElement </span> | ||
<span class="p">})</span> | ||
<span class="p">}</span> | ||
</pre></div> | ||
|
||
</code></pre> | ||
<p>完整代码:<a href="https://github.com/yidafu/kotlin-jupyter-js/blob/50fb7d30cc15d9554e5062986aafe06922470fbf/jupyter-js/src/main/kotlin/dev/yidafu/jupyper/AnyToJsonElement.kt#L5">AnyToJsonElement.kt#L5</a></p> | ||
<p>但是,这种方式不支持类,对于类的支持需要另一种方式。实现<code>DisplayResult</code>或者<code>Renderable</code> 接口。因为<code>DisplayResult</code>有 <code>toJSon</code> 方法的,通过这个方法就能获取到可以 import 的 json 对象。</p> | ||
<p>Kotlin Jupyter JS 变量会判断是否是<code>DisplayResult</code> <code>Renderable</code> 类型,调用<code>toJson</code>方法就能后获取到该变量的 JSON 数据。</p> | ||
<pre><code class="lang-kotlin"><div class="highlight"><pre><span class="k">when</span> <span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="p">{</span> | ||
<span class="k">is</span> <span class="n">DisplayResult</span> <span class="p">-></span> <span class="p">{</span> | ||
<span class="n">value</span><span class="p">.</span><span class="n">toJson</span><span class="p">()</span> | ||
<span class="p">}</span> | ||
<span class="k">is</span> <span class="n">Renderable</span> <span class="p">-></span> <span class="p">{</span> | ||
<span class="n">value</span><span class="p">.</span><span class="n">render</span><span class="p">(</span><span class="n">notebook</span><span class="p">).</span><span class="n">toJson</span><span class="p">()</span> | ||
<span class="p">}</span> | ||
<span class="c1">// other case</span> | ||
<span class="p">}</span> | ||
</pre></div> | ||
|
||
</code></pre> | ||
<h2 id="from-javascript-to-javascript">From JavaScript To JavaScript</h2> | ||
<p>上面提到虚拟 import,需要 import 语句能够被编译成变量声明。我们可以通过正则表达式来将替换 import 语句替换成变量声明,但这不是处理代码的好方式。最好能够将 JS 代码转换成 AST,再操作 AST 进行代码变换。</p> | ||
<p>Kotlin 里并没有一个好的工具了来编译 JS。常见的 JS 编译工具,比如:babel 都是 JS 写的,很难在 JVM 里使用。但是,感谢最近几年前端工具链的锈化,现在已经有了<code>SWC</code>,<code>OXC</code>等 Rust 写的 JS 编译器。可以通过<code>JNI</code> 绑定<code>SWC</code>的动态库来实现 JVM 里编译 JS。Kotlin也可以“原生”支持编译<code>JS</code>。</p> | ||
<p>社区似乎没有现成 <code>swc</code> 的 binding。不过,写一个 binding 总比写一个JS 编译器简单。</p> | ||
<p>我实现了 SWC 的 JVM binding <a href="https://central.sonatype.com/artifact/dev.yidafu.swc/swc-binding">swc-binding</a>。基于官方 node binding 改了改,将 <code>napi-rs</code> 换成了 <code>jni</code>。还支持 DSL 的方式描述 AST。</p> | ||
<p>有了 SWC 以后,那我们不光可以支持 <code>js</code>,<code>ts</code>/<code>jsx</code>/<code>tsx</code> 也可支持了。</p> | ||
<p>参考下面的流程图:</p> | ||
<p><a href="https://mermaid.live/edit#pako:eNp9Ut9r20AM_lfEPceGrdsKoeyh7KGwFQYtDBr34Xonx5f4TkanWwgh__sUe0uTNsxgkHT6Pv36dsaRRzM3TWp72rjOssDtY5NAv1umTUY-c6CqPtSQqbBDOGCr6it8J-lDmvImW9Oua-gk9sCYSy-HtHPCXF6WbIfuL2KxnoCrMmxF66yRE_bPU-5IPAZe_Z9MDnMmfotcZRgYh3_PJxR54xb6w-p3hJeQfEjLdwW08481eBR0cjondDYfqKNdBgfEkGgc6thGky70BjdVBVc1CNuUW-I4cs10KVI4gc2iTzguURu7RKD4TzVE8qHdnuWfFL5c9nOtewhJjigQggfR0PK_9b7U8EsPM0yrHGffBOng7vH-B7QlOQmUxrOfXASTnwwzMxE52uBVVbtDrDHSYcTGzNX02FqVQ6OC22uqLUIP2-TMXLjgzJTBW8Fvwao0opm3ts8aHWx6Inr10Qchvp-UOwp4_wdJ4ue5"><img src="https://mermaid.ink/img/pako:eNp9Ut9r20AM_lfEPceGrdsKoeyh7KGwFQYtDBr34Xonx5f4TkanWwgh__sUe0uTNsxgkHT6Pv36dsaRRzM3TWp72rjOssDtY5NAv1umTUY-c6CqPtSQqbBDOGCr6it8J-lDmvImW9Oua-gk9sCYSy-HtHPCXF6WbIfuL2KxnoCrMmxF66yRE_bPU-5IPAZe_Z9MDnMmfotcZRgYh3_PJxR54xb6w-p3hJeQfEjLdwW08481eBR0cjondDYfqKNdBgfEkGgc6thGky70BjdVBVc1CNuUW-I4cs10KVI4gc2iTzguURu7RKD4TzVE8qHdnuWfFL5c9nOtewhJjigQggfR0PK_9b7U8EsPM0yrHGffBOng7vH-B7QlOQmUxrOfXASTnwwzMxE52uBVVbtDrDHSYcTGzNX02FqVQ6OC22uqLUIP2-TMXLjgzJTBW8Fvwao0opm3ts8aHWx6Inr10Qchvp-UOwp4_wdJ4ue5?type=png" alt=""></a></p> | ||
<p>如果给 Kotlin Kernel 的代码里包含<code>%js</code> magic,<code>JavaScriptMagicCodeProcessor</code>就会将 JS 代码处理成 <code>HTML(""" $jsCode """")</code> 调用以便于能够被 Kotlin Jypyter 正确渲染。</p> | ||
<p><code>JavaScriptMagicCodeProcessor</code>处理流程如下</p> | ||
<p>第一步,会将 <code>jsx</code>/<code>ts</code>/<code>tsx</code> 转换为正常的 JS,如果是 JS 不会特殊处理</p> | ||
<p>第二步,操作 AST。这一步是 Kotlin Jupyter JS 的核心逻辑,主要做一些代码转换的工作。</p> | ||
<ol> | ||
<li>将<code>import { * } from '@jupyter';</code> 变量声明语句</li> | ||
<li>将 <code>jsx</code>/<code>tsx</code> 的默认导出修改为变量声明</li> | ||
<li>其它操作</li> | ||
</ol> | ||
<p>第三步,AST 转回代码</p> | ||
<p>最后将 JS 代码包装的成 HTML 结果返回。</p> | ||
<h3 id="react-jsx-to-js-">React (Jsx to Js)</h3> | ||
<p>对于 React, Jupyter 会将默认导出转换变量声明</p> | ||
<p>比如,下面的例子:</p> | ||
<pre><code class="lang-js"><div class="highlight"><pre><span class="kr">export</span> <span class="kd">function</span> <span class="nx">App</span><span class="p">()</span> <span class="p">{</span> | ||
<span class="k">return</span> <span class="o"><</span><span class="nx">span</span><span class="o">></span><span class="nx">React</span><span class="o"><</span><span class="err">/span></span> | ||
<span class="p">}</span> | ||
</pre></div> | ||
|
||
</code></pre> | ||
<p>会被转换成</p> | ||
<pre><code><div class="highlight"><pre><span class="kr">const</span> <span class="nx">__JupyterCellDefaultExportVariable__</span> <span class="o">=</span> <span class="kd">function</span> <span class="nx">App</span><span class="p">()</span> <span class="p">{</span> | ||
<span class="k">return</span> <span class="o"><</span><span class="nx">span</span><span class="o">></span><span class="nx">React</span><span class="o"><</span><span class="err">/span></span> | ||
<span class="p">}</span> | ||
<span class="kr">const</span> <span class="nx">root</span> <span class="o">=</span> <span class="nx">createRoot</span><span class="p">(</span><span class="nx">cellElement</span><span class="p">);</span> | ||
<span class="nx">root</span><span class="p">.</span><span class="nx">render</span><span class="p">(</span><span class="nx">React</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="nx">__JupyterCellDefaultExportVariable__</span><span class="p">))</span> | ||
</pre></div> | ||
|
||
</code></pre><h2 id="-">尾声</h2> | ||
<p>至此,kotlin Jupyter 支持 <code>%js</code> magic 的思路就梳理清楚了。</p> | ||
<p>Echart 的例子截图:</p> | ||
<p><img src="./echars-example.png" alt=""></p> | ||
<p>实际例子,可以看一下<a href="https://github.com/yidafu/kotlin-jupyter-js/blob/main/examples/js-magic.ipynb">examples/js-magic.ipynb</a></p> | ||
<p>欢迎大家试用,反馈问题。</p> | ||
<script src="prism.js" ></script></div></body> | ||
</html> |