Skip to content

Commit

Permalink
Updates
Browse files Browse the repository at this point in the history
  • Loading branch information
yidafu committed Jan 22, 2024
1 parent 499354b commit 5b2e123
Showing 1 changed file with 172 additions and 0 deletions.
172 changes: 172 additions & 0 deletions how-kotlin-jupyter-work.zh-CN.html
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>&lt;script&gt;</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">&quot;[&quot;</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">&quot;[${it.first}, ${it.second}, ${it.third}]&quot;</span> <span class="p">}.</span><span class="n">joinToString</span><span class="p">(</span><span class="s">&quot;,\n&quot;</span><span class="p">)</span> <span class="p">+</span> <span class="s">&quot;]&quot;</span><span class="p">;</span>

<span class="c1">// render to html</span>
<span class="n">HTML</span><span class="p">(</span><span class="s">&quot;&quot;&quot;</span>
<span class="p">&lt;</span><span class="n">div</span> <span class="n">id</span><span class="p">=</span><span class="s">&quot;chartDom&quot;</span> <span class="n">style</span><span class="p">=</span><span class="s">&quot;width: 600px; height: 600px;&quot;</span><span class="p">&gt;</span> <span class="p">&lt;/</span><span class="n">div</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="n">script</span> <span class="k">type</span><span class="p">=</span><span class="s">&quot;module&quot;</span><span class="p">&gt;</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">&quot;https://unpkg.com/echarts@5.4.3/dist/echarts.esm.min.js&quot;</span>
<span class="k">import</span> <span class="nn">&quot;https://unpkg.com/echarts-gl@2.0.9/dist/echarts-gl.min.js&quot;</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">&#39;</span><span class="n">chartDom</span><span class="err">&#39;</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">&lt;/</span><span class="n">script</span><span class="p">&gt;</span>
<span class="s">&quot;&quot;&quot;)</span>
</pre></div>

</code></pre>
<h2 id="don-t-repeat-yourself">Don&#39;t Repeat Yourself</h2>
<p>可以看到上面的例子有好几处模板代码。</p>
<ul>
<li>kotlin 转 Json</li>
<li>创建容器<code>&lt;div&gt;</code>标签</li>
<li>创建<code>&lt;script&gt;</code>标签</li>
</ul>
<p><code>ipython</code> 就支持<code>%js</code>直接写js,cell 执行前会拦截有<code>%js</code>标记的代码,转为<code>&lt;script /&gt;</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">&quot;hellow jupyter js&quot;</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">&quot;&quot;&quot;</span>
<span class="p">&lt;</span><span class="n">script</span> <span class="k">type</span><span class="p">=</span><span class="s">&quot;module&quot;</span><span class="p">&gt;</span>
<span class="k">var</span> <span class="py">hello</span> <span class="p">=</span> <span class="s">&quot;hellow jupyter js&quot;</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">&lt;/</span><span class="n">script</span><span class="p">&gt;</span>
<span class="s">&quot;&quot;&quot;)</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">&quot;bar&quot;</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">&#39;@jupyter&#39;</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">&#39;variable from kotlin&#39;</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">&lt;script </span><span class="na">type=</span><span class="s">&quot;module&quot;</span><span class="nt">&gt;</span>
<span class="kr">const</span> <span class="nx">foo</span> <span class="o">=</span> <span class="s2">&quot;foo&quot;</span>

<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;variable from kotlin&#39;</span><span class="p">,</span> <span class="nx">foo</span><span class="p">)</span>
<span class="nt">&lt;/script&gt;</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">-&gt;</span> <span class="n">JsonNull</span>
<span class="k">is</span> <span class="n">Collection</span><span class="p">&lt;*&gt;</span> <span class="p">-&gt;</span> <span class="n">toJsonElement</span><span class="p">()</span> <span class="c1">// call Collection&lt;*&gt;.toJsonElement()</span>
<span class="k">is</span> <span class="n">String</span> <span class="p">-&gt;</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&lt;*, *&gt; Array&lt;*&gt;, other primary type</span>
<span class="k">else</span> <span class="p">-&gt;</span> <span class="p">{</span>
<span class="k">throw</span> <span class="n">IllegalStateException</span><span class="p">(</span><span class="s">&quot;Can&#39;t serialize unknown type: $this&quot;</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">&lt;*&gt;.</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">-&gt;</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">-&gt;</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(&quot;&quot;&quot; $jsCode &quot;&quot;&quot;&quot;)</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 &#39;@jupyter&#39;;</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">&lt;</span><span class="nx">span</span><span class="o">&gt;</span><span class="nx">React</span><span class="o">&lt;</span><span class="err">/span&gt;</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">&lt;</span><span class="nx">span</span><span class="o">&gt;</span><span class="nx">React</span><span class="o">&lt;</span><span class="err">/span&gt;</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>

0 comments on commit 5b2e123

Please sign in to comment.