SegmentFault 题叶最新的文章
2024-01-21T01:33:23+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
一些手写 WAT(WebAssembly) 的格式的例子
https://segmentfault.com/a/1190000044573734
2024-01-21T01:33:23+08:00
2024-01-21T01:33:23+08:00
题叶
https://segmentfault.com/u/tiye
0
<h3>poetry</h3><p><a href="https://link.segmentfault.com/?enc=jMnYOoHUbVyDEc0Komp5WQ%3D%3D.f96dt83pIchnZstT4Mf3Iwxru0ZUxGleOEKJVtUWaaq6gYPhEL%2FWQqaeUMBAEt6z" rel="nofollow">https://github.com/FantasyInternet/poetry</a></p><p>一门简单的编程语言. runtime 和 stdlib 用 wasm 手写, 内存处理, 数组处理, 字符串处理, 很有学习参考的意义.</p><h3>wat2wasm</h3><p><a href="https://link.segmentfault.com/?enc=uk3Lp49U0B%2FN4WUZYG4xfQ%3D%3D.3b52bA%2FVUQ5ExpuJJF%2FKhAIvCk5desXEm3jFaWtA%2FqEUzNy43YoEGmGTTHBwPc6DoxNF4m5rJ5Bx4A7ko8oU0g%3D%3D" rel="nofollow">https://webassembly.github.io/wabt/demo/wat2wasm/</a></p><p>几个很简单的 demo. 给出了对应的 WASM 二进制注释的对比</p><h3>raw wasm</h3><p><a href="https://link.segmentfault.com/?enc=S%2B7ZxQ2J9TSKkl35vf6Z4A%3D%3D.oGKmAEAYtdoPZ7cUcHT3iShBZD%2F%2B%2FwMW%2FC%2FjCrICfEM498%2FmXdtcCuUgxF5kduhf" rel="nofollow">https://github.com/binji/raw-wasm/</a></p><p>jit, raytrace 之类的一些 demo, 偏底层模拟和算法. 略复杂.</p><h3>hand-crafted-wasm</h3><p><a href="https://link.segmentfault.com/?enc=Nt54nh6vTP6QK7IBfsr2NQ%3D%3D.JJ66%2BHhYnA%2FkwwukvHOHr%2BlF5TtzMJsVE2mL6hZCDQOG8o8LCAryQ%2FbIun2Qus8F31hLuPU43AyxS4tn6BQ9Ug%3D%3D" rel="nofollow">https://github.com/austintheriot/hand-crafted-wasm</a></p><blockquote>Creative coding and other mischief in handwritten WebAssembly, using the WebAssembly .wat text format.</blockquote><p>一些比较复杂的 demo. 调用 Canvas 渲染做绘图.</p><hr><h3>评论</h3><p>临时搜集的. 后边遇到再整理一下, 便于理解更多 WASM 运行的原理.</p>
wgsl 代码的格式化
https://segmentfault.com/a/1190000044359297
2023-11-03T01:02:47+08:00
2023-11-03T01:02:47+08:00
题叶
https://segmentfault.com/u/tiye
0
<blockquote>内容来自:<br><a href="https://link.segmentfault.com/?enc=09KC%2Ficru7hoPmR850W4gw%3D%3D.v4KKCFgXm%2FnP2qYcrovWzfvde%2BNkyDhMcbbh1xvXJGomP69kufCQSusUYEdm2uWreJ6yo%2Bsm%2BORmi9uulb1Vk1WuFjFC5OmydyZ4NPxTqV76R20BJUoR7ySf2x58Dy%2FS" rel="nofollow">https://github.com/wgsl-analyzer/wgsl-analyzer/issues/67#issuecomment-1789388437</a></blockquote><p>最近写 WGSL 比较多, 一直想要一个格式化. 今天在 WGSL Analyzer 的 issue 里看到人问,</p><pre><code class="bash">cargo install --git https://github.com/wgsl-analyzer/wgsl-analyzer wgslfmt
wgslfmt file.wgsl</code></pre><p>结果这个功能直接在对应 VS Code 插件里边已经集成了, 我一直默认没有开启.<br>不过开启以后试了一下, 发现 4 格缩进, 就挺不适合 CoffeeScript 起手的用户的,<br>于是我自己改了一个分支, 就替换了一下缩进, 其他没怎么动..</p><pre><code class="bash">cargo install --git https://github.com/Triadica/wgsl-analyzer wgslfmt</code></pre><p>下载到本地安装的话, 可以用命令:</p><pre><code class="bash">cargo install --path crates/wgslfmt/</code></pre><p>相关配置主要是在 VS Code 的 <code>settings.json</code> 里指定, 虽然默认可能已经是这个配置了,</p><pre><code> "[wgsl]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "wgsl-analyzer.wgsl-analyzer"
},</code></pre><p>替换了命令行, 还需要重新在 VS Code 执行 Reload Window 才能生效. 略奇怪.</p><hr><h3>import 语法</h3><p>试了试 import 语法, 确实有用, 参考:</p><ul><li><a href="https://link.segmentfault.com/?enc=8qnUyqUFQDassUzmGn1tsA%3D%3D.sjv0tVczZQz%2BXHZ36MP%2BasdqMu2jzmO6Zx4c7V6%2BvPo4X9FZ50cyp4lAKWn0BGooV1QBt%2FiIs1%2B2QfVpxLXgEg%3D%3D" rel="nofollow">https://github.com/wgsl-analyzer/wgsl-analyzer/issues/108</a></li><li><a href="https://link.segmentfault.com/?enc=X5n8KcpY8eSCyfhLGvCvXA%3D%3D.AHPVL2%2BnaYDoQXjvVC4fENb%2B99p0kn9sbsOIZ%2FMRrDtYZZ0U98vl9Kfy7USujj9IgPOxdrYbMVbQ8Odhj%2BMJo3P37%2BBkTioAICBybayGZjgLfxQbN5tXESVtk%2Bqn0av6" rel="nofollow">https://github.com/wgsl-analyzer/wgsl-analyzer/tree/main/editors/code#custom-imports</a></li></ul><p>几个步骤,</p><ul><li>VS Code 里边配置 <code>wgsl-analyzer.customImports</code>, 建议 <code>file://</code> 协议,</li><li>代码里通过 <code>#import a::b</code> 的语法引用</li></ul><p>然后现在最新版本是有 bug 的需要按照 issue 提示 revert 掉一个 commit 才行.. 希望 bug 能快点修掉... <a href="https://link.segmentfault.com/?enc=wmK05to4IYphKD40NQ9d8w%3D%3D.SS2edxZRMLmNKgBTMwdyvsD3eLs%2FTmKSReJ8kHo6Q86cR5Y0%2FG2iSE2sUrwnpbW0DlMjnMm0QYDaz1rSr%2FonRg%3D%3D" rel="nofollow">https://github.com/wgsl-analyzer/wgsl-analyzer/issues/100</a></p>
Node.js 发出请求走 Proxyman 代理调试的 tip
https://segmentfault.com/a/1190000044067284
2023-08-01T10:20:24+08:00
2023-08-01T10:20:24+08:00
题叶
https://segmentfault.com/u/tiye
0
<p>一些情况需要从请求来调试, 所以找了一个方案让 Node.js 请求走代理.<br>基于 <a href="https://link.segmentfault.com/?enc=BR75AOmc1ApBHKK99OS2xA%3D%3D.VJ6TMZP%2B%2B0b4wpxz2EYWP%2BaRvwFqrozxmlibLJ%2Fp8pm5qN7l1JVOem3sbbFDhF0R" rel="nofollow">https://stackoverflow.com/a/62174988/883571</a> 验证了这样一个方案:</p><pre><code class="js">import proxy from "node-global-proxy";
proxy.setConfig({
http: "http://localhost:9090",
https: "http://localhost:9090",
});
proxy.start();</code></pre><p>我使用的是 Proxyman, 端口是 9090, 并且提供的是 HTTP 代理, 两个参数都用 <code>http:</code> 地址.<br>代码用了 <code>import</code> 由于我使用的是 mjs 文件执行.</p><p>这个时候直接运行, 发出请求, Node.js 会报错, 证书验证不通过:</p><pre><code> cause: Error: unable to verify the first certificate
at TLSSocket.onConnectSecure (node:_tls_wrap:1539:34)
at TLSSocket.emit (node:events:513:28)
at TLSSocket.emit (node:domain:489:12)
at TLSSocket._finishInit (node:_tls_wrap:953:8)
at TLSWrap.ssl.onhandshakedone (node:_tls_wrap:734:12) {
code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE'
}</code></pre><p>本地开发的时候可以先通过环境变量临时关闭证书验证的行为:</p><pre><code class="bash">export NODE_TLS_REJECT_UNAUTHORIZED=0</code></pre><p>再重新运行脚本时, 会有警告提示, 请求会正常通过:</p><pre><code>(node:93084) Warning: Setting the NODE_TLS_REJECT_UNAUTHORIZED environment variable to '0' makes TLS connections and HTTPS requests insecure by disabling certificate verification.</code></pre><p>然后在 Proxyman 当中可以开始抓取请求内容了. 这部分不赘述.</p><h3>其他</h3><p>未知项,</p><ul><li>如果 Node.js 启动一个服务, 外部调用时是否有办法统一将其展示的 Proxyman?</li><li>不通过环境变量关闭证书验证, HTTPS 直接代理应该怎样处理?</li></ul>
尝试 WebGPU 过程中掉的一些坑
https://segmentfault.com/a/1190000043639587
2023-04-10T00:20:54+08:00
2023-04-10T00:20:54+08:00
题叶
https://segmentfault.com/u/tiye
0
<p>需要参考, 建议看 WebGPU Samples:<br><a href="https://link.segmentfault.com/?enc=%2BALvMaV5rE2q8BWzAEi6nw%3D%3D.yayCooZVhf34%2BaaxoBmVH8mVY7GtzRCw4%2BLt5oeH59J5qNbQQNXXqkXW3alKXwDy3UaHg5AdcYYMCtUQsQKq9A%3D%3D" rel="nofollow">https://webgpu.github.io/webgpu-samples/samples/computeBoids</a></p><hr><h3>buffer 编码对齐问题</h3><p>uniform buffer 的编码规则. 数据会按照大小对齐, 但是编码的时候</p><p><a href="https://link.segmentfault.com/?enc=2GR2JzylS4KuYKG8DM9C7w%3D%3D.2u0q%2BA%2Bpkr3z7YVvq%2F3vLDEaE3aadrg%2BUuAH5rkeMK2qKl32TW%2BDC5ab%2FlOW%2FkfuDhl7f7Sy5o2dBoPO12eZZQ%3D%3D" rel="nofollow">https://www.w3.org/TR/WGSL/#address-space-layout-constraints</a></p><p>可以试试自己加上 padding 来 buffer</p><p><a href="https://link.segmentfault.com/?enc=G2q0x9miF6KNxzzjFPw%2FAw%3D%3D.C9omNjkpt%2BZ44%2B8GOIMrBr9sj%2F4RUIBUQvIBfRJ4UxWZYhcPqRoxWyth7Sd7CrNZ4ZUXKgY3iuvZyuMYyxHdSsgXqJQAhypVC3LeCdddAIudwZojgS8EHnknUqxWQIXbIadvsvbg7ZR4yfLCDhhwK24qde9DVKWXREh0hpT5hg0%3D" rel="nofollow">https://stackoverflow.com/questions/74186801/is-there-any-way-to-enforce-a-16-byte-alignment-for-a-uniform-buffer-in-glsl</a></p><p>按照文档说的, 不止对 uniform buffer 是这样...</p><p>听说可以用工具做一下可视化, 但是我还没有尝试过...<br><a href="https://link.segmentfault.com/?enc=yp97AOWl3q9hKfsWwDhpcQ%3D%3D.N%2Ft1SWe1XOzmDsLBb0oxhpF%2FoN7F%2BiyocsOqSEKe33yfFXfTIrWvZ3tpt5qCCiAtPCltWMdU%2FSC1ZyxLlVmUD5%2BKcd98%2BvNIJW%2Fvx6KcCVyW1C9vem1lbNPLT%2BvAZhb9" rel="nofollow">https://webgpufundamentals.org/webgpu/lessons/resources/wgsl-...</a></p><hr><p>Storage Texture, 在 compute shader 计算时会用到, 结果遇到问题, 发现也是 padding 的问题.</p><p>这个例子当中默认使用的是 <code>vec2f</code> 表示二维的点,<br><a href="https://link.segmentfault.com/?enc=jvo9AUiFwYPorflImCvK9A%3D%3D.vL1wZrscuPmP79lSVlWoWAWdw%2FmrZHUrbvGyBsIcHr7VXvkPl9Zp%2FI53DWSRJFMGVFnBNSuUWAmDA8ZjlT770Rqavgs4kpylNW9PrHqw8CI%3D" rel="nofollow">https://webgpu.github.io/webgpu-samples/samples/computeBoids#...</a><br>我需求当中的改成了 <code>vec3f</code> 的三维点, 结果发现数据不对, 包括 0 位也出现了数据,<br>突然反应过来可能是 padding 的问题. 修改以后果然, 两个 <code>vec3f</code> 数据之间需要有一个 padding.</p><hr><h3>没有 FBO 怎么办</h3><p><a href="https://link.segmentfault.com/?enc=lOCwxnWEzAbClGvhOY%2F1uw%3D%3D.TKK9Ypw%2B9KiOn3F0TeS6BNCD5nuMj785QNbkZpeSm1HPgQisB1Qw%2FTQ5cnJhPQCTeGFJP9fUl5%2BaTQuPG1%2Fa5cuH1RPL%2FBGlmBQAyn6%2FgR8%3D" rel="nofollow">https://www.cnblogs.com/onsummer/p/the-missing-fbo-and-rbo-in...</a></p><p>整体思路可以参考 blur 的做法, 至少能跑通的,</p><p><a href="https://link.segmentfault.com/?enc=2mnaDxtOF8CihaXBU22KLQ%3D%3D.Z8Puw2h4fBsRsAxZxOXEv9EYRKa%2F%2FYizVPfGI%2BoJ%2FGMrVQR1dg%2FgQniXayrrHdzhuQ%2FvMbwKiSfg3rtPHqh3psUddBfNqzGkMtQCsg46dwSggEQtLZ8ec1Ec0xmtfqc1A1bsQhPwk0L779iSlcjt8w%3D%3D" rel="nofollow">https://webgpu.github.io/webgpu-samples/samples/imageBlur#../...</a></p><p>整个思路是通过 Storage Buffer 和 Pipeline 自己拼接一套流程,<br>Storage Buffer 可以用来存储中间状态, 从而写出 ping/pong buffer 的用法.<br>render pipeline 相当于一种特殊的 compute pipeline, WebGPU 是比较基础的 API 可以拼出来对应功能.</p><hr><p>binding 的 expectation 有的是跟 WGSL 里定义的 texture 类型定义的, 在 js 代码中看不到. 比如 multisampilsed.</p><hr><h3>Buffer Resize</h3><p>调整 <code>canvas.style.height</code> 之后, <code>canvas.height</code> 也需要更新, 而且也要考虑 pixel ratio.<br>canvas context 的 <code>getCurrentTexture</code> 获取的 size 这时也是要改变的.</p><p>depth texture 也类似, 当窗口大小发生变化, canvas 大小发生变化, depth texture 也需要对应更新. 不然会提示大小不对应.</p><hr><h3>vec3f 缩写</h3><p><code>vec3f</code> 是 <code>vec<f32></code> 的缩写, 这个还是看教程的时候发现的,<br>对应的 <code>vec2f</code> <code>vec4f</code> 也用.<br>不过 wgsl analyzer 使用的时候发现还没识别号这个缩写.</p><hr><p>TODO</p>
Nginx querystring 转写的两个例子
https://segmentfault.com/a/1190000043338766
2023-01-16T12:30:00+08:00
2023-01-16T12:30:00+08:00
题叶
https://segmentfault.com/u/tiye
0
<p>转发请求时需要两个功能, 一个是去掉 querystring 上特定的字段, 一个是替换掉一个字段的值.</p><p>参考两篇文章得到一个可用的方案,</p><p><a href="https://link.segmentfault.com/?enc=HlDa%2F1yZaaVD134bCYA7Cw%3D%3D.3%2F2NLsS0glGi1FNtFvNRRIi6tHEvDMnvLoGHb%2BdqWnrWuSo%2FO8WfxhXlPZ1%2BPwfuDkZXePG7ZpD1s5PcJZCYBw%3D%3D" rel="nofollow">https://itecnotes.com/server/...</a><br><a href="https://link.segmentfault.com/?enc=5z6mlt9O5H8LMILjN%2BOFWg%3D%3D.K27c7Q%2FUqULnua6dOr%2FTV2UR0ni7feAeDFdQwSs2rFyruy31Q0squjnYc1F%2BdfisQ9Q1%2BmwCL1Hx9PFZ7Z%2FYOeydCT%2BKUYPxjSrYdKg9BzI%3D" rel="nofollow">https://zzyongx.github.io/blo...</a></p><p>主要的思路是通过正则, 对 <code>$args</code> 反复进行替换, 得到自己想要的结果,</p><pre><code class="nginx">upstream myhost {
server myupstream.com:443;
}
# 强制 log 输出到 stdout
access_log /dev/stdout;
error_log /dev/stderr info;
server {
listen 5555;
location /my/api {
# 目前接口都转发到该域名
set $target_host "myupstream.com";
# set $target_host REPLACED_HOST;
set $target_data "REPLACED_DATA";
# set $target_data "dev";
# 正则处理的不是数据结构, 各种组合需要手动遍历
# 去掉后边的 extraKey
if ( $args ~* ^(.*)&extraKey=[\w\d]+(.*)$ ) {
set $args $1$2;
}
# 去掉开头的 extraKey
if ( $args ~* ^extraKey=[\w\d]+(.*)$ ) {
set $args $2;
}
# 清除 myData
if ( $args ~* ^(.*)&myData=\w+(.*)$ ) {
set $args $1$2;
}
# 清除开头的 myData
if ( $args ~* ^myData=\w+(.*)$ ) {
set $args $1;
}
# 清除后设置一个指定一个参数
set $arg_myData "$target_data";
proxy_set_header Host $target_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Real-IP $remote_addr;
proxy_pass_request_headers on;
# HTTP 转 HTTPS 用
proxy_ssl_server_name on;
set $args "$args&myData=$arg_myData";
# 调试用
# return 200 "$target_host/api$uri$is_args$args&myData=$arg_myData";
proxy_pass https://myhost/api$uri$is_args$args;
}
}</code></pre><p>注意 <code>$arg_myData</code> 虽然是从 querystring 解析出来的 field, 但是修改了只会增加, 而不是替换. 直接增加可能出现两个 <code>myData</code> 的请求, 不能保证服务器解析和处理都是对的. 所以应该是先清除, 再自己加上.</p><p>功能不难, 调试时报错不够透明, 一搬用 <code>return 200 "content";</code> 强行返回查看. 其他看不到的中间过程只能尽量抓出来看了.</p>
Vue3 JSX 写法笔记
https://segmentfault.com/a/1190000043153659
2022-12-26T18:21:13+08:00
2022-12-26T18:21:13+08:00
题叶
https://segmentfault.com/u/tiye
0
<p>Vue3 是可以用 JSX 语法直接写的, 大体可以从 <a href="https://link.segmentfault.com/?enc=oXYE%2Fo%2BkZTHBzxY4f8jxHg%3D%3D.qOL0iO30dJTYXs62V0YcPduZ94EP0lKgVRCzaJHdV%2Bk%3D" rel="nofollow">https://sfc.vuejs.org/</a> 的示例看到,<br>其中 <code><div></code> 会编译为 <code>h('div')</code>, 具体参考 <a href="https://link.segmentfault.com/?enc=lTkKDrBOeGcHHixkHqdTDg%3D%3D.7XDk3%2FXyUVRKDubuCRnupDNw8GDZpaep8SAUT9TDJZMwlmw6UTH89adbMjWsri76EuWjxlImLGSfHqnmfKpkQA%3D%3D" rel="nofollow">https://vuejs.org/guide/extras/render-function.html</a> .</p><p>完整的组件定义形如:</p><pre><code class="tsx">import { defineComponent, PropType } from 'vue';
import { onMounted, ref, watch } from 'vue';
const App = defineComponent({
name: "App"
props: {
appId: {
type: String as PropType<string>,
default: '',
},
},
emits: [],
setup(props, {emit, expose, slots}) {
return () => (
<div>TODO</div>
);
},
});
export default App;</code></pre><p>其中</p><ul><li><code>name</code> 调试中组件的名字,</li><li><code>props</code> 需要用这样的写法用 Object 格式传入, 类型部分用 <code>PropType<T></code> 做标记,</li><li><code>emits</code> 可以用字符串格式指定事件, 而 <code>emit</code> 函数从参数中拿到,</li><li><code>slots</code> 也是从参数当中拿到,</li><li><code>expose</code> 也是从参数当中得到的,</li><li>注意最终的 render 函数, 范围与 <code>setup</code> 函数有区别, 其中 <code>setup</code> 函数只会被执行一次, 而 <code>render</code> 函数可能多次执行. 而需要响应式追踪的逻辑, 需要写在 <code>setup</code> 函数里边, 否则行为不能达到预期,</li></ul><p>有了 JSX, 原有的 <code>v-if</code> 和 <code>v-else</code> 可以和 React 一样直接写了,</p><pre><code class="jsx"><div>
{!!a ? <span>true</span> : null}
</div></code></pre><p><code>v-model</code> 较为特殊, 转换后需要手动绑定 <code>modelValue</code> 的行为:</p><pre><code class="jsx"><A modelValue={a.value} onUpdate:modelValue={(v) => a.value = v} /></code></pre><p><code>expose</code> 的用法, 传入一个对象, 参考 <a href="https://link.segmentfault.com/?enc=RLwaS%2FwMf1ZpkbitFXE%2BRQ%3D%3D.qu98dU3Pt9aiYUIcClRI10OOPLH23N33spqN%2BKp81b20xElOtxGA60bbwvni1tavgdcLbt57M5CSh6VFhqCdGQ%3D%3D" rel="nofollow">https://www.vuemastery.com/blog/understanding-vue-3-expose/</a></p><pre><code class="jsx">expose({ reset })</code></pre><p><code>@click</code> 写法统一变成 <code>on</code> 加上大写首字符,</p><pre><code class="jsx"><div onClick={() => console.log("TODO")} ></code></pre><p><code>v-slots</code> 用法比较复杂, 参考 <a href="https://link.segmentfault.com/?enc=bGgYx3oHBcbymzCneJMoMg%3D%3D.%2BtZoMdCZ2Gya5VnstnsN4fqZwa4QHVIEmQF2OL32K08EImgHOPkVC4fSR2sU%2FXPT" rel="nofollow">https://github.com/vuejs/babel-plugin-jsx/#slot</a> :</p><pre><code class="jsx">const A = (props, { slots }) => (
<>
<h1>{ slots.default ? slots.default() : 'foo' }</h1>
<h2>{ slots.bar?.() }</h2>
</>
);</code></pre><p>有个 slot/template 写法比较绕, 定制插槽的写法:</p><pre><code class="vue"><NSelect>
<template #optionEmptyRender>
<div>Demo</div>
</template>
</NSelect></code></pre><p>写成:</p><pre><code class="jsx"><NSelect
v-slots={{
optionEmptyRender: () => {
return (
<div>Demo</div>
);
},
}}
/></code></pre><h3>提取组件 props 的类型</h3><p>对组件进行封装的时候, 会遇到需要获取 Component Props 进行复用的场景,<br>网上有找到用 TypeScript 泛型来获取组件 props 类型的写法:<br><a href="https://link.segmentfault.com/?enc=lqF4Bhxhy8G%2FyKuydrOUIw%3D%3D.mXwVnAxtZbE8w5ptP1lzwyurTKArLwQumgM%2BvjYEgiTHWMWnBQd2ZoxSQbmPZ2qv" rel="nofollow">https://stackoverflow.com/q/68602712/883571</a></p><pre><code class="ts">import MyComponent from "./mycomponent.vue";
// 得到这个类型
type MyComponentProps = InstanceType<typeof MyComponent>["$props"];</code></pre><h3>Directive 写法</h3><p><a href="https://link.segmentfault.com/?enc=nZgdgoxLraSVpaZl98%2F3WQ%3D%3D.tAMhvTU2pVUwA%2BWlPBZIDcn1yQo4ts41ZAwvhIVKDvmOBe%2FliwrwgIeapUUjBcF%2BkwIMumOrvVxZOEn0EJD8FcqR3yvmCFxexepNsnTax8w%3D" rel="nofollow">https://vuejs.org/guide/extras/render-function.html#custom-di...</a></p><h3>TODO</h3>
尝试在 Vue composition API 中返回包含 JSX
https://segmentfault.com/a/1190000042975667
2022-12-05T11:42:48+08:00
2022-12-05T11:42:48+08:00
题叶
https://segmentfault.com/u/tiye
0
<p>对应 <a href="https://link.segmentfault.com/?enc=NMCJkJfTPtZCLy%2FJKrfmHg%3D%3D.HkCAMl20no1ZCZp5PlLzQ8Q2e%2FvjDYjdkp%2FiKxHQ8jxoCpOnMPnVnC%2FvGE6KibQ8treiCnSV4lkIFn8xiK36iakeet92cpW3XTd5%2BMJ%2B2tRpsSZkmvdfHKb54kqQ2tdv" rel="nofollow">https://blog.bitsrc.io/new-re...</a></p><p>抽逻辑到一个 composition API:</p><pre><code class="jsx">/**
* 将逻辑统一抽到一个文件当中, 方便逻辑的复用
* 没有确认清楚是否可能带来性能方面的问题, 以及对调试工具不如组件友好
*/
import { computed, ref } from 'vue';
import { NDialog, NLink } from '<ui-package>';
/** 用法, 作为 Hooks API 使用
* const p = warnDeletePublishment()
*/
export const warnDeletePublishment = () => {
const visible = ref(false);
const showDialog = () => {
visible.value = true;
};
const dom = computed(() => {
return (
<NDialog
modelValue={visible.value}
onUpdate:modelValue={(v) => {
visible.value = v;
}}
v-slots={{
footer: () => {
return (
<div class="flex flex-row justify-between">
<span />
<NLink onClick={() => (visible.value = false)}>关闭</NLink>
</div>
);
},
}}
>
<div>
warning content
</div>
</NDialog>
);
});
return {
/** 用法
* p.show() */
show: showDialog,
/** 用法
* <DynamicDom dom={p.dom} />
*/
dom,
};
};</code></pre><p>用法非常简短, 状态和 UI 都封装了:</p><pre><code class="vue"><template>
<DynamicDom :dom="warnDeletePlugin.dom"></DynamicDom>
</template>
<script setup lang="ts">
const warnDeletePlugin = warnDeletePublishment();
function handleRemove(publishment: PackagePublishment) {
warnDeletePlugin.show();
}
</script></code></pre><p>渲染动态的 UI, 需要一个组件包装:</p><pre><code class="jsx">/**
* 这种写法可能带来了额外的性能问题?
*
* 配合 plugin 写法使用, 用于渲染 `ComputedRef<VNode>` 到 template 写法的页面当中
*/
import { ComputedRef, defineComponent, PropType, VNode } from 'vue';
import { onMounted, ref, watch } from 'vue';
const DynamicDom = defineComponent({
props: {
dom: {
required: true,
type: Object as PropType<ComputedRef<VNode>>,
},
},
setup(props) {
return () => props.dom.value;
},
});
export default DynamicDom;</code></pre><hr><h2>使用 expose</h2><p>Vue 也支持直接从外部操作 Component 方法, 借助 <code>expose</code>:</p><p><a href="https://link.segmentfault.com/?enc=cGIRWRyzkzB4UkwsEQSldQ%3D%3D.4MKa2lUOjStGoJmLrUeTlP82MBJTzhgZhfMHFzfJhm4GkW9RyEEdgjgD3FUKZkwT%2Bnh1xIH8Pg7p0B8HsmP2gQ%3D%3D" rel="nofollow">https://www.vuemastery.com/bl...</a></p><p><a href="https://link.segmentfault.com/?enc=dsFXdmDF1mz23c%2B0GX7%2ByQ%3D%3D.1Tu%2FnS54upmGCKDzUzYbjRBmPYq%2B4ghW%2Br2d7N90qHwbZAG7OS5zf67O3tlGRPHfPjVuEQHKruEz3MI%2BA5sOoDl7ZInYWhJI4oBApmvZLcg%3D" rel="nofollow">https://vuejs.org/api/composi...</a></p>
Haskell Monoid(幺半群)的介绍
https://segmentfault.com/a/1190000041738420
2022-04-21T17:06:26+08:00
2022-04-21T17:06:26+08:00
题叶
https://segmentfault.com/u/tiye
0
<blockquote>翻译自 <a href="https://gist.github.com/cscalfani/b0a263cf1d33d5d75ca746d81dac95c5">https://gist.github.com/cscal...</a></blockquote><p>为什么程序员应该关心 Monoids?因为 Monoids 是一种在编程中反复出现的常见模式。当模式出现时,我们可以将它们抽象化并利用我们过去所做的工作。这使我们能够在经过验证的稳定代码之上快速开发解决方案。</p><p>将"可交换性"添加到 Monoid(Commutative Monoid),你就有了可以并行执行的东西。随着摩尔定律的终结,并行计算是我们提高处理速度的唯一希望。</p><p>以下是我在学习 Monoids 后学到的。它未必完整,但希望能够对于向人们介绍 Monoids 有所帮助。</p><h2>Monoid 谱系</h2><p>Monoid 来自数学,从属于代数结构的谱系。因此,从头开始并逐步展开到 Monoids 会有所帮助。 实际上,我们进一步可以推到"群"(Groups).</p><h3>Magma(元群)</h3><p>Magma 是一个集合以及一个必须闭合的二元运算:</p><blockquote><em>∀ a, b ∈ M : a • b ∈ M</em></blockquote><p>如果将二元运算应用于集合的任意 2 个元素时,它会生成集合的另一个成员,则该二元运算是封闭的。 (这里 <code>·</code> 表示二元运算)</p><p>Magma 的一个示例是 Boolean 和 <code>AND</code> 运算的集合。</p><h3>Semigroup(半群)</h3><p>Semigroup 是具有一个附加要求的 Magma。二元运算对于集合的所有成员必须是"可结合"的:</p><blockquote><em>∀ a, b, c ∈ S : a · (b · c) = (a · b) · c</em></blockquote><p>一个 Semigroup 的例子是"非空字符串"和"字符串拼接"运算的集合。</p><h3>Monoid(幺半群)</h3><p>Monoid 是包含一个附加条件的 Semigroup。集合中存在一个"幺元"(Neutral Element),可以使用二元运算将其与集合的任何成员结合,而产生属于相同集合的成员。</p><blockquote><em>e ∈ M : ∀ a ∈ M, a · e = e · a = a</em></blockquote><p>一个 Monoid 的例子是字符串集合以及"字符串拼接"运算。注意,集合中添加的空字符串是"幺元",并使 Semigroup 称为 Monoid。</p><p>另一个 Monoid 的示例是非负整数和加法运算的集合。幺元为 <code>0</code>。</p><h3>Group(群)</h3><p>一个 Group 是包含一个附加条件的 Monoid. 集合中存在"逆",使得:</p><blockquote><em>∀ a, b, e ∈ G : a · b = b · a = e</em></blockquote><p>其中 <code>e</code> 是幺元.</p><p>一个 Group 的例子是整数和加法运算的集合。 "逆"是负数,幺元是 <code>0</code>。</p><p>通过允许负数,我们将上面的 Monoid 的第二个示例变成了一个 Group。</p><p>引用: <a href="https://link.segmentfault.com/?enc=IN6i4u55Uz%2BrYj8%2FjvpT7Q%3D%3D.V2XagXmrbAgmHKm2dvFaaaYLcfrhZGwBYI1DbUoQVr36hZAPoCnj3cN2VVabSKL2" rel="nofollow">Math StackExchange question: What's the difference between a monoid and a group?</a></p><h2>Haskell 中的 Monoids</h2><h3>Monoid typeclass(类型类)</h3><p>在 Haskell Prelude (基于 <code>GHC.Base</code>)中, Monoid typeclass 定义为:</p><pre><code class="haskell">class Monoid a where
mempty :: a
-- ^ 'mappend' 的幺元
mappend :: a -> a -> a
-- ^ 一个"可结合"的操作
mconcat :: [a] -> a
-- ^ 使用 monoid 来折叠一个列表.
-- 对于大多数类型,会使用 'mconcat' 的默认定义
-- 但该函数包含在类定义中,所以可以为特定类型提供优化的版本.
mconcat = foldr mappend mempty</code></pre><p>其中 <code>mempty</code> 是幺元, <code>mappend</code> 是二元可组合操作符. 这足以成为 Monoid,但为了方便添加了 <code>mconcat</code>。 它有一个默认实现,使用二元运算 <code>mappend</code> 从幺元 <code>mempty</code> 开始折叠列表。</p><p>实例可以覆盖这个默认实现,我们稍后会看到。</p><h3>Monoid 实例</h3><h4>Monoid <code>()</code></h4><p>一个简单例子是仅包含 <code>()</code> 的集合:</p><pre><code class="haskell">instance Monoid () where
mempty = ()
_ `mappend` _ = ()
mconcat _ = ()</code></pre><p>这里集合只包含一个幺元 <code>()</code>。 所以 <code>mappend</code> 并不真正关心参数,只会返回 <code>()</code>。 意味着唯一有效的参数始终是 <code>()</code>,因为我们的集合只包含 <code>()</code>。</p><p>此外,为了提高效率,<code>mconcat</code> 函数被覆盖从而忽略集合中的元素列表,因为它们都是<code>()</code>,因此它只返回<code>()</code>。 请注意,如果此处省略了 <code>mconcat</code>,由于 <code>mappend</code> 的实现,默认实现将产生相同的结果。</p><h5>Monoid <code>()</code> 用例</h5><p>用这个 Monoid 本身做不了做多少事情。</p><pre><code class="haskell">n :: ()
n = () `mappend` ()
ns :: ()
ns = mconcat [(), (), ()]</code></pre><h4>Monoid [a]</h4><p>任意列表的 Monoid:</p><pre><code class="haskell">instance Monoid [a] where
mempty = []
mappend = (++)
mconcat xss = [x | xs <- xss, x <- xs]</code></pre><p><code>mappend</code> 是"拼接"运算,这意味着幺元 <code>mempty</code> 只能是空列表,<code>[]</code>。</p><p>着重要意识到 <code>mconcat</code> 从集合中获取一份"元素"的列表,这里是"列表的列表"。因此,它需要一个"列表的列表",因此参数名称为 <code>xss</code>。</p><p>我怀疑 List Comprehensions 比 <code>foldr</code> 更有效,否则没有理由实现 <code>mconcat</code>。</p><p>如果我们想一下,<code>foldr</code> 将重复用 2 个列表调用的 <code>mappend</code>,由于对每个迭代返回的中间列表中的元素进行重复处理,因此效率不高。</p><p>使用 List Comprehension 将是一个低级操作,很可能只访问每个子列表的每个元素一次。</p><h5>Monoid [a] 用例</h5><pre><code class="haskell">as :: [Int]
as = [1, 2, 3]
bs :: [Int]
bs = [4, 5, 6]
asbs :: [Int]
asbs = mconcat [as, bs] -- [1, 2, 3, 4, 5, 6]</code></pre><h4>(Monoid a, Monoid b) => Monoid (a, b)</h4><p>任意 Monoid 的 2 元组的 Monoid:</p><pre><code class="haskell">instance (Monoid a, Monoid b) => Monoid (a,b) where
mempty = (mempty, mempty)
(a1,b1) `mappend` (a2,b2) = (a1 `mappend` a2, b1 `mappend` b2)</code></pre><p>起初,<code>mempty</code> 的定义似乎令人困惑。 乍一看,该定义可能会被误解为递归定义。</p><p>实际上这个元组中的第一个 <code>mempty</code> 是 <code>a</code> 类型的 <code>mempty</code>。第二个 <code>mempty</code> 是 <code>b</code> 类型的 <code>mempty</code>。</p><p>想象一下 <code>a</code> 是 <code>()</code> 而 <code>b</code> 是 <code>[Int]</code>。 那么 <code>mempty</code> 将是 <code>( (), [] )</code>,即第一个是 <code>()</code> 的 <code>mempty</code>,第二个是 <code>[Int]</code> 的 <code>mempty</code>。</p><p><code>mappend</code> 的实现非常简单。 它为 <code>a</code> 和 <code>b</code> 执行一个 <code>mappend</code>,返回一个 <code>(a, b)</code> 的 2 元组。 因为 <code>a</code> 和 <code>b</code> 都是 Monoids,所以 Magmas 和 Monoids 的闭合约束得以延续。</p><h5>Monoid (a, b) 用例</h5><pre><code class="haskell">p1 :: ((), [Int])
p1 = ((), [1, 2, 3])
p2 :: ((), [Int])
p2 = ((), [4, 5, 6])
p1p2 :: ((), [Int])
p1p2 = mconcat [p1, p2] -- ((), [1, 2, 3, 4, 5, 6])</code></pre><h4>Monoid b => Monoid (a -> b)</h4><p>"接受一个或多个参数, 返回 Monoid, 的任意函数"的 Monoid:</p><pre><code class="haskell">instance Monoid b => Monoid (a -> b) where
mempty _ = mempty
mappend f g x = f x `mappend` g x</code></pre><p>这个定义如何处理带有多个参数的函数并不明显。可能需要给点提醒。</p><p>函数注解是<strong>右结合</strong>,即它们在右侧结合:</p><pre><code class="haskell">f :: Int -> (Bool -> String) -- 不必要的括号
f s1 s2 = s1 ++ s2</code></pre><p><code>Int -> (Bool -> String)</code> 等价于 <code>Int -> Bool -> String</code>,这就是我们不包含括号的原因。"右结合性"提示了这一点。</p><p>记住 <code>String</code> 等价于 <code>[Char]</code>,我们知道 <code>f</code> 最终会返回一个 Monoid,因为我们已经在上面看到了 <code>Monoid [a]</code>。</p><p>但没那么快。 我们首先必须按照 Monoid 实例中定义的 <code>a -> b</code> 来分解注解:</p><pre><code class="haskell">Int -> (Bool -> String)
a -> b</code></pre><p>这里 <code>b</code> 必须是 Monoid. 得益于 <code>Monoid (a -> b)</code>,它是的。</p><p>现在查看 <code>b</code>,我们得到:</p><pre><code class="haskell">(Bool -> String)
( a -> b )</code></pre><p>因此,重新应用 <code>Monoid (a -> b)</code> 能处理具有多个参数的函数,例如:</p><pre><code class="haskell">Int -> (String -> (Int -> String))
a -> ( b )
a -> (a' -> ( b' ))
a -> (a' -> (a'' -> b'' )</code></pre><p>这里 <code>b</code> 是 Monoid, 因为 <code>b'</code> 是 Monoid, 也因为 <code>b''</code> 是 <code>String</code> 是 Monoid, 还因为 <code>String</code> 是 <code>[Char]</code> 并且我们之前看到所有列表都是 Monoids。</p><p>再看定义:</p><pre><code class="haskell">instance Monoid b => Monoid (a -> b) where
mempty _ = mempty
mappend f g x = f x `mappend` g x</code></pre><p>如愿地 <code>mempty</code> 的定义现在更有意义了。 <code>mempty</code> 属于 <code>a -> b</code> 类型,这就是它接收单个参数的原因。 它忽略参数并简单地返回类型为 <code>b</code> 的 <code>mempty</code>。</p><p>对于 <code>Bool -> String</code> 类型的函数,<code>mempty</code> 是 <code>[]</code>,即 <code>Monoid [a]</code> 的 <code>mempty</code>。</p><p>对于类型为 <code>Int -> Bool -> String</code> 的函数,<code>mempty</code> 是递归的,即它首先以 <code>Bool -> String</code> 类型返回自身,因而会返回 <code>[]</code>。</p><p>注意 <code>a</code> 在这里是无关紧要的。 事实上,函数的所有输入类型都是无关紧要的。 这里唯一重要的是返回值的类型。 这就是为什么只有 <code>b</code> 必须是 Monoid。</p><p>因此,以下函数类型将具有 <code>mempty</code> 最终返回 <code>[]</code>,因为它们都返回 <code>String</code>:</p><pre><code class="haskell">Int -> String
Int -> Int -> String
Int -> Bool -> Int -> Double -> String</code></pre><p>类似地,<code>mappend</code> 将单个参数应用于全部两个函数,然后调用 <code>b</code> 的 <code>mappend</code>。</p><p>对于类型为 <code>String -> String</code> 的函数,<code>mappend</code> 使用输入 <code>String</code> 调用全部两个函数,然后为 <code>Monoid [a]</code> 的 <code>String</code> 调用 <code>mappend</code>,即 <code>(++)</code>。</p><p>对于类型为 <code>String -> String -> String</code> 的函数,<code>mappend</code> 使用<strong>第一个</strong>输入参数 <code>String</code> 调用全部两个函数,然后为 <code>String -> String</code> 调用 <code>mappend</code>,它是 <code>Monoid (a -> b)</code>,即它本身。</p><p>再接着,使用<strong>第二个</strong>输入参数 <code>String</code> 调用全部两个函数,然后对类型为 <code>Monoid [a]</code> 的 <code>String</code> 调用 <code>mappend</code>,也即调用 <code>(++)</code>。</p><h5>Monoid (a -> b) 用例</h5><pre><code class="haskell">import Data.Monoid ((<>))
parens :: String -> String
parens str = "(" ++ str ++ ")"
curlyBrackets :: String -> String
curlyBrackets str = "{" ++ str ++ "}"
squareBrackets :: String -> String
squareBrackets str = "[" ++ str ++ "]"
pstr :: String -> String
pstr = parens <> curlyBrackets <> squareBrackets
astr :: String
astr = pstr "abc"</code></pre><p>注意 <code><></code> 操作符在 <code>pstr</code> 中使用。 这个操作符是从 <code>Data.Monoid</code> 导入的,是 <code>mappend</code> 操作的别名(中缀)。</p><p>如果你回顾 Monoid 的 <code>class</code> 定义,你会看到 <code>mappend</code> 的类型是 <code>a -> a -> a</code>。</p><p>由于 <code>parens</code> 和 <code>curlyBrackets</code> 都具有类型 -> <code>String -> String</code>,因此 <code>parens <> curlyBrackets</code> 将具有 <code>String -> String</code> 类型,<code>parens <> curlyBrackets <> squareBrackets</code> 也将具有该类型。</p><p><code>pstr</code> 将接收 <code>String</code> 并将其应用于 <code>parens</code>、<code>curlyBrackets</code> 和 <code>squareBrackets</code> 拼接这些调用的结果。</p><p>因此,<code>astr</code> 是<code>(abc){abc}[abc]</code>。</p><p>如果要应用的函数数量很大,使用 <code><></code> 方法会变得繁琐。 这就是 Monoid class 为什么有个辅助函数 <code>mconcat</code>。</p><p>我们可以这样重构代码:</p><pre><code class="haskell">pstr :: String -> String
pstr = mconcat [parens, curlyBrackets, squareBrackets]
astr :: String
astr = pstr "abc"</code></pre><h4>Monoid \<number-type\></h4><p>回顾 Monoid 的定义,我们必须选择可结合的二元运算,但对于数字,它可以是加法或者是乘法。</p><p>如果我们选择加法,那就会错过乘法,反之亦然。</p><p>不巧的是,每种类型只能有 1 个 Monoid。</p><p>解决这个问题的方法是创建一个新类型,其中<strong>包含</strong>一个用于加法的 <code>Num</code> 和另一种用于乘法的类型。</p><p>这些类型可以在 <code>Data.Monoid</code> 中找到:</p><pre><code class="haskell">{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
import GHC.Generics
newtype Sum a = Sum { getSum :: a }
deriving (Eq, Ord, Read, Show, Bounded, Generic, Generic1, Num)
newtype Product a = Product { getProduct :: a }
deriving (Eq, Ord, Read, Show, Bounded, Generic, Generic1, Num)</code></pre><p>现在我们可以为每个创建 Monoids。</p><h4>Monoid Sum(和)</h4><pre><code class="haskell">{-# LANGUAGE ScopedTypeVariables #-}
import Data.Coerce
instance Num a => Monoid (Sum a) where
mempty = Sum 0
mappend = coerce ((+) :: a -> a -> a)</code></pre><p><code>mempty</code> 是 <code>0</code> 包裹在 <code>Sum</code> 中。</p><p>这里 <code>coerce</code> 用于安全地将 <code>Sum a</code> 强制转换为它的 "Representational type",例如 <code>Sum Integer</code> 将被强制转换为 <code>Integer</code> 并使用适当的 <code>+</code> 运算。</p><p><code>ScopedTypeVariables</code> pragma 允许我们将 <code>a -> a -> a</code> 中的 <code>a</code> 等同于 <code>instance</code> 的范围,从而等同于 <code>Num a</code> 中的 <code>a</code>。</p><h5>Monoid Sum 用例</h5><pre><code class="haskell">sum :: Sum Integer
sum = mconcat [Sum 1, Sum 2] -- Sum 3</code></pre><h4>Monoid Product(积)</h4><pre><code class="haskell">{-# LANGUAGE ScopedTypeVariables #-}
import Data.Coerce
instance Num a => Monoid (Product a) where
mempty = Product 1
mappend = coerce ((*) :: a -> a -> a)</code></pre><p><code>mempty</code> 是 <code>0</code> 包裹在 <code>Product</code> 中。</p><p>这里 <code>coerce</code> 用于安全地将 <code>Product a</code> 强制转换为它的 Representational type,例如 <code>Product Integer</code> 将被强制转换为 <code>Integer</code> 并使用适当的 <code>*</code> 运算。</p><p><code>ScopedTypeVariables</code> pragma 允许我们将 <code>a -> a -> a</code> 中的 <code>a</code> 等同于 <code>instance</code> 的范围,从而等同于 <code>Num a</code> 中的 <code>a</code>。</p><h5>Monoid Product 用例</h5><pre><code class="haskell">product :: Product Integer
product = mconcat [Product 2, Product 3] -- Product 6</code></pre><h4>Monoid Ordering(排序)</h4><p>在看这个 Monoid 之前,让我们回顾一下排序和对比:</p><pre><code class="haskell">data Ordering = LT | EQ | GT</code></pre><p>在使用 <code>class Ord</code> 中的 <code>compare</code> 时用到此类型,例如:</p><pre><code class="haskell">compare :: a -> a -> Ordering</code></pre><p>其使用示例:</p><pre><code class="haskell">compare "abcd" $ "abed" -- LT</code></pre><p>现在 <code>Data.Ord</code> 中有一个很棒的辅助函数用于比较,称为 <code>comparing</code>:</p><pre><code class="haskell">comparing :: (Ord a) => (b -> a) -> b -> b -> Ordering
comparing p x y = compare (p x) (p y)</code></pre><p>该辅助函数在比较<strong>之前</strong>对每个元素应用一个函数。 这对于元组之类的东西非常有用:</p><pre><code class="haskell">comparing fst (1, 2) (1, 3) -- EQ
comparing snd (1, 2) (1, 3) -- LT</code></pre><p>现在对于 Monoid:</p><pre><code class="haskell">-- lexicographical ordering
instance Monoid Ordering where
mempty = EQ
LT `mappend` _ = LT
EQ `mappend` y = y
GT `mappend` _ = GT</code></pre><p>这个实现看起来很随意。 为什么有人会以这种方式实现 <code>Monoid Ordering</code>?</p><p>好吧,如果你想在 <code>sortBy</code> 追加一部分对比,那么你需要这个实现。</p><p>看一下 <code>sortBy</code>:</p><pre><code class="haskell">sortBy :: (a -> a -> Ordering) -> [a] -> [a]</code></pre><p>请注意,第一个参数与 <code>compare</code>、<code>comparing fst</code>、<code>comparing snd</code> 和 <code>comparing fst `mappend` comparison snd</code> 的类型相同。</p><p>为什么? 因为 <code>mappend</code> 的类型是 <code>a -> a -> a</code>,这里的 <code>a</code> 是 <code>(a, b) -> (a, b) -> Ordering</code>。</p><p>所以我们可以结合或 <code>mappend</code> 比较函数,我们将有一个整体的比较函数。</p><p>请记住,<code>Monoid (a -> b)</code> 要求 <code>b</code> 也是 <code>Monoid</code>。</p><p>因此,如果我们希望能够 <code>mappend</code> 我们的比较函数,我们必须将 <code>Ordering</code> 设置为 <code>Monoid</code>,就像在上面做的那样。</p><p>但是我们仍然没有回答为什么它有这个看似奇葩的定义。</p><p>好吧,评论有点线索,即“字典顺序”。 这本质上意味着“字母顺序”或“左优先”,即如果最左边是 <code>GT</code> 或 <code>LT</code>,那么所有对于右边的比较都不再生效。</p><p>但是,如果最左边的是 <code>EQ</code>,那么我们需要向右看以确定组合比较的最终结果。</p><p>这正是该实现所做的。 这里再次添加一些额外的注释来说明这一点:</p><pre><code class="haskell">-- 字典序
instance Monoid Ordering where
mempty = EQ -- EQ 直到左边或直到右边, 对最终结果没有影响
LT `mappend` _ = LT -- 如果左边是 LT 则忽略右侧
EQ `mappend` y = y -- 如果左边是 EQ 则用右侧
GT `mappend` _ = GT -- 如果左边是 GT 则忽略右侧</code></pre><p>花点时间来好好理解这一点。 一旦你这样做了,这将更容易理解:</p><pre><code class="haskell">sortBy (comparing fst <> comparing snd) [(1,0),(2,1),(1,1),(2,0)]
-- [(1,0),(1,1),(2,0),(2,1)]</code></pre><p>要理解它是如何工作的,你必须记住 <code>Monoid (a -> b)</code>。</p><p>我们是在对 <code>(a, b) -> (a, b) -> Ordering</code> 类型的函数做 <code>mappend</code>. 一旦这两个函数都执行完成,我们就将按照我们的“字典顺序”返回的两个 <code>Ordering</code> 值做 <code>mappend</code>。</p><p>这意味着对比 <code>fst</code> 相较于对比 <code>snd</code> 更优先,这就是为什么所有 <code>(1, x)</code> 都将在所有 <code>(2, y)</code> 之前,即使当 <code>x > y</code> 时也是如此。</p><p>我们可以做一个不同的比较,我们只关心比较 <code>snd</code>:</p><pre><code class="haskell">sortBy (comparing snd) [(1,0),(2,1),(1,1),(2,0)]
-- [(1,0),(2,0),(2,1),(1,1)]</code></pre><p>这里 <code>fst</code> 术语不可预测的顺序,而 <code>snd</code> 是升序的。</p><p>为了好玩,我们可以分别控制升序和降序。 首先让我们定义一些辅助函数:</p><pre><code class="haskell">asc, desc :: Ord b => (a -> b) -> a -> a -> Ordering
asc = comparing
desc = flip . asc</code></pre><p>现在我们可以对 <code>fst</code> 降序和 <code>snd</code> 升序排序:</p><pre><code class="haskell">sortBy (desc fst <> asc snd) [(1,0),(2,1),(1,1),(2,0)]
-- [(2,0),(2,1),(1,0),(1,1)]</code></pre><h5>优化 <code>Monoid Ordering</code></h5><p>示例排序都只使用少量的对比。 事实上,大多数排序只会使用少量的比较。</p><p>即便如此,即使第一个返回 <code>LT</code> 或 <code>GT</code>,也必须执行 <code>mappend</code>。 当只有很少量的比较时,这似乎没什么大不了的。 但它可能叠加成为一个大列表。</p><p>我们希望我们的对比走的是“短路”,这通常用布尔二元运算 <code>&&</code> 和 <code>||</code> 来完成。</p><p><code>Monoid Ordering</code> 的当前定义不可能走短路,因为它依赖于默认的 <code>mconcat</code> 实现,该实现使用访问每个列表元素的 <code>foldr</code> 函数。</p><p>如果我们编写自己的 <code>Moniod Ordering</code> 并实现一个提前返回结果的 <code>mconcat</code>,我们将有一个更高效的排序。</p><pre><code class="haskell">import Prelude hiding (Monoid, mempty, mappend, mconcat)
import Data.List
import Data.Maybe
import Control.Arrow
instance Monoid Ordering where
mempty = EQ
LT `mappend` _ = LT
EQ `mappend` y = y
GT `mappend` _ = GT
mconcat = find (/= EQ) >>> fromMaybe EQ</code></pre><p>这个实现允许我们重构我们之前的排序:</p><pre><code class="haskell">sortBy (mconcat [desc fst, asc snd]) [(1,0),(2,1),(1,1),(2,0)]
-- [(2,0),(2,1),(1,0),(1,1)]</code></pre><p>结果相同,但任何时候 <code>dest fst</code> 返回了 <code>LT</code> 或 <code>GT</code>,那么 <code>asc snd</code> 将被跳过。</p><p>注意: 我们的实现依赖 <code>Data.List</code>、<code>Data.Maybe</code> 和 <code>Control.Arrow</code>,如果在标准中实现它们会不必要地耦合 <code>Data.Monoid</code>。 这个限制可以通过编写一个专用的函数来克服(不是很 "Don't repeat yourself")。</p><p>但是,覆盖标准实现的最大问题是我们必须遮盖<strong>所有</strong> Monoid 定义。</p><p>这些是针对边缘情况进行优化的一些相当大的缺点。 但它同样是一个很好的练习。 此外,如果我们尝试排序的列表很大,那么它可能是值得的。</p><p>引用:</p><ul><li><a href="https://link.segmentfault.com/?enc=1VlA6591HzDvCEZIE3x7LQ%3D%3D.WueydUW4SCJ0%2BXSf%2BsZ7BaMLS5zgTHwpFFWj0Gfaq0HoASGGMlNi6WzyTa%2BaOPTLzOPLKA0NHHBiR28vzetnGg%3D%3D" rel="nofollow">The Monoid Instance for Ordering</a></li><li><a href="https://link.segmentfault.com/?enc=b2vp4r5gLCdMsEjM5sr2cg%3D%3D.XBdBuDmymj3XeRavIn0t%2BEPyuUzZnWmp7yL%2Br%2FklR5pW98zybH6XJdr4XPLb6nYgJy0VV9rqfYfnAJ23CT7pWw%3D%3D" rel="nofollow">Monoids in Haskell</a></li></ul><h4>可交换 Monoid (Abelian Monoid)</h4><p>如开头所述,如果我们向 <code>Monoid</code>(或 <code>Group</code>)再添加一个约束,我们可以并行执行操作。</p><p>该约束是"可交换性"。</p><blockquote><em>∀ a, b ∈ M : a · b = b · a</em></blockquote><p>通过施加该约束,我们可以按<strong>任何</strong>顺序处理列表。 这可以交由编译器并行化,借助类库甚至分发给其他机器。</p><p>这是定义:</p><pre><code class="haskell">class Monoid m => CommutativeMonoid m</code></pre><p>没有写函数可能看起来很奇怪,但它的接口与 <code>Monoid</code> 相同,只是要求二元操作支持交换律。</p><p>不幸的是,在 Haskell 中没有办法要求这些约束。</p><h3>Num a => CommutativeMonoid (Sum a)</h3><p>这是定义:</p><pre><code class="haskell">instance Num a => CommutativeMonoid (Sum a)</code></pre><p><code>Sum</code>(或 <code>Product</code>)使用 <code>CommutativeMonoid</code> 而不是 <code>Monoid</code> 的原因:</p><ol><li>更好地传达如何使用 <code>Monoid</code></li><li>调用<strong>需要</strong>一个 <code>CommutativeMonoid</code> 的函数</li></ol><h2>结论</h2><p>Monoids 是拼接相似事物的强大抽象,这些抽象可以在编程中反复地呈现。</p><p>希望这对 <code>Monoids</code> 是一个好介绍。 还有很多其他类型的 Monoid,但是一旦你有了大致的了解,研究这些其他特化的 Monoid 应该会容易很多。</p>
一些 Rust Tips 记录
https://segmentfault.com/a/1190000041183892
2021-12-27T10:11:33+08:00
2021-12-27T10:11:33+08:00
题叶
https://segmentfault.com/u/tiye
1
<h3>打印 MIR 的命令</h3><pre><code class="bash">cargo +nightly rustc -- -Z unpretty=mir</code></pre><h3>打印 enum 各个 variants 体积的命令</h3><p>参考 <a href="https://link.segmentfault.com/?enc=7aN0gVS%2Ba0jdtDb7iK13ng%3D%3D.HqBfkGHQMeAnLIkY2%2BmH8pC7ZfP4LP8D6klJn%2Fhc%2BAlG%2FGMUHvOCveYWx253zvpf%2FwNNb2Hpm8SurLcQ6TCp4g%3D%3D" rel="nofollow">https://nnethercote.github.io...</a></p><pre><code class="bash">rustc +nightly -Zprint-type-sizes input.rs</code></pre><p>或者运行 cargo 的时候<a href="https://link.segmentfault.com/?enc=mGhG5RyL3%2F6E3mPH3NUA4Q%3D%3D.BOs%2FlFRyI9KUPxFiwmbOXvEa%2B%2ByiVbp1dogfCdTRl4adJiIlIQ5lMM2DtPsvjIQp" rel="nofollow">从环境变量加上参数</a>:</p><pre><code class="bash">RUSTFLAGS="-Z print-type-sizes" cargo +nightly run</code></pre><h3>运行测试</h3><p>限制只使用单线程:</p><pre><code class="bash">cargo test -- --test-threads 1</code></pre><p>运行测试过程中能使用 <code>println</code>:</p><pre><code class="bash">cargo test -- --nocapture</code></pre><h3>TODO</h3>
Rust 使用 libloading 的入门笔记
https://segmentfault.com/a/1190000040591394
2021-08-29T23:39:33+08:00
2021-08-29T23:39:33+08:00
题叶
https://segmentfault.com/u/tiye
0
<p>Rust 是静态类型语言, 如果有部分代码想要单独编译再加载, 就需要通过 link 来处理,<br>先把一个模块打包成 dynamic library, 然后运行的时候再来调用.<br>在 Windows 里边是 <code>*.dll</code> 文件, Linux 里是 <code>*.so</code> 文件, macos 是 <code>*.dylib</code>.<br>其他还有更小众的操作系统, 可能还有不同的后缀...<br>我这边对应的系统是 macos.</p><p>首先单独编译的部分, Rust 文档给出了比较多的种类, 看文档,<br><a href="https://link.segmentfault.com/?enc=ICDI61%2FJHy6HKX2qE%2BwR%2Fw%3D%3D.flsIzE1HKweL5bCvQz8M4UVMQyG5XwbVtSaat2zWPWZ6mWJMegN3MpeX17mcD9So9gL%2BIBZDYIWlHhC%2BHzQICw%3D%3D" rel="nofollow">https://doc.rust-lang.org/ref...</a><br>我的场景用到的是 <code>dylib</code> 或者 <code>cdylib</code> 的方案, <code>Cargo.toml</code> 指定一下配置就好.<br>我试了一下, 两者对我来说都是可以的, <code>cdylib</code> 是对 C 的支持, 我暂时用不到.</p><p>然后加载 dynamic library 的部分我直接用这个库了,<br><a href="https://link.segmentfault.com/?enc=%2FCRGlslIrWi5SIfJXlFGFQ%3D%3D.cOLPngcbw4ismztrs0mZnMFfHhwZ%2FBECwqjlhbJbP%2FTbA1A3RVLNDdRPAGF6X77b" rel="nofollow">https://docs.rs/libloading/0....</a><br>但是需要注意的是, 加载的过程传参比较麻烦, 复杂结构会提示 "ffi unsafe".<br>具体原因没理解, 大概是跟内存布局有关, 我看传递的时候有时候是用的指针,<br>直接 usize 或者 bool 是可以直接传的, 但是 String 和 &str 传不了,<br>网上给的方案是用 CString 和 CStr 转换这边有例子,<br><a href="https://link.segmentfault.com/?enc=df6kKQwGf1mfJcmQrVil%2Fg%3D%3D.BRcAoi5p1yzOz4PQi4TcH34wkUdrSpha5YKGLbQVgHTpfFh7cvKSWxgugvtPnkxIMt%2BUs0Wo20aE%2BQ8ot6VvcA%3D%3D" rel="nofollow">https://doc.rust-lang.org/std...</a></p><p>此外就是 externs 写法的细节了, 我抄了写例子, 然后修 bug, 最终得到:</p><pre><code class="rs">#[no_mangle]
pub unsafe extern "C" fn read_file(name_a: *const c_char) -> *mut c_char {
let name = CStr::from_ptr(name_a).to_str().unwrap();
let task = fs::read_to_string(&name);
match task {
Ok(s) => {
let a = CString::new(s).unwrap();
CString::into_raw(a)
}
Err(e) => {
panic!("Failed to read file {:?}: {}", name, e)
}
}
}</code></pre><p>然后对应调用的部分是:</p><pre><code class="rs">fn main() {
let u = call_dynamic();
println!("Hello, world! {:?}", u);
}
fn call_dynamic() -> Result<String, Box<dyn std::error::Error>> {
unsafe {
let lib = libloading::Library::new(
"/Users/chen/repo/calcit-lang/std/target/release/libcalcit_std.dylib",
)?;
let func: libloading::Symbol<unsafe extern "C" fn(name_a: *const c_char) -> *mut c_char> =
lib.get(b"read_file")?;
let a = CString::new("/Users/chen/repo/gist/rs-demo/Cargo.toml").expect("should not fail");
let c_name = a.as_ptr(); // <-- 标记行A
let ret = CStr::from_ptr(func(c_name)).to_str().unwrap();
Ok(ret.to_owned())
}
}</code></pre><p>中间遇到一些坑, 导致结果传递参数比较长时间只能得到空字符串,<br>大致是两个问题吧, 一个是"标记行A"的位置, 需要定义成变量才能正常,<br>刚开始我写在同一行, 调试始终拿不到内容, 最后参考网上的例子调整, 就好了,<br>看某个文章提到原因说这是为了给 <code>a</code> 一个单独的引用.(来源丢了)</p><p>另一个是从 dynamic library 返回的数据, 我本来用的 <code>*const char</code>,<br>也是长时间读到空数据, 最后参考某个 FFI 的例子, 改成了 <code>*mut char</code> 随后成功,<br><a href="https://link.segmentfault.com/?enc=EIbf4IphYB94BpMKourVkQ%3D%3D.aWwn9ePT5j6QjjkLM%2BrutHkVZB8%2Fw9ENXm7j%2BhgHnrN2OcgRL1135UOEvY73GUnO" rel="nofollow">https://stackoverflow.com/a/4...</a><br>依然不确定是怎么回事, 依稀翻到一个评论说这样为了让数据停留在堆上,<br>如果是前者, dynamic library 函数调用完成之后数据请跨了, 就读不到.(来源也丢了)</p><p>总之按照上边的写法, 编译后就能得到 macos 上的 <code>*.dylib</code> 文件, 动态加载.</p><p>初次使用, 没搞懂的细节还是挺多的, 包括后续怎么分发, 怎么跨平台, 都没有经验.<br>后续要进展还是提早留笔记.</p><hr><p>更新... 发现还有一个 <code>extern "Rust"</code> 的模式, 对应直接使用 Rust ABI.<br>然后就可以直接用 Rust 当中的各种数据类型了. 省事很多啊.</p>
Expression Problem 和 Calcit 相关引用笔记
https://segmentfault.com/a/1190000040493964
2021-08-10T17:52:15+08:00
2021-08-10T17:52:15+08:00
题叶
https://segmentfault.com/u/tiye
0
<ul><li>Wiki <a href="https://link.segmentfault.com/?enc=k4%2FgNZd62o0Sw8vK0dU0pw%3D%3D.NaCVn4L2IzVniRRPuRU2OkLGc5B3uCDTfaqUp8LxxrRgHrLVopxnpkI6ZLEpNBMwyvlPSOB84Mis3Fbf9uc4JQ%3D%3D" rel="nofollow">https://en.wikipedia.org/wiki...</a></li><li>知乎引用 <a href="https://link.segmentfault.com/?enc=CHrUfyVdW48OFNT1gQt6VQ%3D%3D.4F47c60tSXXbnST3iGgV36Lg74JpEyHM7HBIwryIt8PaXQfQbOl2SFs2a1RVerkI6UPTVS2iMOcw6skfj4WabA%3D%3D" rel="nofollow">https://www.zhihu.com/questio...</a></li><li>中文简介 <a href="https://link.segmentfault.com/?enc=rjPD6lXerprKjUSQoOyiYA%3D%3D.oyQC523PlZwKYQDXoUNV0o0LUAuHLAo0I3SW8VkqkmNaWqidJneKumRSFGlKBGMbGmwfNp92Mee%2B88Gjx3K6WLcG1priXipdwHkxHgPXiv0%3D" rel="nofollow">http://mgampkay.github.io/pos...</a></li><li><a href="https://link.segmentfault.com/?enc=wy0dpw%2FcwgkEXC6PbZKBPA%3D%3D.MVeKtY31swpbKFqCaCNnzBW6XgTRaRYhi%2BZs4o0Jd4nX6pbQQIz4vq%2FJmNmyuYCcAdctT7%2B0HxW8VltQ%2Blb%2BX7fiph097EDF3s7WCZk%2BZW8%3D" rel="nofollow">The Expression Problem and its solutions</a></li><li><a href="https://link.segmentfault.com/?enc=pwUkBASCUnvZYQNSr9lmpQ%3D%3D.C%2F%2FbR74Lp6dNM6F%2B%2BqOZ1zP5yP%2BeWjeHCpuBNs1gpf7hRlp43ncyCMMTUfeg6DX4Hj3YDpKoIAKgBF6qsvRFTKShKI1ZQ07varqIJRTLF%2BskurHVJdP32zsTKddirYQH" rel="nofollow">More thoughts on the Expression Problem in Haskell</a></li><li><a href="https://link.segmentfault.com/?enc=QaAtZf9YE5EsXDDl1P%2BTlg%3D%3D.%2FKHSoLnbnmn1kvgV778LMPCQwsLqENcYTd%2BCRzWLj9CXJIPJ6bl4F1JCJdCSTNRg2KxOOKW3WMcVuApRbGKssYgCr53Q8B2LYDg8HQgUX7c%3D" rel="nofollow">3 ways to solve the expression problem</a></li><li><a href="https://link.segmentfault.com/?enc=ReFjiac0o8aa%2BnIVnOvDXw%3D%3D.Q3GKqE6dtfm%2BNtDr6FBY0o0buHlujBmfzFCB5c6iPAHimkOkjhLKKrutADUh%2BqQYZp1SrbMn9IJbgPLyO0%2F9%2BTfv7%2FpDdpQGl9H4LoR%2BWAI%3D" rel="nofollow">The Expression Problem in Rust</a></li><li><a href="https://gist.github.com/elnygren/e34368a86d62f0cb75f04ba903f7834a">Solving the Expression Problem with Clojure(Protocol)</a></li></ul><p>Calcit 示例代码</p><pre><code class="cirru">ns app.main
defrecord %expr-methods :eval
def %const $ %{} %expr-methods
:eval $ fn (tp)
nth tp 1
def %binary-plus $ %{} %expr-methods
:eval $ fn (tp)
&let
pair $ nth tp 1
+ (nth pair 0) (nth pair 1)
def %const-2 $ .extend-as %const '%const-2
, :stringify $ fn (tp)
str "|(Const " (nth tp 1) "| )"
def %binary-plus-2 $ .extend-as %binary-plus '%binary-plus-2'
, :stringify $ fn (tp)
&let
pair $ nth tp 1
str "|(BinaryPlus " (first pair) "| " (last pair) "| )"
defn main ()
echo $ .eval $ :: %const 1
echo $ .eval $ :: %binary-plus $ [] 1 2
echo $ .stringify $ :: %const-2 1
echo $ .eval $ :: %const-2 1
echo $ .stringify $ :: %binary-plus-2 $ [] 1 2
echo $ .eval $ :: %binary-plus-2 $ [] 1 2</code></pre><p>运行示例:</p><pre><code>=>> bundle_calcit && cr -1
file created at ./compact.cirru
calcit_runner version: 0.4.16
1
3
(Const 1 )
1
(BinaryPlus 1 2 )
3
took 7.997ms: nil</code></pre>
PureScript 的 equality
https://segmentfault.com/a/1190000040418472
2021-07-28T16:57:15+08:00
2021-07-28T16:57:15+08:00
题叶
https://segmentfault.com/u/tiye
1
<p>从 PureScript 的角度反过来看, JavaScript 的好多概念还是比较模糊的.<br>前几天群里看到讨论 js 的 equality 的事情, 我就觉得 js 设计挺不清晰的.<br>js 里用 <code>===</code> 的话, 遇到</p><ul><li>literals 和 null/undefined, 按照 value 进行判断(我不懂 NaN...)</li><li>Array, Object, 按照引用进行判断</li></ul><p>但是作为 calcit-js 作者, 我想说, 这些不是数据本身 equality 的性质,<br>解释器当中的语义, 完全是编程语言作者设计了暴露出来的接口,<br>判断引用来判断是否相等, 运行指令的时候会非常快, 但这不是我们业务当中想要的语义.<br>我们在业务当中遇到数据要判断是否"等价", 就需要按照值进行判断,<br>当我们用组合而成的数据结构来表示数据的时候, 就需要根据结构递归进行判断.<br>而 js 的 <code>===</code> 只是一个封装暴露平台底层能力的功能, 没有往函数式编程做.</p><p>我们来看 PureScript 当中怎么判断是否相等的.<br>开始之前, 需要定义一个数据类型, 比如我直接叫做 <code>Cirru</code>,</p><pre><code class="purs">data Cirru = CirruString String | CirruList (Array String)</code></pre><p>这段代码的含义是我定一个了一个类型 Cirru, 有两种情况,<br>一个是通过数据构造器 <code>CirruString "a"</code> 构造的第一种可能,<br>一个是通过数据构造器 <code>CirruList ["b"]</code> 构造的第二种可能,</p><p>比方说我们用两个简单的数据进行判断的时候,</p><pre><code class="purs">(CirruString "A") == (CirruString "B")</code></pre><p>PureScript 类型检查会报错, 认为找不到 Cirru 这个类型怎么处理的办法,</p><pre><code> No type class instance was found for
Data.Eq.Eq Cirru
while applying a function eq
of type Eq t0 => t0 -> t0 -> Boolean
to argument CirruString "A"
while inferring the type of eq (CirruString "A")
in value declaration main
where t0 is an unknown type</code></pre><p>因为是函数式编程, 它的内部其实会把 <code>==</code> 转换回到一个函数 <code>eq</code>,</p><pre><code class="purs">eq (CirruString "A") (CirruString "B")</code></pre><p>通过函数进行判断, 但是 <code>eq</code> 在 type class 当中只是通用的定义,</p><pre><code class="purs">eq :: a -> a -> Boolean</code></pre><p><a href="https://link.segmentfault.com/?enc=3G%2BtwlZC8LtsX2gJzCcp5g%3D%3D.LNY1jRB2UClxyFupO6x1gFZURq53FgIj1k4s9S1Z3%2F2HQN9rF8t4YdSiZDOrwjbSb3CQovnGdSQlPYj7acz7UPemtdSSnQzrGU9lMtCYOF%2BGY7Gc827WwAw31Xqtv0R8" rel="nofollow">https://pursuit.purescript.or...</a></p><p>其中 <code>a</code> 是某个类型, 但他并不知道 Cirru 对应的 <code>eq</code>, 就得到了上边的报错,</p><pre><code> No type class instance was found for
Data.Eq.Eq Cirru
while applying a function eq
of type Eq t0 => t0 -> t0 -> Boolean
to argument CirruString "A"
while inferring the type of eq (CirruString "A")
in value declaration main
where t0 is an unknown type</code></pre><p>作为 js 程序员, 当然你也会有一些常见的处理办法, "直接递归判断不就好了?",<br>那么 PureScript 也提供了这样的一种方式, 你可以通过 <code>derive</code> 集成内置的一个实现,</p><pre><code class="purs">derive instance cirruEq :: Eq Cirru</code></pre><p>这样当你再去判断的时候, 就能得到期望的结果了,</p><pre><code class="purs">main = do
log $ show $ (CirruString "A") == (CirruString "B")
log $ show $ eq (CirruString "A") (CirruString "B")
log $ show $ (CirruString "A") == (CirruString "A")
log $ show $ eq (CirruString "A") (CirruString "A")
log $ show $ (CirruList ["A"]) == (CirruList ["A"])</code></pre><pre><code>=>> spago run
[info] Build succeeded.
false
false
true
true
true</code></pre><p>或者, 另一种更常见的方式是你自己去定义 <code>eq</code> 对应对的实现是什么样?<br>比如这样的代码, 就通过递归, 把判断变成内部的数据的判断,</p><pre><code class="purs">instance cirruEq :: Eq Cirru where
eq (CirruString x) (CirruString y) = x == y
eq (CirruList x) (CirruList y) = x == y
eq _ _ = false</code></pre><p>最后 <code>eq _ _</code> 表示剩下的其他的没覆盖到的可能性.</p><p>从这个代码的原理上说, 你完全可以自己定义数据是否相等的规则,<br>即便你把 <code>(CirruString "A") == (CirruList ["A"])</code> 定义相等, 也能写出来</p><pre><code class="purs">instance cirruEq :: Eq Cirru where
eq (CirruString x) (CirruString y) = x == y
eq (CirruList x) (CirruList y) = x == y
-- 看这行
eq (CirruString x) (CirruList ys) = if (length ys) == 1 then (Just x) == (head ys) else false
eq _ _ = false</code></pre><p>你可以简单认为是 type class 先定义出来了一套接口, 其中有 <code>eq</code>,<br>然后其他类型定义好后, 也需要定义对应的 <code>eq</code> 的实现, 然后才能用于计算.<br>JavaScript 作为脚本语言, 把其中一种特例, 硬编码在语言解释器上了.<br>你要找准确的理解, 就要回到 PureScript 这样的定义当中去.</p><p>可以看到, 在语义层面我们不会去触碰"内存引用的地址是否相等"的判断,<br>当你在 PureScript 用 FFI 的方式去调用 js, 可能是会碰到,<br>但是这些硬件相关的实现细节, 是尽量被封装到语言实现的底层当中去隐藏起来的.</p><p>本文是我的一些学习笔记和新得, 使用的术语不大准确.<br>如果你需要更精确的概念, 可能需要翻一下 Haskell 相关文档,<br>或者到聚聚推荐的某些书上去找答案了.</p>
一种 Monad 的偏门的理解方式
https://segmentfault.com/a/1190000039643272
2021-03-16T16:14:53+08:00
2021-03-16T16:14:53+08:00
题叶
https://segmentfault.com/u/tiye
2
<p>我对数学概念属性符号掌握得不好, 所以理解比较慢,<br>这篇文章是从概念性的内容去理解 Monad, 不精确, 可能具体到数学概念也不准确.<br>但是希望提供一个比较直观的方式去了解, Monad 是怎么来的?</p><p>概念上简单说 Monad 是"自函子范畴上的一个幺半群".<br>新概念很多, 函子, 自函子, 范畴, 半群, 幺半群.<br>模糊地讲, 这些就是数学的概念, 集合啦, 集合元素间的映射啦, 单位元啦,</p><h3>Monad 概念</h3><p>模糊理解的话, 函子可以当做是函数, <code>a -> b</code> 的映射, 当然也可以比函数更抽象点的东西,<br>然后"自函子", 涉及到类型的概念, 函子从一个集合 A 到另一个集合 B,<br>但我们把程序所有东西都放在一起的话, 函子可以认为是从 A 到 A 自己了, 所以是"自函子".<br>"范畴"我解释不来, 大致是这些函子的构成和关系, 具体看<a href="https://link.segmentfault.com/?enc=L1AcRYIhaLzlOtanFsc7JA%3D%3D.q0bUpJqSfmuA1H3rYY49COLzQ8C789uizhKvSgOnQjuHXMWYRe6H4eTiRjl%2BGRvE81RdhIHLNzByaCvDkLhy%2BA%3D%3D" rel="nofollow">何幻的文章</a>.<br>从前面这部分看, Haskell 把程序当成各种集合还有映射来看待了,<br>程序中, 无论值的变化, 甚至副作用的变化, 全都纳入到范畴里边来理解.</p><p>然后幺半群呢? 要理解这些概念, 就要知道相关的几个概念,</p><ul><li>原群(Magma)</li></ul><p>一个集合, 然后集合上的元素的二元操作, 操作结果都在这个集合内部,<br>一个例子, 比如 <code>{ true false }</code>, 还有二元操作 <code>and</code> <code>or</code>,<br>任何二元操作的结果都在集合内.</p><ul><li>半群(Semigroup)</li></ul><p>半群在原群的基础上增加了一个条件, 满足结合律:<br>比如所有非空的字符串的集合, 以及 <code>concat</code> 操作.<br><code>"a" `concat` "b"</code> 得到 <code>"ab"</code> 还在集合内,<br>然后 <code>("a" `concat` "b") `concat` "c"</code> 得到 <code>"abc"</code>,<br>然后 <code>"a" `concat` ("b" `concat` "c")</code> 得到 <code>"abc"</code>,<br>两者是等价的, 满足结合律.</p><ul><li>幺半群(Monoid)</li></ul><p>幺半群, 在半群的基础上增加一个条件, 存在幺元,<br>幺元跟任何元素 <code>a</code> 结合得到的都是 <code>a</code> 本身,<br>例子的话, 在上面这个非空字符串的例子当中再加上空字符串,<br>那么 <code>"" `concat` "a"</code> 得到 <code>"a"</code>,<br>然后 <code>"a" `concat` ""</code> 得到 <code>"a"</code>,<br>两者等价的, <code>""</code> 就是这个集合的幺元.</p><ul><li>群(Group)</li></ul><p>群在幺半群的基础上在加上了一个条件, 存在逆元,<br>一个例子比如整数的集合, 其中<br><code>(x + y) + z = x + (y + z)</code> 满足结合律,<br><code>x + 0 = 0 + x</code> 存在幺元,<br><code>x + (-x) = 0</code> 存在逆元,<br>所以整数的集合就是一个幺半群了.</p><p>当然这个叙述就不精确了, 但你大致应该知道幺半群对应什么了,<br>集合, 二元操作闭合, 交换律, 幺元.<br>特别是字符串这个例子, 可以看到程序当中明显会有很多这样的对应,<br>我们大量使用数组, 数组也是满足这几个条件的,<br>还是那个 <code>concat</code> 操作, 闭合, 交换律, 幺元(<code>[]</code>), 都是成立,<br>然后数值计算, <code>+</code>, <code>*</code> 这两个操作, 闭合, 交换律, 幺元, 也是存在的.</p><p>然后需要绕过来理解一下了, 对于函数, 对于副作用, 是不是也是幺半群?</p><p>函数吧, 有 <code>f g h</code> 三个函数, 然后有个复合函数的操作 <code>compose</code>,<br><code>(f `compose` g) `compose` h</code> 是一个,<br><code>f `compose` (g `compose` h)</code> 是另一个,<br><code>compose</code> 是右边函数先计算的, 所以总是现在 <code>h(x)</code> 先计算, 最终是 <code>f(g(h(x)))</code>.<br>这个是结合律.<br>可以直观理解一下, 函数结合知识结合的函数, 最终调用还是固定的顺序的.<br>另外幺元就是 <code>function(x){ return x }</code> 这个函数了. 左右都一样.</p><p>然后副作用呢, 我们把副作用拿函数包一下, <code>function(){ someEffect() }</code>,<br>然后按照函数 <code>compose</code> 的操作, 还是有结合律的, <br>然后我们定义一个副作用为空的 <code>Unit</code> 操作, 可以作为幺元,<br>因为是空的副作用, 所以放在左边放在右边都是不影响的.</p><p>所以函数, 副作用, 这些也还是能按照幺半群来理解和约束的.<br>这样想下去, 程序里边大量的内容都是可以套用幺半群去理解了.<br>而关键位置就是结合律还有幺元, 对于函数来说就是函数复合还有 <code>(x) => x</code> 函数.</p><p>既然幺半群是程序当中普遍存在的结构, 也就能直接地接受这个概念了.<br>然后"自函子范畴上的幺半群", 也就是说限定在"自函子"而不是"普通集合元素"的幺半群了.<br>这个不能准确描述, 但是应该足够提供一些观念上的理解了, Monad 是怎么出来的...</p><h3>Monad class</h3><p>在 Haskell 里定义又要再另外理解了, 首先对幺半群来说还是清晰的,<br>而交换律作为 law 没有写在 class 的定义当中了,</p><pre><code class="hs">class Monoid m where
-- 定义元素
mempty :: m
-- 定义闭合二元操作
mappend :: m -> m -> m
-- 定义多个元素的结合, 默认是用的 List 表示
-- 满足结合律, 所以 foldr 是从右边开始结合的, 跟左边结合效果一致
mconcat :: [m] -> m
mconcat = foldr mappend mempty</code></pre><p>对应的 Monad 版本, 也有着跟 Monoid 对应的一些函数的结构,</p><pre><code class="hs">class Monad m where
-- 定义幺元
return :: a -> m a
-- 对应上边的二元操作 mappend
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
x >> y = x >>= \_ -> y
-- 对应上边的多个元素的组合 mconcat
join :: m => m (m a) -> m a</code></pre><p>这边容易分歧的用法出现了, 首先幺元的定义:</p><pre><code class="hs">-- 在 Monoid 当中是
mappend :: m
-- 在 Monad 当中是
return :: a -> m a
-- 或者有的地方用是 pure 的名称
pure :: a -> m a</code></pre><pre><code class="hs">-- 那么, Monoid 中的二元操作
mappend :: m -> m -> m
-- 到了 Monad, 应该是
mappend :: m a -> m a -> m a</code></pre><p>不过我们实际看到的是两个类型变量, <code>a</code> <code>b</code>:</p><pre><code class="hs">-- 包含从 a 到 m b 态射的一个过程
(>>=) :: m a -> (a -> m b) -> m b
-- 包含 a 也包含 b 但是 a 被丢弃的过程, 比较可能是通过副作用丢弃了
(>>) :: m a -> m b -> m b
x >> y = x >>= \_ -> y</code></pre><p>这个仔细想想也可以理解, 比如 <code>List Int</code>, 整数的数组,<br>经过映射之后, 可能还是 <code>List Int</code>, 也可能通过 <code>length</code> 得到 <code>Int</code>,<br>不过在 <code>m a</code> 这个类型约束当中, 不会是纯的 <code>Int</code> 了,<br>可能是 <code>List String</code>, 比如 <code>[1,2,3,4].map(JSON.stringify)</code>,<br>可能是 <code>List Boolean</code>, 比如 <code>[1,2,3,4].map(x => x > 0)</code>,<br>总之这个地方由于态射的存在而变化了.</p><p>至于为什么要这样定义, 如果说 <code>a -> m b</code> 这个过程不需要跟 m 发生作用,<br>那么我们用态射, 直接用 Functor 就能达成了,</p><pre><code class="hs">class Functor f where
fmap :: (a -> b) -> f a -> f b</code></pre><p>但是存在情况, 就是需要跟 m 发生作用的, 比如 IO, 就必然会,<br>然后是 flatMap 的情况, 计算过程要跟 List 这个构造器发生作用:</p><pre><code class="hs">Prelude> (>>=) [1,2,3,4] (\x -> [0..x])
[0,1,0,1,2,0,1,2,3,0,1,2,3,4]</code></pre><p>IO Monad 的特殊性在于主流语言习惯性不去管 IO,<br>但是按照 Monoid 这套理解下来, IO 确实用是这样的结构.</p><h3>其他</h3><p>里边的概念都太抽象了, 特别是范畴相关的, 这个写得不太能自圆其说.</p>
[译] 函数式 Arrow 实用案例
https://segmentfault.com/a/1190000039416877
2021-03-15T14:50:30+08:00
2021-03-15T14:50:30+08:00
题叶
https://segmentfault.com/u/tiye
0
<blockquote>原文 <a href="https://link.segmentfault.com/?enc=fC1xrtUUbys72NGqZFjLSg%3D%3D.ekLq4nmEMHa8%2FYsoNHm6yRgdSCOr1R44GINKbFcJ5ex1gXZhy9RBX6UeubN58zqrdI12ZMT46HgeaYHyHGd19g%3D%3D" rel="nofollow">https://tuttlem.github.io/201...</a></blockquote><h3>介绍</h3><p>Arrow 提了一种表示"计算"的手段. 甚至有些 Monad 用法里边看不到的复杂操作, 通过 Arrow 提供的一些有意思的组合式的组合方法, 也可以构造出来.</p><p><a href="https://link.segmentfault.com/?enc=zVIcKG8lFHcsLbJQvZSlOw%3D%3D.aguhHhW2iPBuLNm8Q%2F1AVf7I7PnfRsSGW6EDMnKwGE6f%2FVq%2FfmHAppvD1MIIbUAkr%2FKUebZrvAen7Uk%2FLpvrRJheuxmP8K4KdhnS4yq1snk%3D" rel="nofollow">Arrow Class</a> 在 <code>Control.Arrows</code> 里定义, 从基础库可以引入.<br>根据 <a href="https://link.segmentfault.com/?enc=YTIyl0uJk%2FXrXj%2BLVHbdXw%3D%3D.dlZ35Ij43tD9Tu0Q0h4BJWmQJUKncyDJOpsREcyfcQA%3D" rel="nofollow">Haskell 官网的文档</a>表示:</p><blockquote>Arrows are a new abstract view of computation, defined by John Hughes [Hug00]. They serve much the same purpose as monads – providing a common structure for libraries – but are more general. In particular they allow notions of computation that may be partially static (independent of the input) or may take multiple inputs. If your application works fine with monads, you might as well stick with them. But if you’re using a structure that’s very like a monad, but isn’t one, maybe it’s an arrow.<p>Arrow 是 John Hughes 定义的一种对于"计算过程"的抽象表示. 其功能与 Monad 相似 - 为各个类库提供一个常用结构 - 甚至更加通用. 特别是 Arrow 允许计算的表达可以部分地作为静态(跟输入无关), 或者接受多个输入. 如果一个应用用 Monad 写没问题, 那你可以继续沿着 Monad. 不过如果用到一个结构非常像 Monad, 但又不是 Monad,, 那有可能就是 Arrow 了.</p></blockquote><ul><li><a href="https://link.segmentfault.com/?enc=0GMJ%2BOxKraye%2BvrxL5PUhw%3D%3D.Gm4jOAT6qEPUyatMA9pj5AJe56R2Usn8ix29eLAALHjk0KDq2xZeDo8Y9YTn2AUw" rel="nofollow">Haskell Wiki page on Arrows</a></li><li><a href="https://link.segmentfault.com/?enc=VoGrwXggyTTi3BgmgJHR2w%3D%3D.q8g4Xpl2pD82aDFinVh7Sz%2F%2B%2BMSudHaBunVKbZUrd5o%3D" rel="nofollow">Arrows on Haskell.org</a></li><li><a href="https://link.segmentfault.com/?enc=YPV%2FnS3E7cs9S7dFIP%2BzxQ%3D%3D.2iSo%2FXALOh%2FZF8nDKwZzgGXOsPO0mcdA%2F5RpDvu6ngmsTUyS77yA4xdoVZHT3BhD9drub%2BNcEAlf%2FDCvc2P%2B3w%3D%3D" rel="nofollow">Understanding Arrows</a></li><li><a href="https://link.segmentfault.com/?enc=uz353tbyjIkCYUE41H0dCw%3D%3D.UZ1Aq0YlzgGb6VWzakakoSRGKp6veAWHmG0ZLQcCyoqla9TZvZ3bIzfHxHzP5oNyEbTB8r9F4v9w8XBHS8fIVA%3D%3D" rel="nofollow">The Arrow Tutorial on Haskell Wiki</a></li></ul><p>如果你对于学习 Arrow 背后的理论感兴趣, 或者想要深入了解, 我强烈推荐你把上边几个链接彻底读一读.</p><p>这个文章的内容是带着你看看 Haskell 里边 Arrow 有哪些实际的例子.</p><h3><code>returnA</code></h3><p><a href="https://link.segmentfault.com/?enc=we%2FC5lxbTKTMpb77WBOG6w%3D%3D.JZj9adeL2a5uYNFrOSDkQoRVNMOSFxR4fpH18FvN67DuEqKr6X9D7kiXszDpIwYpekIJzAqM4a9rUjJi8P7mESJwDamyhPTIg3MuA%2BRtaXoY4xFhJxu7vkeo2q1hx2Jn" rel="nofollow"><code>returnA</code></a> 提供 <code>identity</code> Arrow. 对应 Monadic 上下文档中原来你用 <a href="https://link.segmentfault.com/?enc=DSUBrxT9yJuPyzOXC9NSTQ%3D%3D.ehxMGZudxMWLZpu1O8WCznOrZT%2F1LZJ6dzis5J7NVAzCfI5OcfbXpiu2J4voj15fbJitzPPTmdHSXarEvF6huxN7UnZVFRGl3ls%2B722b4wM%3D" rel="nofollow"><code>return</code></a> 的地方, 用这个代替.</p><pre><code>λ> :t returnA
returnA :: Arrow a => a b b
λ> returnA 5
5
λ> :t returnA 5
returnA 5 :: Num b => b</code></pre><h3><code>arr</code></h3><p><a href="https://link.segmentfault.com/?enc=EM6unPwTzlamyokAwtXfzg%3D%3D.EWxLBDYyTgZfdupw4o%2FMCB16yb%2FJgPrGe9bdUWrP0PS3n%2FEQsTuySvb5D6Vx%2Bad5uNqPrPNnJY92xSs2s61aeYgYCJGIbWsviV2sbrFGftQ%3D" rel="nofollow">arr</a> 接受一个普通函数, 然后返回一个构造的 Arrow:</p><pre><code>λ> let a1 = arr (+)
λ> :t a1
a1 :: (Arrow a, Num b) => a b (b -> b)</code></pre><p>然后调用 Arrow 可以直接用:</p><pre><code>λ> a1 (returnA 5) 6
11
λ> a1 5 6
11</code></pre><h3><code>>>></code></h3><p><a href="https://link.segmentfault.com/?enc=WZ7RXvpLL2tZGjrAVupg%2Bw%3D%3D.3tL8L9%2Ftnt4jGcpnae0AGqxDdctshB1Gv%2FP%2B758zsFg3ZQUUUIhn6wsft13Mr1qSFpLVu1VTYeAbxwG4H3VX51FX8f49euzbHnYGALRb3iSo4izGYOhXyW8VSOD7bZMW" rel="nofollow"><code>>>></code></a> 对两个 Arrow 进行从左到右的组合. 这个和 Monad 提供的 <a href="https://link.segmentfault.com/?enc=QPGPCpUjVE%2B2RIgGi51K%2BA%3D%3D.uPczb8Hl39WbESfTXPl2vrmQw65HEzVeTkzPradU5L8baK3KiaWXtIJWTrnJdRZXYV5tN%2B%2F6xTvmfgRg7Dkdhl9eGHRo7KysQyBmfGNyeDcY4%2FMC8DeYPWXH%2BUY6u9YU" rel="nofollow"><code>>>=</code></a> 非常相似.</p><pre><code>λ> let a1 = arr (+5)
λ> let a2 = arr (*2)
λ> a1 >>> a2 $ 3
16</code></pre><p>后面文章罗列的几个函数用于元组(Tuple)结构.<br>这是 Arrow 相对于 Monad, 在组合能力方面产生差别的地方.<br>作为例子, 先定一个整数的元组:</p><pre><code>λ> let q = (1,2)</code></pre><h3><code>first</code></h3><p><a href="https://link.segmentfault.com/?enc=cQ%2BOZc3hZ2BYRhYprM%2FUeg%3D%3D.FkfNVhHfhnsMXS4TIPZitFvwdvomL3j23nQ%2BrR4hWAZzYQipUUr5kclQVJYKTU3u2URuDEaCOkZgC2NAb09ni%2BTQ02hzSH3vGnhLfu924D8%3D" rel="nofollow"><code>first</code></a> 对元组的第一个元素调用 Arrow,<br>而第二个值不发生改变.</p><pre><code>λ> first a1 q
(6,2)</code></pre><h3><code>second</code></h3><p><a href="https://link.segmentfault.com/?enc=YI9dHRoC9pzRZPdKhDgE3A%3D%3D.4nOFZSjdfNsMVV9jxcuAhsl2C2Yjd2gYG96JGm1Mrv66qmXpZsTkaWsPPXboMWW1xpHXVerTuNHynnQlaKwclZzS7g%2BtitVx7leEIQN2wTXrxyjLhEXLwzdYIJJryjYY" rel="nofollow"><code>second</code></a> 对元组第二个值调用, 第一个不改变.</p><pre><code>λ> let q = (1,2)
λ> second a1 q
(1,7)</code></pre><h3><code>***</code></h3><p><a href="https://link.segmentfault.com/?enc=yQLrLsJQ14cvGx1IsicAIg%3D%3D.xOPWDv0BlN0Sqh3BS5LWdwLyUrl0qqLcSU2r3TxtGfROVB%2F1%2FPRlc%2BlZic%2BREYcDpXHiuMqBLYXddHN3NlByBJUsLzJFduuNN1prwGf%2BbAwjRi%2BTlvxJnrYsAGz5i0t%2F" rel="nofollow"><code>***</code></a> 对元组第一个元素调用第一个 Arrow, 对第二个元素调用第二个 Arrow.<br>后面这个例子, <code>1</code> 用 <code>a1</code> 调用, <code>2</code> 用 <code>a2</code> 调用.</p><pre><code>λ> let q = (1,2)
λ> a1 *** a2 $ q
(6,4)</code></pre><h3><code>&&&</code></h3><p><a href="https://link.segmentfault.com/?enc=NJT23XiBCu%2B%2F7ml3EHcyCQ%3D%3D.53UyUTLfVaetYMO%2B5RwjiDj3hyeaFJX0vQ9JeCi6NM4uqmjuMnAqI8kmJ2IpGJm3%2B938UQ%2FK9HFT4esVC%2FCj9XzTCNbTIrUsX2X1ngMDDkVtDJbEE89kAJwywZ1qRgds" rel="nofollow"><code>&&&</code></a> 会把一个值变成一个元组, 然后分别进行调用, 最后返回元组.</p><pre><code>λ> let a1 = arr (+3)
λ> let a2 = arr (+30)
λ> 1 &&& a2 $ 3
(6,33)</code></pre><h3><code>proc</code></h3><p><a href="https://link.segmentfault.com/?enc=hgQRQDpshJ%2F0p9g9LtzImg%3D%3D.dP2x4vf9WmU1xkURiZF%2FFfSRMdFfU9Mb3TYv5cZPrg5oynDANezar4emyosmOecgve3MDHRJE1uDwriTLPAYjA%3D%3D" rel="nofollow"><code>proc</code></a>(Arrow 抽象) 通过 Arrow 而不是普通函数来构造一个 lambda 函数.<br><code>proc</code> 允许你用 <a href="https://link.segmentfault.com/?enc=cfMYPSb4Jio8lBZh6P%2BTmg%3D%3D.%2BdpqpobjLQG9HbwONwxVU1CHsiYENerDSgYAXnjZb7gQB5hnlF0jtTx%2BdXII%2FLj%2B%2BOAgSBNxUwdbAjhHDN6Qc84XGRO0a38AA83Mfbb%2F8Cs%3D" rel="nofollow">Arrow Notation</a>来建构表达式(或者 Arrow).</p><pre><code class="hs">{-# Language Arrows #-}
import Control.Arrow
addA :: Arrow a => a b Int -> a b Int -> a b Int
addA f g = proc x -> do
y <- f -< x
z <- g -< x
returnA -< y + z</code></pre><p>该例中, <code>addA</code> 的类型签名接受两个从 <code>b</code> 到 <code>Int</code>(<code>a b Int</code>)的 Arrow, 返回一个同类型的 Arrow.</p><p><code>proc</code> 代码块有一个 lambda 变量 <code>x</code>, 在 Arrow 调用时被应用.<br>注意, 这里只是构造了一个 Arrow, 而不是现在就运行它.</p><p>后续代码用了一个新的操作符 <code>-<</code>(Arrow Application), 用于将表达式的值传给 Arrow.<br>可能这样好理解一点:</p><blockquote>"绑定了变量的表达式" <- "Arrow" <- "作为 Arrow 的输入项的纯表达式"</blockquote><p>调用 <code>addA</code> 是这样子:</p><pre><code>λ> let a1 = arr (+4)
λ> addA a1 a1 (returnA 3)
14</code></pre><p>这其中的 arrow <code>a1</code> 表现就像是 <code>(+4) 一样. </code>a1` 用在第一个和第二个参数.<br>最后我们把纯的值 <code>returnA 3</code>(就是 3)传给 <code>addA</code>.</p><p>所以这里可以看到 <code>(+4)</code> 应用到了 3, 另一个位置也是 <code>(+4)</code> 应用到 3, 对的, 发生了两次.<br>这是因为里边两个参数都用了 <code>a1</code>.<br>最后的结果是两次 Arrow 的调用结果再被相加, 得到 14.<br>纯的数值是通过 <code>returnA 3</code> 提供的.</p><p>这就是一些关于 Arrow 的非常基础的用例.<br>前面提供的链接当中能看到更多, 可以了解到 Arrow 是多么强大的结构.</p><hr><h2>译注</h2><p>文章就是个简单 Arrow 的介绍, 链接里边的就非常难懂了.<br>大致可以看到, 简单场景 Arrow 相当于一个抽象,<br>普通函数就是一种 Arrow, 而 Arrow 的概念上就是抽象化的这样一种计算过程.<br>前面例子当中, <code>arr (+)</code> 可以直接当做 <code>(+)</code> 使用,<br>看下相似的函数的类型签名:</p><pre><code class="hs">Prelude Control.Arrow> :t (\x -> x + 1)
(\x -> x + 1) :: Num a => a -> a
Prelude Control.Arrow> let a1 = arr (\x -> x + 1)
Prelude Control.Arrow> :t a1
a1 :: (Arrow a, Num c) => a c c</code></pre><p>其中:</p><pre><code class="hs">(\x -> x + 1). :: Num a => a -> a
arr (\x -> x + 1) :: (Arrow a, Num c) => a c c</code></pre><p>这里的 <code>a -> a</code> 跟 <code>a c c</code>, 或者 <code>Num -> Num</code> 到 <code>Arrow Num Num</code>, 对应,<br><code>-></code> 也是一种 <code>Arrow</code> 的实例.<br>这样大致可以理解 Arrow 相对于普通函数来说是怎样一种抽象吧.</p><p>另外文章后面的例子, 详细一点就是:</p><pre><code class="hs">{-# Language Arrows #-}
import Control.Arrow
addA :: Arrow a => a b Int -> a b Int -> a b (Int, Int)
addA f g = proc x -> do
y <- f -< x
z <- g -< x
returnA -< (y, z)
a1 = arr (+4)
a2 = arr (+40)
main = do
putStrLn $ show $ addA a1 a2 3</code></pre><pre><code>=>> runghc demo.hs
(7,43)</code></pre><p>可以看到这个静态的过程具体的例子了.<br>至于再强大的一点的例子, 还得再扒别的文章看看...</p>
笔记: 关于 SKI 组合子及其实现
https://segmentfault.com/a/1190000039315935
2021-03-02T12:35:40+08:00
2021-03-02T12:35:40+08:00
题叶
https://segmentfault.com/u/tiye
0
<p>按照 Wiki, 组合子(Combinator)狭义的定义是:<br><a href="https://link.segmentfault.com/?enc=OVDBBG8mZjsROPqXHZooag%3D%3D.3CedfmCGq4ePUB5FmxHDEmVtWPAwKqBM0L3W754tV62O38ubAhhU2c0%2Baivg%2BdiM" rel="nofollow">https://wiki.haskell.org/Comb...</a></p><blockquote>A function or definition with no <a href="https://link.segmentfault.com/?enc=icZUZdmt9RNwjQQpaD6fMg%3D%3D.sNayGmMs1TlqbedwawOdS%2BPy7U7XmzXHzklNKb89IM0610JN7XtpDclXOXucOjFA" rel="nofollow">free variables</a>.</blockquote><p>比如 <code>\f -> \a -> \b -> f b a</code> 定义的函数, 没有自由变量, 就是组合子.<br>参考这样的例子, Haskell 当中按照类型签名定义的函数都是这样写的.<br>大概 Haskell 里边基本上都是用着组合子在编程的, 而不是主流语言习惯意义的"函数".</p><p>SKI 组合子是更基础的组合子, 往前追溯到 Haskell 以前几十年的逻辑学的研究.<br>1924 苏联人发现的 SKI, 后来 Haskell Curry 的版本是 BCKW:<br><a href="https://link.segmentfault.com/?enc=wJV6eIKEgZE%2FoBB7C6J9Pw%3D%3D.c3N5KGL%2FsvpBuja0P8Jj76%2FglmKm%2BeGacKrWjx1QM0NifJ%2FNTz%2FG9PoLYCpSNZOR" rel="nofollow">https://esolangs.org/wiki/Com...</a></p><p>这些基本的算子, 注意跟 Haskell 语法一样, 都是柯理化的函数, 从左边开始计算.</p><pre><code class="coffee">S f g x = f x (g x);
K x y = x;
I x = x;</code></pre><p>写成 lambda 表达式的话:</p><pre><code>S = λx.λy.λz.(xz)(yz)
K = λx.λy.x
I = λx.x</code></pre><p>其中 <code>I</code> 可以用另两个表示, 也就是说最简的版本是 <code>SK</code> 组合子逻辑.</p><pre><code class="coffee">S K K x = I x = x</code></pre><p>相比 SKI, 另一个版本更直观一点, 大概都能对应到 Haskell 里的一些函数,</p><pre><code class="coffee">B x y z = x (y z);
C x y z = x z y;
K x y = x;
W x y = x y y;</code></pre><p>网上的说法, 本来这些组合子就跟 Haskell 当中有对应关系<br><a href="https://link.segmentfault.com/?enc=6yUFYjOInEgT5%2FnuSF1o8A%3D%3D.8N2ADyVOnEKPdvtzjMbK562ubjkVycUfABwS5D1rzr4re2Utw32EhyStV6HsashVcNj6TLlULn4gXFWLiyPU2hrzrJHfCKvXphwOZknDZcCllsHCJS59bHAfu60mjF5zGOCoU%2Fo579Rr%2FXqPg%2BMvWw%3D%3D" rel="nofollow">https://stackoverflow.com/que...</a><br>比如一些常见的 Combinators 对应 Haskell 或者 js 中的函数.<br><a href="https://gist.github.com/Avaq/1f0636ec5c8d6aed2e45">https://gist.github.com/Avaq/...</a></p><p>关于组合子的详细的介绍, 推导, 数字表示, 具体看这个文章了<br>Fun with Combinators: <a href="https://link.segmentfault.com/?enc=ix%2BcU9GSSYWukZlEou2dSw%3D%3D.60wkIZn%2Bko2GSgbFs1FtlXtHBbhxjyNYbFePmf2GyrxsU64UXPCkG12nLe13pjf2jDt81ZQXMmBOuz7lBS4ZJw%3D%3D" rel="nofollow">https://doisinkidney.com/post...</a></p><p>更详细的版本要看 Wiki 了..<br><a href="https://link.segmentfault.com/?enc=O4qTQjEHzrTBmgHBgttkSQ%3D%3D.%2B7JIyNTppvhd%2FsmOH3vjQejS8WXK3I4IePn7Arq4pzvQNbeGe9Hm%2BQ1PwQBTDK4U" rel="nofollow">https://en.wikipedia.org/wiki...</a><br>lambda 表达式都是可以转化为 SK 组合子的表达式的,<br>而 lambda 表达式当中使用了自由变量, SK 组合子当中全部都是组合子函数.<br>以前用于研究的语言, Unlambda, Miranda, 程序内不是转化到组合子再执行的.<br>不过由于组合子本身表示的程序会很冗长, 到 GHC 就改变很多进行性能优化了.</p><p>有个视频把 Combinator 用的树的结构具体展示了一下<br>"An Introduction to Combinator Compilers and Graph Reduction Machines" by David Graunke<br><a href="https://link.segmentfault.com/?enc=E1Wmo%2FzV8Cx2I%2FJ3mDFuDg%3D%3D.n1KKmUqW9r3UsRZosDIeMnOLMWj0tZhqz%2Bp4Qz1vzILTiG0kJS4UW1JM%2B%2B703%2BKM%2FchPdKGmKfrrNvaJ2qlwUA%3D%3D" rel="nofollow">https://www.youtube.com/watch...</a></p><p>Wolfram 的演讲视频, 中间涉及到很多 Combinators, 包括 SKI 原作者的历史<br>"Combinators: A 100-Year Celebration"<br><a href="https://link.segmentfault.com/?enc=AoDW0lRnxgHk%2Fy0vCYlJWQ%3D%3D.oMPmVXXojsNB%2FfwGoQsPo4cSeNprqC6GpMtKSxjxn1bOZLxK8yEKJ%2BhxBSe5c%2B9H" rel="nofollow">https://www.youtube.com/watch...</a></p><h3>代码实现</h3><p>有翻到别人的实现, 具体代码我还没能看懂...<br><a href="https://link.segmentfault.com/?enc=Bnc37ZFSvF82PbC9SXMJ4A%3D%3D.4fQk4xhq0Cgi8oVoTi7bV5q1RaAztqUGZ%2BWJvnrCphE%3D" rel="nofollow">https://github.com/ngzhian/ski</a></p><p>有翻到一个比较直观的版本, 某大佬实现的 combinator 的示例程序,<br><a href="https://link.segmentfault.com/?enc=UYEDKz58WLNspe1KTZBD7A%3D%3D.io8QE1l6ae3SQtrRyModUjj4qBcdUGTUtu332NNGgvIrc62N%2BWa1wr8%2FIvu06IZEKpLpvUp2bJ4SqehVmhGkpQ%3D%3D" rel="nofollow">https://crypto.stanford.edu/~...</a></p><pre><code class="hs">true = \x y -> x
false = \x y -> y
0 = \f x -> x
1 = \f x -> f x
succ = \n f x -> f(n f x)
pred = \n f x -> n(\g h -> h (g f)) (\u -> x) (\u ->u)
mul = \m n f -> m(n f)
is0 = \n -> n (\x -> false) true
Y = \f -> (\x -> x x)(\x -> f(x x))
fact = Y(\f n -> (is0 n) 1 (mul n (f (pred n))))
main = fact (succ (succ (succ 1))) -- Compute 4!</code></pre><p>对应到 SK 组合子结构...</p><pre><code class="lisp">s(k(s(skk)(skk)))(s(k(ss(k(s(skk)(skk)))))k)(s(k(s(k(s(s(s(s(skk)(k(k(sk))))(kk))(sk))))(s(s(ks)k))))(s(k(ss(k(s(k(s(k(s(k(ss(k(sk))))))(s(k(s(k(ss(kk)))k)))))(s(k(ss(k(s(k(s(k(s(k(s(k(s(skk)))k))))(s(skk))))k))))k)))))k))(s(s(ks)k)(s(s(ks)k)(s(s(ks)k)(skk))))uz</code></pre><p>具体实现还是没看懂..<br>大致意义, 以前也听到过说 church number 这种基于 lambda 的数字表示,<br>比较抽象, 这次看到真的基于这种方式表达的代码实现了, 开始相信了.<br>理论上 SK 组合子有着图灵等价的能力(?), 应该是能表示任何的算法的.</p><p>unlambda 是一门很早的基于 SK(I) 组合子实现的语言,<br><a href="https://link.segmentfault.com/?enc=QrmVjdsLHXqmL0h5fcZopQ%3D%3D.OHDe1SnMp4sOwynXHEsInFwUL6%2BMHmcawpK%2BfGYLmLXw3EbgbxK%2Bkh1h6yZl%2BOlm" rel="nofollow">http://www.madore.org/~david/...</a><br>这个名字 unlambda 是因为里边用了 SK 代替了 lambda 表达式.<br>就像前面说的组合子的用法, "everything is a function".<br>代码就显得极其晦涩了, 我就有点奇怪为什么作者一点圆括号都不用...</p><p>对应上边的用函数表达条件表达式 true, false 的写法, 在 lambda calculus 也有,<br><a href="https://link.segmentfault.com/?enc=J%2B3IJxTEax5Gb5SUFjhNhQ%3D%3D.soQFqDoFjI73u1sNqcERwrFxhk5atb11e1E6PWNbRCUGUgf9tgD%2FxmPvNesHXA96fBbVYWk6Bd7HEzU6tG0yKxW94bBCCqA%2FGybrOawD%2FFslySyoUdBKMP%2B6%2B9gC4Q%2B9amriJYryNKm1bd0Qo02gKA%3D%3D" rel="nofollow">https://medium.com/@marinalim...</a><br>但是这个在理解上也要注意, 涉及到 lazy evaluation 的概念了,<br><a href="https://link.segmentfault.com/?enc=HX72fji9Z3o8jwgp2%2BzkBw%3D%3D.rEgFEHh4RvAsdYo0SYlrHKk5Hj6MnpWCtG%2BvqDY4ZMFidSE9vu36N%2BHe8I%2BPbNf%2FsUOxCN5Fo8U0jOJDZ%2Bdfubwt3ECm0qtTBVIIPkzMZKE%3D" rel="nofollow">http://augustss.blogspot.com/...</a><br><code>if cond t-branch f-branch</code> 普通函数的版本, 所有参数都被求值的,<br>但作为条件表达式, 比如在 JavaScript, 明显只有其中一个表达式被求值,<br>在 strict evaluation 的语言当中, 基本上就是这样了, 参数会先求值掉, 再调用函数,<br>而这种条件表达式, 就需要语言内部或者 macro 提供能力, 来对分支进行求值.<br>在 Haskell 这样的语言, 却结果 lazy evaluation 能统一地实现该功能.<br>其中 branch 的代码表示作为 chunk 存在, 真的命中分支了, 才进行展开求值.<br>lambda calculus 和组合子这种数学化的表示, 应该也是对应 lazy 的版本.</p><h3>其他</h3><p>还有个 paper 详细解释了一下用组合子模拟数值和计算的具体过程.<br><a href="https://link.segmentfault.com/?enc=Uw99EJLeo5s7Nfi%2F1ZQYCw%3D%3D.iFFeFSQmj4E7xFmRwyCk8L%2B%2FEw%2BkqwlyPs33dglV%2B7fu4PzpsTkkrCAQXuMNh6if%2BN8uDUB3qukWjHgMoYSWRQQSbe2Wyx%2Fv7hIf0F9CbwnHKoasnxbyr8v79stQC9uR" rel="nofollow">Combinatory Logic: From Philosophy and Mathematics to Computer Science</a><br>以及 lambda 写法 <a href="https://link.segmentfault.com/?enc=ImAzpFadXGHTDtg0VAjYxg%3D%3D.zsGnhQ2zl0bAqWqiTHbs1f56GFROdNedzMtTAqNik%2B4%3D" rel="nofollow">https://dotink.co/posts/lambda/</a></p><p>Combinator 在 JavaScript 提供抽象能力的一些展示.<br><a href="https://link.segmentfault.com/?enc=yq2wz9bgMPM3Kkz1vXwwng%3D%3D.SuBshz1L4msg1vpRe9wLAXlQCkkBGgpkS6Y0LENOkygJj3eBT66i9%2FmUgnmDUZ8t" rel="nofollow">https://codeburst.io/combinat...</a><br>这个例子透露出来, Combinator 提供了强大的抽象能力,<br>我们平时编写程序, 函数内部没有对 free variables 的约束, 可以随便引用,<br>随便引用也就意味着这个函数的依赖, 甚至隐形依赖, 就会很多,<br>而基于 Combinator 不能使用 free variables, 所有依赖就只能从函数参数进入,<br>这样, 基于函数参数就有很多控制的手段了, 就有强大的抽象能力.<br>这样做伴随的问题就是抽象能力很强, 代码容易晦涩.</p><hr><p>后续新闻中出现的 Binary Lambda Calculus (BLC) 实现:<br><a href="https://link.segmentfault.com/?enc=5fOplPa0tW9j9aJMXT0c4A%3D%3D.1yENvX1z2sefQlXZAut%2F6%2FMcNcEPUG7Jm2pFDANZ1GK%2BS8cQHn0l5J4oyuko7tfu" rel="nofollow">https://www.ioccc.org/2012/tr...</a></p>
交替使用 TypeScript 和 Nim 的一些感想
https://segmentfault.com/a/1190000039247339
2021-02-21T23:59:23+08:00
2021-02-21T23:59:23+08:00
题叶
https://segmentfault.com/u/tiye
1
<p>交替使用 TypeScript 和 Nim 的一些感想</p><p>我之前的背景主要是 js 和 ClojureScript, 对类型了解很有限,<br>到 Nim 算是才开始长时间使用静态类型语言吧. TypeScript 那只当 type checker.</p><h3>Nim 的明显问题</h3><p>JavaScript 到底是 Google 砸钱了的, 调试的体验真的是好.<br>至于 Nim, 大部分的报错靠着类型信息倒是也能定位出来,<br>不过没有趁手的断点调试工具, 经常要靠大量的 log, 我也挖得不够深.<br>Profiler 我也用过, 导出的是文本的调用栈和开销占比, 当然没 Chrome DevTools 清晰.</p><p>VS Code 使用体验自然也远远不能跟 TypeScript 比, 我直接 Sublime 了.</p><p>Nim 的 echo 首先就很让我头疼, 没有自动加空格, 挺烦的.<br>Nim 没有内置的 string interpolation, fmt 不确定是函数还是 macro.<br>通常 <code>fmt"balbla {b}"</code> 这样的语法可以插值, 但是效果完全不如语言级别的插值方便,<br>这个写法中如果有 <code>{</code> 或者 <code>}</code> 还需要手动处理, 我在使用的时候就感觉比较糟心.<br>要在语言里边实现 interpolation, JavaScript 或者 CoffeeScript 都不会这么麻烦.</p><h3>Nim 类型和运行时的一致性</h3><p>TypeScript 虽然有很风骚的类型系统, 但我也没用着 strict, 也不是所有依赖都 ts.<br>然后偶尔会遇到写了类型但是底下完全不是那么回事, 就很莫名.</p><p>Nim 的类型跟数据是直接对应的, 使用当中除了一些 edge case, 都能对应上,<br>意味着类型检查报错的地方修复, 代码对应的报错也就解决了,<br>这让我感觉到类型才是可靠的. 当然, 很多静态语言应该就是这样子.</p><h3>Method call syntax</h3><p>Nim 不是面向对象语言, 里边的 object 大致对应 C 的 struct, 而不是对象.<br>object 里边就是数据, 这个还是比较习惯的.<br>不过代码观感上, Nim 还是有点贴近 js 这样支持 OOP 的写法的,<br>我是说大量的 <code>a.b(c)</code> 这样方法调用的语法, Nim 里边叫做 Method call syntax.<br>也刚知道这在 D 里边已经有了, Wiki 上都明确说了:<br><a href="https://link.segmentfault.com/?enc=cZRROAWYGK6pA5l33k5vFg%3D%3D.bS1asJmkYRcPGZD6Btbndm1DYplN0s2XUgBfrrR%2BacU%2FMdQOPz24URsn5J1MA1InKx00N8TFr8ZZz05s5BSGhA%3D%3D" rel="nofollow">https://en.wikipedia.org/wiki...</a></p><p>这个特性对应的 Nim 代码是这样子的:</p><pre><code class="nim">type Vector = tuple[x, y: int]
proc add(a, b: Vector): Vector =
(a.x + b.x, a.y + b.y)
let
v1 = (x: -1, y: 4)
v2 = (x: 5, y: -2)
v3 = add(v1, v2)
v4 = v1.add(v2)
v5 = v1.add(v2).add(v1)</code></pre><p>最初我使用的时候没有在意, 但是随着迁移一些代码到 ts, 才感受到灵活.</p><p>在 JavaScript 当中, 继承, 多态, 依赖 class 结构才能实现,<br>这也意味着我要定义 class, 然后 new instance, 然后才能用上.<br>但定义了 class 也就意味着这份数据不是 JSON 那样直白的数据了.</p><p>我对 OOP 使用不多, 但是思来想去大致也理解, 动态类型能做到 JavaScript 这样已经很强了.<br>Nim 的多态是通过编译器判断类型来实现的, 比如前面的 <code>add</code> 可以有很多个 <code>add</code>,</p><pre><code class="nim">proc add(x: Y, y: Y): Z =
discard
proc add(x: P, y: R): Q =
discard</code></pre><p>后边遇到 <code>o.add(j, k)</code> 根据类型在编译时候就能实现多态了.<br>当然这在 JavaScript 靠 class 是能够实现的, 但那就一定要把数据操作绑在一起了.<br>长期使用受 Scheme 影响的语言, 对 class 这个臃肿的做法就很难适应.</p><p>有类型的情况下, 在这套方案当中 overloading 很自然的,<br>比如 Nim 当中对类型 <code>A</code> 定义 equality 判断的写法这这样的,</p><pre><code class="nim">type A = object
x: number
proc `==`(x, y: A): bool =
discard</code></pre><p>没有耦合在一起, 意味着我引用 <code>A</code> 类型在另一个项目也能自行扩展,<br>而且这基于类型的, 不会修改到原始的模块当中, 不影响到其他引用 A 的代码.<br>这一点, 我的代码从 Nim 转译到 TypeScript 就比较头疼,<br>因为我定义数据结构的访问和判断需要 overload 这些个 array accessing 和 equality,<br>我一时半会也想不出来 TypeScript 当中能怎么做, 只能用 mutable data 在运行时强行模拟.</p><p>Nim 里边就很简单, 我对 <code>[]</code> 进行重载, 后边就能 <code>ys[index]</code> 直接用了:</p><pre><code class="nim">proc `[]`[T](xs: MyList[T], idx: number): T =
discard</code></pre><p>这个对于 iterator 的场景也是类似, 定义了 iterator 就能直接写 for..in 了.<br>iterator 这在 JavaScript 当中也行, 只是说 Nim 当中很多运算符都能自己重载.<br>然后好处也是比较明显的, 比如我重构了操作内部实现, 但使用的地方基本不需要调整.<br>而在 JavaScript 里边, 长久我就习惯性直接面对 Array 跟 Object 了.</p><p>没有碰过 Java 跟 C#, 碰过语言当中这套玩法跟 Haskell 倒是挺像的,<br>Haskell 从 class 产生 instance 的时候可以定义一些函数, 就很灵活.<br>(具体 Haskell type class 高阶玩法真是还玩不起来.)<br>不过 Nim 相比来说, 简化是简化了, 但这个语法糖在编码当中就是很方便.<br>也因为缺失这个功能, 导致我对 Clojure 跟 TypeScript 这都有点不适应了.</p><h3>动态数据的类型</h3><p>转译代码还发现的问题是由于 JSON 跟 EDN 极为便利,<br>引起我在大量代码当中直接使用 Array 和 Map 直接表示数据,<br>这个不能算错, 但数据在 Nim 当中这些都是明确用类型表示的,<br>也意味着在 Nim 当中有明确的结构进行判断, explicitly...</p><p>反观我的 TypeScript 代码, 大量的 <code>Array.isArray</code>,<br>然后还有那个不知道怎么说的 <code>typeof x === 'object'</code> 的尴尬,<br>在类型系统当中使用习惯之后, 回过头感觉特别不踏实,<br>然后我自动跑去折腾 <code>instanceof</code> 的玩法来做对应功能了.</p><p>当然, JSON 或者 EDN 通用地表示各种数据, 确实在通用型来说非常好,<br>我跨项目调用数据, 这些动态类型就是直接通用的结构,<br>在 Nim 当中, 一般传递数据是会涉及到一些类型转换, 写写是有点麻烦的.<br>我不是很能衡量那种方案是更好, 但是对于底层类库, 我是希望有明确类型的.</p><h3>内存相关问题</h3><p>因为要编译到 C 运行, Nim 当中的数据结构多少还是要涉及到一点内存的部分.<br>不过好在 Nim 当中指针绝大部分已经封装成 ref 了, 也很少要去操心.<br>主要是感觉就是不同数据结构之间性能的区别比较容易体现出来了.<br>这个在 JavaScript 这边, JIT 老是偷偷帮忙优化, 自己写出问题没那么容易察觉.<br>我感觉到如果我早使用 Nim 的话, 对算法和性能的朦胧感就会轻很多.</p><p>而能触碰到内存, 也就意味着内存管理会遇到一些问题,<br>我之前遇到, 似乎是用 macro 的时候, 遇到语言内部的代码出错了,<br>然后 illegal memory access, 这就变得很无助了,<br>论坛上给我的帮助让我编译 Nim 编译器本身然后打 log 来获取细节,<br>这个体验还是蛮新奇的, 反正 V8 我是没有自己带参数编译过...</p><p>至少我目前用到的还不需要很清晰了解内存布局细节, 以后再看吧...</p><h3>一些语法细节</h3><p>Nim 当中的语法糖还是挺多的, 有很多使用 CoffeeScript 时候那种轻快的感觉,<br>比如说 JavaScript 现在不好加语言级别的 range,<br>这在 Nim 当中直接用 <code>..</code> 或者 <code>..<</code> 就能生成 range 了:</p><pre><code class="nim">for i in 0...<n:
echo i</code></pre><p>然后 if 在 Nim 当中虽然比起 CoffeeScript 有那么点不顺手, 但也还是表达式:</p><pre><code class="nim">let a = if b:
c
elif d:
e</code></pre><p>这样的代码转到 TypeScript 马上就变得挺长了, 我更不能用三元表达式去牺牲可读性.<br>当然还是跟 CoffeeScript 去比的话, Nim 毕竟还要考虑类型, 没的那么灵活.</p><p>对于代码格式化, Nim 有个内置的 <a href="https://link.segmentfault.com/?enc=w8BWaavsXaMVYnilFjMctA%3D%3D.FmFl%2F2UEkYRn6Hq9Z8%2B4XMspivWjwV8kzi1Hk5WffTgwlDJCPBtYdVKgeD3tWRe%2B" rel="nofollow">nimpretty</a> 命令.<br>我没怎么用, 只是试了一下, 快当然是很快的.<br>不过我用缩进写代码本来就已经精确管理空格了, 再弄一个好像没必要, 也没手写灵活.</p><h3>其他</h3><p>TypeScript 的强大是我不得你承认的. 为此我对 AssemblyScript 还挺期待的.<br>但是随着 Nim 带来的这些感受, 我也起了一些疑惑.<br>比如说基于 WebAssembly 我们将来有个更好的浏览器语言了, 怎样才更好?<br>一方面要兼顾 Web 应用大量的界面处理的场景, 一方面高性能和灵活,<br>单纯 AssemblyScript 这样, 总感觉还是不够的吧</p>
关于双平衡三进制的表示和计算
https://segmentfault.com/a/1190000038478182
2020-12-15T02:01:01+08:00
2020-12-15T02:01:01+08:00
题叶
https://segmentfault.com/u/tiye
2
<p>双平衡三进制计算细节</p><p>很久以前罗列过一些关于"双平衡三进制"的内容, 都挺短的, 而且有点过时,<br><a href="https://segmentfault.com/a/1190000000474676">https://segmentfault.com/a/11...</a><br>最近在 Nim 里边实现了一个版本, 顺着整理了一下.<br><a href="https://link.segmentfault.com/?enc=fEk115Oz2%2FMUI5qowN7ZzA%3D%3D.aQw3%2B4XUAr%2FLED6vM0qr9tuI3Wht5ChWiMg52FkSUxm4vrXClg%2FntZet8vYzUzETMRXW5kPpo34y%2FGKxZQ99rnkqLY9wc9LooihhbfzLvE8%3D" rel="nofollow">https://github.com/dual-balan...</a></p><h3>平面的表示</h3><p>双平衡三进制是我大学时候发现的一个比较有意思的平面表示方式,<br>中学数学, 我们用笛卡尔坐标系来表示平面的, 而且图形界面也常用这个方法,<br>但是笛卡尔坐标系表示平面用的是两个数字, 就没有那么直观了.<br>基于一维的线段上十进制表达点位置的方式, 我推想怎样表示平面更直观...</p><p><img src="/img/bVcLB5b" alt="image.png" title="image.png"></p><p>首先一维的表示位置的方式, 先有一个单位长度 1, 超出长度放大 10 倍, 直到包含目标位置,<br>然后要精确定位的话, 就是十等分再十等分, 直到无限接近目标位置.<br>在平面上, 单位元一般就是一个正方形, 那么仿照一维的方式,<br>如果目标位置在外部, 就不断倍数放大正方形, 然后等分正方形, 直到无限接近目标位置.</p><p>这样做, 有个绕不过去的问题, 就是单位放大的时候, 默认是朝着一个方向放大的,<br>1, 10, 100... 然后反方向就需要负数表示了, 在二维空间也是一样,<br>我就想能不能直接用更简洁的方式表示呢, 后来想到把正方形按照九宫格放大,<br>那么, 原来的的单位正方向, 就处在大的区域中间, 这样也是满足要求的,<br>而等分的时候, 单位正方形中心点, 依然是中间的正方形的中心点,<br>这样在思路上是行得通的, 也相对比较直观, 也就是数独的样子</p><p><img src="/img/bVcLB48" alt="image.png" title="image.png"></p><p>后来仔细了解的时候, 我了解到对于一维来说, 已经有个"平衡三进制"的表示法<br><a href="https://link.segmentfault.com/?enc=mju09OlQtAJpizIzZCymuA%3D%3D.99tj3d6vANBgNLXQcy8I9Af4%2FWnYRPoF2mr2fbmowJY8wx%2BUvQIhNq%2F6O6R%2FepuNTVHQQI8aRPrZRFgWgMoL5eykNzMVpRe9kxBihsm6yb4%3D" rel="nofollow">https://zh.wikipedia.org/wiki...</a><br>这个就跟我用九宫格表示平面的思路一致了, 零点在中心, 左右两边各一个单位长度.<br>于是我这边的命名也就跟着同步, 叫做"双-平衡三进制"了.</p><p>(写文章的时候搜到个图示, 这个很好展示了"三进制"和"平衡三进制"的区别.)</p><p><img src="/img/bVcLB5c" alt="image.png" title="image.png"></p><h3>符号定义</h3><p>后面为了简化表示, 我考虑定义一下符号, 刚开始想过箭头什么的,<br>不过因为电脑不方便输入, 而且也很奇怪, 没深入.<br>我注意到是九宫格, 就把三阶幻方的数字引过来试试了,<br>三阶幻方古代有个名字叫"洛书", 我也拿过来当项目代号了, 长这样:</p><pre><code>4 9 2
3 5 7
8 1 6</code></pre><p><img src="/img/bVcLB45" alt="image.png" title="image.png"></p><p>幻方基本的数学性质就是平衡, 行列斜向各自相加分别都是 15.<br>我后来表示的时候, 为了直观的方便, 把 1 定为前方, 所以就倒转了一下,<br>大致想想, 1 放在北方, 我们现在的人看看是更习惯一点, 而且后面计算也方便.</p><pre><code>6 1 8
7 5 3
2 9 4</code></pre><p>基于这样的方式, 就可以用来表示一个平面上的各个位置了.<br>比如 <code>&326</code>, 前面用 <code>&</code> 标记是一个"双平衡三进制数",<br>在这个区域里边, <code>&326</code> 第一位 <code>3</code> 定位了右边一个大的区域,<br>然后第二位 <code>2</code> 定位了左下角位置, 再然后 <code>6</code> 定位右上角位置,<br>这样就比较直观表示了一个区域了, 如果用笛卡尔坐标系就是 <code>(5, -2)</code>.</p><p><img src="/img/bVcLB46" alt="image.png" title="image.png"></p><p>这样一对比, 也可以看到所谓的"直观"其实只是某个方面上的直观,<br><code>&326</code> 我知道 <code>&3??</code>第一位的时候, 就能知道一个大致的区域了,<br>然后再是 <code>&32?</code>, 一步步更加精确到 9 个格子当中的一个.<br>从简短来说, <code>(5, -2)</code> 已经非常简短了. 换一种表达, 反而边长了.</p><p>因为这样的对应关系, 所以笛卡尔坐标表示的区域, 也就能用双平衡三进制表示.<br>至于实际上是不是有好处, 这也就比较飘忽了.</p><h3>数值计算</h3><p>现在我用 Nim 实现的模块当中包含加减乘除这四个运算,<br>中间包含反转和旋转, 不过相对来说不那么实用了.</p><p>基于上面那个图, 可以看到比较直接的加法的一些规则:</p><pre><code class="nim">&5 + &5 = &5 # 这个是原点了
&5 + &1 = &1 # 原点加上任何 x 都得到 x
&3 + &7 = &5 # 两个相反的数想相加, 得到原点
&4 + &6 = &5 # 同上
&1 + &7 = &6 # 左 + 上 = 左上</code></pre><p>然后还有包含进位的情况:</p><pre><code class="nim">&1 + &1 = &19 # 进位了, &19 相当于 3 + (-1)
&4 + &4 = &46 # 右下角
&8 + &1 = &14 # 对照图形了...</code></pre><p>参照图形上的样子, 这个还是比较容易看出来的.<br>对于减法, 也就是先对数值去反, 然后再相加. 比较简单了.</p><p>然后是乘法,</p><pre><code class="nim">&1 * &1 = &1 # 这时候 &1 是不动点, 单位元
&1 * &7 = &7 # 单位元乘以任何数 x 都得到 x
&9 * &9 = &1 # 相当于 (-1) * (-1)
&3 * &3 = &9 # 对照图形, 相当于旋转
&7 * &7 = &9
&4 * &4 = &73 # 注意旋转加上进位</code></pre><p>复杂的我不罗列了. 但是中间有个比较有意思的规律,<br>就是奇数奇数相乘的时候, 偏偏跟十进制乘法的尾数对上的,<br>比如说 <code>3 * 7 = 21</code>, 这边也有 <code>&3 * &7 = &1</code>, 以及三三得九.<br>感觉不是巧合, 但是我也没仔细挖掘出来, 验证了一下都是满足的.<br><code>&3 * &4 = &2</code>, <code>&4 * &9 = &6</code>, 都是的.</p><p>进位方面跟十进制或者二进制都是一样玩的, 不深入说了.</p><h3>除法</h3><p>勉强弄了一个示意图, 复数的除法, 对应的是旋转, 在双平衡三进制中也一样,<br>新得到的商, 相对于原来的九宫格坐标系, 就相当于一个旋转缩放的新的九宫格坐标系,<br>比如 <code>&3.811</code> 这个位置, 在商的这个坐标系当中还是有着九宫格对应的嵌套关系,<br>从视觉上来说, 除法就是原来的点(大致在 <code>&3.64</code>)在新的坐标系当中的位置,<br>视觉上按照一个个格子, 定位下去就行了. 可惜在程序当中不可能做视觉计算, 大量算距离太慢了..</p><p><img src="/img/bVcLMOY" alt="image.png" title="image.png"></p><p>除法实现起来是比较头疼的, 虽然也可以参考十进制二进制的思路,<br>不过中间实现会遇到一个问题, 就是某些情况会不够准确.<br>十进制除法, 因为是不平衡的, 大小区分就很容易, 大于就大于了.<br>然而表示平面的时候, 就没有那么直观的大于, 因为会有多个方向的.<br>比如图上看 <code>&199</code> <code>&511</code> 的位置, 就是挨着的, 但是首位数字差挺多的,<br>这就导致列竖式进行计算的时候, 有些情况取值会不准确,<br>特别是写程序的时候, 总不能计算的时候再强行算一下偏差大小作对比吧.</p><p>后来经过比较多的思考, 我尝试参考复数的计算, 分开坐标轴计算,</p><pre><code>a + b*i (a + b*i) * (c - d*i)
-------- = ---------------------
c + d*i (c + d*i) * (c - d*i)
(a * c - b * d) + (b * c - a * d)*i
= -----------------------------------
c * c + d * d
(a * c - b * d) (b * c - a * d)*i
= --------------- + ------------------
c * c + d * d c * c + d * d</code></pre><p>其中 <code>c * c + d * d</code> 在复数计算当中已经知道, 只有一个方向的分量的值.<br>在 x 方向上, 只有 <code>&3</code> <code>&7</code> 的值, y 方向只有 <code>&1</code> <code>&9</code>,<br>这样就能进行转化沿用复数的方案了, 加减法和乘法都是有的.<br>然后我就只需要搞定 <code>&1</code> <code>&5</code> <code>&9</code> 构成的平衡三进制除法就好了.<br>这个步骤还是比较绕的. 我想了想还是担心平衡三进制除法有问题, 最后实现出来效果还行.<br>具体涉及到代码, 我也不再展开了, 思路就是这个思路了.<br><a href="https://link.segmentfault.com/?enc=kOlQz8tGaY89bnstll8gtQ%3D%3D.FdtL2QhVv3zVHEb%2F86PoAZOTgOAg0PYNAmGXcULLISAt5oDFB4tnB6DILXo%2FjJuPs9%2Bb0R7aLGPIDYSbCVrVlAabZWkhK01mYG3WYmdU%2Bpk%3D" rel="nofollow">https://github.com/dual-balan...</a></p><h3>其他</h3><p>从前面的描述也就能看到, 双平衡三进制的思路有点不一样,<br>十进制我们的习惯是一个点对应一个位置, 包括二进制三进制也是,<br>但是双平衡三进制, 比如 <code>&1</code>, 虽然也是一个点, 同事也像是代表一个区域.<br>具体从计算来说, 还是一个点, 否则没法有准确的值了.</p><p>习惯上十进制的数, 分数的时候, 我们有 <code>0.5</code> 表示一半, <code>0.33333...</code> 表示 <code>3/1</code>,<br>这里拜三进制所赐, <code>1/2</code> 是一个无法整除的数, 可以是 <code>&1.11111...</code> 或者 <code>&19.99999...</code>.<br>可以看到边界上的一个点是可以从两个方向逼近的, 或者也可以是 4 个方向,<br>比如 <code>&8.888...</code> <code>&82.222...</code> <code>&14.444...</code> <code>36.666...</code> 都逼近同一个点.</p><p>目前综合看下来, 这个方案就是比较有意思, 但是说不上能有什么好处,<br>另外看到 WIKI 上还有平衡三进制的开分计算, 就复杂了, 没有尝试过.</p>
关于 ternary-tree 不可变数据结构复用方案的一些解释
https://segmentfault.com/a/1190000038456432
2020-12-12T00:49:30+08:00
2020-12-12T00:49:30+08:00
题叶
https://segmentfault.com/u/tiye
0
<p><a href="https://segmentfault.com/a/1190000038390819">前面一篇讲 ternary-tree 模块的文章</a>是丢给 Clojure 论坛用的, 写比较死板.<br>关于 ternary-tree 开发本身的过程还有其中的一些考虑, 单独记录一下.<br>中间涉及到的一些例子不再详细跑代码录了, 看之前那篇文章应该差不多了.</p><p>首先 structural sharing 的概念, 在看 Clojure Persistent Data 那篇文章之前, 我也是模糊的.<br>常规的, 如果按照 C 学习的话, 一个 struct 对应的是连续的内存,<br>然后要不可变数据结构, 就是要复制才可以, 当然这样就无法达到 sharing 的概念了.<br>而具体到 Clojure 那个 Persistent Data, 他是用 B+ 树实现的, 才能复用结构.<br>那个系列文章其实讲得蛮详细了, 就差对着代码分析每个操作了.</p><p>我刚开始弄 ternary-tree 模块的时候, 只是看了文章前几篇,<br>后面几篇关于位操作还有性能方面的, 看得迷糊就没仔细读了.<br>Clojure 关于 vector 操作的源码, 我也是后来再去看了下. 其他部分也没去看.<br>所以当时对 Clojure 具体的实现, 心里还是有点茫然的.<br>当然, 从前面的文章当中, 我知道, 那是要的 B+ 树, 然后 32 分支, 然后结构复用.</p><p>我为了简化问题, 就考虑直接用比较少的分支, 比如 2 个 3 个这样,<br>选择 3 的原因首先还是考虑到数据插入有从开头插入, 从结尾插入, 都有,<br>设定 3 个分支的话, 操作应该会比较平衡, 所以我就用 3 来尝试了.<br>当然 3 有个问题, 计算机是二进制, 那么"除以2"这个操作就比较快, 而 3 会慢.<br>当时就没管这么多了, 并没有指望性能追上那个 Clojure 的实现.</p><h3>树的初始化</h3><p>树形结构存储数据, 从基本的就能知道, 要访问数据需要一层层从根节点访问进去,<br>我设计每个内部节点上有 size 树形, 记录当前分支的大小,<br>然后访问 idx 位置的话, 按照子节点的 3 个 size 分别算就行了, 这个而简单,<br>那么要性能快, 就是要查的次数尽量少了, 也就是树的深度尽量少.<br>这样很容易就有一个方案, 初始化时候每个分支数据尽量平分, 这样深度就会尽量少.<br>那么到每个节点来说, 个数除以 3, 余数可能是 0, 1, 2, 那么只能说尽量平均吧.<br>我当前的是按照平衡来的, 多一个放中间, 多两个放两边, 这样尽量是平衡的.</p><p>使用以后, 发现这个方案也不是最优的, 因为我打印一看就知道很多的空穴.<br>除了性能, 整个树也是有储存空间的消耗的, 叶子节点是数据, 肯定是需要的,<br>然后数量的区别就是不同的结构, 导致的中间节点数量不同.<br>比如说 <code>[1 2 3 4 5 6]</code> 这个序列, 就可能不同的结构,<br>首先是我按照平衡分配的方案, 先 3 等分, 然后再左右均分:</p><pre><code class="clj">((1 _ 2) (3 _ 4) (5 _ 6))</code></pre><p>这个例子当中内部节点, 4 对括号对应 4 个节点, 加上 3 个空穴.</p><p>或者我手动紧凑一点, 但不按照平衡的逻辑来:</p><pre><code class="clj">((1 2 3) (4 5 6) _)</code></pre><p>可以看到是 3 对括号就是 4 个内部节点, 加上 1 个空穴.<br>明显, 这个比起上面是更加紧凑的, 当然这个是手动排列出来的.<br>可以设想, 数量更大的列表, 结构的可能性会更多, 空穴也会更多.</p><p>当然, 极端一点, 比如我每次新增元素都在当前节点右边, 那结果就更夸张了:</p><pre><code class="clj">((_ (_ (_ (_ 1 2) 3) 4) 5) 6)</code></pre><p>5 对括号了, 空穴也有 4 个, 就比较浪费, 每增加一个元素就增加一对括号, 一个节点.<br>当然这个明显有问题, 就是树的深度, 数据到 N 就就会有 N 层, 性能肯定不行,<br>最少也要保证, 至少初始化的时候, 树的深度要尽量小.</p><p>空穴的多少, 其实也还有一个考虑, 就是后续插入数据的时候, 空穴增加还是减少.<br>比如说平衡的那个, 我需要往中间插入数据的话, 就有可能利用空穴.<br>注意, ternary-tree 这个还是不可变数据, 插入数据并不是说直接填上去,<br>从实现来说是复用部分的分支, 能复用越多越好, 某种程度上, 填空穴也能认为复用多.<br>空穴这个, 主要是优化存储的效率.</p><p>比如深度为 3 的话(根节点也算进去), 最终是容纳 3 * 3 总共 9 个节点, 4 就是 27,<br>这样空间利用的效率, 同个深度就是最高的. 4 层能存 27 个数据.<br>主要就是不满 27 个数据时, 4 层以内, 中间的数据怎么排列?<br>也大致可以知道, 手动设计每个节点 3 个位置尽量填满, 利用率是最大的.</p><p>所以前面文章我想到一个方案是尽量放到中间去, 一定程度上减小生成的体积.<br>不过实际试了一下, 那样填的话, 要算中间取多少个, 计算就挺复杂了,<br>复杂的计算对性能有点影响, 而且数据集中在中间, 中间就是满的,<br>结果就是新的数据在中间插入的话, 肯定很容易增加深度, 也未必是好的.<br>所以没有想清楚到底好坏这个结果. 我没有切换掉方案.</p><h3>头尾增加数据</h3><p>就数据的高频操作来说, 从头部和尾部追加数据是高频的, 特别 Clojure 这种依赖尾递归的场景.<br>就前面来说, 数据初始化的时候集中在中间的话, 那么后续从头尾加, 也方便紧凑.<br>直观理解的话要看几个简单的场景了. 我就拿这个数据做例子, 在后面增加 7:</p><pre><code class="clj">((1 _ 2) (3 _ 4) (5 _ 6))</code></pre><p>从结构复用的角度来说, 最粗暴的当时肯定是直接增加,<br>为了直观我左右调整一下, 看清楚增加的位置:</p><pre><code class="clj"> (_ ((1 _ 2) (3 _ 4) (5 _ 6)) 7)
(_ (_ ((1 _ 2) (3 _ 4) (5 _ 6)) 7) 8)
(_ (_ (_ ((1 _ 2) (3 _ 4) (5 _ 6)) 7) 8) 9)</code></pre><p>可以看到, 这样子每次增加新数据, 中间的结构都是完全复用的,<br>缺点么就是深度增加极快了.</p><p>或者采用一点更复杂的策略, 从右边开始, 看看有没有直接能用的空穴, 有就复用,<br>然后再看看深度是不是比左边的小, 小的话就折叠一下, 只要不比左边的深就好了,<br>这样尽量多一点堆叠起来, 至少在插入的时候复用一下内存空间:</p><pre><code class="clj"> ((1 _ 2) (3 _ 4) (5 _ 6))
((1 _ 2) (3 _ 4) (5 6 7))
(((1 _ 2) (3 _ 4) (5 6 7)) 8 _)
(((1 _ 2) (3 _ 4) (5 6 7)) (8 9 _))
(((1 _ 2) (3 _ 4) (5 6 7)) (8 9 10))
(((1 _ 2) (3 _ 4) (5 6 7)) ((8 9 10) 11 _))
(((1 _ 2) (3 _ 4) (5 6 7)) ((8 9 10) 11 12))</code></pre><p>挺复杂的, 判断逻辑就多出来很多了. 实际的代码规则其实也比较绕了...<br>这样做的话, 可以想象, 左边的一些分支是复用的, 右边就不一定了.<br>然后往上的那些根节点都是会被查新创建的, 为了不可变数据嘛, 会有大约 log3(N) 的一个消耗.</p><p>这个是当前 ternary-tree 代码当中使用方案, 实现起来还没很复杂.<br>应该说是一个兼顾了内存使用效率和数据复用的一个方案. 偏向于内存使用效率.<br>同时由于前面的部分一般是复用的, 可以看到空穴就是留着没动.</p><p>由于 ternary-tree 这个对称的特性, 如果换成从头部插入数据, 这个基本也是一样的.</p><h3>内部插入数据</h3><p>然后是在内部插入数据的情况, 当然这边不可变数据, 其实还是从根节点开始创建索引的,<br>那么, 左边和右边的一些数据 还是有可能复用的, 比如说下面这个例子,<br>在 after 2(对应到元素 3 的位置)的位置插入一个数据 88,</p><pre><code class="clj">((1 _ 2) (3 _ 4) (5 _ 6))
((1 _ 2) (3 88 4) (5 _ 6))</code></pre><p>可以看到最左和最右的分支可以被继续使用, 然后中间相近的还是要重新创建索引了.<br>大致是这么一个情况.<br>然后再看一个如果没有空穴的情况呢? 在 after 4(对应元素 5 的后面):</p><pre><code class="clj">((1 2 3) (4 5 6) (7 8 9))
((1 2 3) (4 (5 88 _) 6) (7 8 9))</code></pre><p>可以看到, 这种情况为了复用左后, 就是中间直接增加和展开, 也就是增加了深度.<br>也就意味着如果持续在中间的某些位置增加的话是很容易增加深度的的,<br>这不像是在头尾连续增加, 头尾的话可以对元素做一些位移, 然后复用的时候控制一下位置,<br>中间的话能调整的空间就不多了, 中间分支增加深度以后, 周围那是没有增加深度的.<br>但是从访问中间的数据来说, 访问的深度就容易增加很多了. 性能隐患.</p><h3>concat 和 slice 操作</h3><p>concat 跟前面的尾部增加数据相似, 只不过现在换成了增加的是一串数据,<br>简单的 concat 方式就是增加一个共同的父节点了. <code>(A _ B)</code> 这样子. 访问是不影响的.<br>这样的隐患也明显, 就是多次之后树的深度增加也是很快.<br>现在 ternary-tree 的方案是设定一个深度的范围, 增加到超出了, 再考虑是不是处理一下.</p><p>slice 操作复杂一点, 就是要提取中间一段范围的数据.<br>可以想象, 范围内的完整分支, 当然是可以直接复用的, 边缘的就只能部分部分复用了.<br>这部分原理比较清晰, 没有什么需要犹豫的地方, 优化的途径也比较容易定位.</p><p>具体不深入了.</p><h3>树的平衡</h3><p>前面也提到了说, 插入或者 concat 的情况, 会增加树的深度,<br>而为了复用树的结构, 尽量是不应该对已有的数据的结构进行破坏的.<br>这两个当然就存在着冲突, 只能权衡了.<br>现在 ternary-tree 实现当中, 考虑的是尽量在局部重建, 远处的分支尽量复用,<br>然后等到发现深度大, 真的需要处理的时候, 就一次性重新初始化, 降低深度.<br>这个策略不算很好, 因为重新初始化树结构的消耗是比较大的, 特别是内存.<br>其次, 真的要我写一个算法, 重建树的结构, 还要部分部分复用, 这难度也大很多了.</p><p>我网上翻的时候, 发现红黑树做了自平衡的事情, 用在数据库的场景里边.<br>老实说我大致看明白了自旋, 但是也没搞明白为什么要区分颜色,<br>同样也有一个问题, 二叉树空间利用率更高, 我用三叉树反而增加复杂度了.<br>当然二叉树的话, 节点容纳的效率也有区别, 可以做一个对比,</p><pre><code class="clj">((1 2 3) (4 5 6) (7 8 9))
(((1 2) (3 4)) ((5 6) (7 8)))</code></pre><p>分支为 3 的时候, 9 个元素, 用到 4 个内部节点进行索引,<br>分支为 2 的时候, 8 个元素, 用到 7 个内部节点进行索引,<br>这样一比, 3 个分支的话, 内部节点的使用效率还是高一点的... 32 分支还更高.</p><p>理想情况下, 以后出于性能优化的需要, 可能也找一找三叉树进行快速自旋的方案,<br>如果能智能地在树的结构改变的时候做一下局部的自旋维持平衡, 效率应该还是不错的.<br>就触发的时机来说, 树不平衡的话, 访问的性能有影响,<br>但是总是触发进行平衡的话, 重建树的结构性能的开销一次也很大.<br>除非真的能找到一个低成本的重建的方案, 不然现在也只能做一定的容忍.</p><h3>跟 Clojure 方案作对比</h3><p>我后面翻了一下 Clojure 的源码, 就 Vector conj 这部分,<br>除了 32 分支那个事情, 如果用 ternary-tree 这个表示的话, 堆积的方式是这样的,</p><pre><code class="clj"> (1 _ _)
(1 2 _)
(1 2 3)
((1 2 3) (4 _ _) _)
((1 2 3) (4 5 _) _)
((1 2 3) (4 5 6) _)
((1 2 3) (4 5 6) (7 _ _))
((1 2 3) (4 5 6) (7 8 _))
((1 2 3) (4 5 6) (7 8 9))
(((1 2 3) (4 5 6) (7 8 9)) ((10 _ _) _ _) _)</code></pre><p>可以看到就是从左边开始堆积, 然后元素的深度始终是维持一致的.<br>这个结构, 查找访问的位置就很容易了, 位操作算一算, 马上就知道, 而且深度稳定的.</p><p>问题也能看出来, Clojure 常说的, Vector 进行 conj 操作最快,<br>conj 就是说在尾部追加元素了, 这个当然快, 尾部就是留着位置的.<br>如果我要在头部加数据就麻烦点了, 说不得还得重新创建一棵树.<br>如果要取出局部的数据的话, 结构复用这个事情就不一定了.<br>翻了一下 subvec 倒是用虚拟的 index 计算的, 性能应该也还快:<br><a href="https://link.segmentfault.com/?enc=rt311uNcHYUmAYDB%2FB9gTQ%3D%3D.vj%2FPvWX43rW9HkGCdGl%2BA%2Brj%2FvecRZzKBeyATySX0McU0rcPHgc32Y%2FCq29f0ARX4d%2FzIsBspdEnzN6hzDGLMJR6P5ASzmLRie3KxGfoqY9oJXjQSqTP7e4%2F5TKka42wT2CqdactwM55gU%2Fe5WNWU2PO4zgYw0nDmPxZWnVnH0tW3OUrnVYpRU%2Bnsf0hUHOu" rel="nofollow">https://github.com/clojure/cl...</a><br>就真实的场景来说, Clojure 真的头部尾部访问, 占了绝大多数了,<br>而且 Vector 的 rest 调用之后直接得到 List, 变成方便从头部读取, 也没毛病,<br>谁有事没事总从后面取啊, 实在不行通过 index 自己去取, 也不是不行.</p><p>真要说好处的话, ternary-tree 这个方案, 一个结构有 List Vector 两者的用法,<br>就是支持头部尾部较为高效添加, 也支持随机访问, 甚至随机操作,<br>同时总体上结构复用的还比较多... 倒是可以避免像学习 Clojure 的时候那么的困惑,<br>毕竟在 Clojure 当中两个数据动不动要转换, 而且默认是自动转换 List 的, 也不方便.<br>其他的, 就是研究和试验的意义比较多了.</p><h3>性能方面...</h3><p>没有对比的测试... 如果有人想要试试的话, 搜是有搜到 Nim 的实现的, 没细看过,<br><a href="https://link.segmentfault.com/?enc=ijz%2FxwwaRYP3SDAvRVLmCg%3D%3D.5e2HqvV%2Bsnrvj%2BsU2usGboFbjhkZIED2nDjwwoZGXvpw07uyGZodQamLj243n9qh" rel="nofollow">https://github.com/PMunch/nim...</a><br>从原理估计, ternary-tree 访问速度肯定是慢的,<br>至于说 append 的性能, 我估计 ternary-tree 不稳定,<br>遇到刚才复用比较多的时候, 创建的新数据成本是很低的, 前面可以看到某些节点深度很小,<br>而遇到大部分情况, 由于 ternary-tree 普遍更深, 也就意味着可能有更多次判断.</p><p>再想想, Clojure 用 List 是有好处的, 如果从头部一个个取,<br>比如用 <code>rest</code> 获取后续的序列, 链表的话每次引用都是一样的.<br>然而用 ternary-tree 的方案, 绝大部分情况都是产生新的引用,<br>如果程序当中使用了 memoization, 根据引用做判断的话, Clojure 代码性能就更高了.<br>ternary-tree 就会产生新的引用, 至少 <code>identical?</code> 的操作是不够了.</p><p>就已有的 ternary-tree 实现, 我用 nimprof 定位看了看,<br>明显性能问题的地方已经被我优化掉了, 稍微深层的一些, 棘手的都没有去深入处理.<br>等到 ternary-tree 后续如果遇到真实场景有明显的问题, 我再着手处理一下.</p>
笔记: 关于使用 calcit-runner 的 GitHub Actions 配置
https://segmentfault.com/a/1190000038454665
2020-12-11T19:04:46+08:00
2020-12-11T19:04:46+08:00
题叶
https://segmentfault.com/u/tiye
0
<p>需求, calcit-runner 目前提供了一个 <code>cr_once</code> 命令用来跑 CI 脚本.<br>原始的 <code>cr</code> 命令存在对 SDL2 和 fswatch 的依赖, 这个场景并不方便.<br>所以 <code>cr_once</code> 这个命令是专门编译提供的, 托管在 <a href="https://link.segmentfault.com/?enc=kjvavghc5MsBrlgquJ7YEg%3D%3D.9SIOv7LPLoBBTFZedYa6drRxWkuF27ughIo23f0YlMZsVM6s7au1q2GhtpWKcP7%2B" rel="nofollow">http://bin.calcit-lang.org/li...</a> .</p><p>其他的项目运行 GitHub Actions 跑测试时, 就需要使用 <code>cr_once</code> 来执行.<br>思路上讲, 就是要把这个命令加载到容器当中, 然后给与可执行权限.<br>然后, 项目本身会有依赖, 就需要下载依赖, 存放到指定的位置.</p><p>目前 calcit 相关项目, 依赖的管理方式只是指定路径加载文件, 所以直接用 <code>git clone</code> 就可以了.</p><p>按说理想的完善方案是 <code>cr</code> 命令本身不要依赖奇怪的东西, 直接能在 CI 运行,<br>然后通过扩展模块的方案, 也就是动态加载动态链接库或者其他模块引入扩展功能, 比如 SDL2,<br>其次模块管理也内置一个命令来做, 自动下载, 自动维护依赖关系....<br>目前 calcit 功能残缺, 这些只好先不管了.</p><p>回到 GitHub Actions, 就需要加载二进制文件到容器, 我尝试了两个方案,</p><h3>Dockerfile 方案(最终未采用)</h3><p>首先, GitHub 是支持在 Actions 里边使用容器的,<br><a href="https://link.segmentfault.com/?enc=uadL5kdvzOtPcgUPYIyoKQ%3D%3D.B5j%2FI7lEOLzEAC4A%2BUuDlB9uxSQBynkrP3MtRHp%2FfMavsy09rwSeSGeJldLYdWth%2F54MUxiS64FtjWudz0IZcUz2IGMOP5%2FYQ7IZHwCXMd0r8fuBVmo9lbjBYLwDrpGPS3bi20eZdwS%2FDkjF%2B7SAhA%3D%3D" rel="nofollow">https://docs.github.com/en/fr...</a><br>按照教程, 我写 Dockerfile 打包一个容器, 这个容器内部包含 <code>cr_once</code>.<br>调试完成一些报错, 我也能在容器里正常运行起来了.</p><p>但是这个方案让我了解到 GitHub Actions 这个环境的一些限制,<br>或者说 docker 容器本身的限制, Dockerfile 里边定义的命令是在容器内部跑的.<br>然后容器运行完成, 对外部没有影响(具体不知道怎么配才能暴露能力).<br>于是我在项目里尝试 git clone, 那个容器内部算是能访问到,<br>但是那个容器内部 git clone, 容器运行结束, 外部就访问不到了.<br>这样也就是说, 最终运行是容器内部的, 我在外部就比较难做各种配置了. 逻辑就不自然.</p><h3>setup 方案</h3><p>然后我去看一眼别的编程语言怎么做的, 比如 Nim 也是二进制执行文件:<br><a href="https://link.segmentfault.com/?enc=tgHQHjkUsYHVbwmFQvEvrA%3D%3D.3DOHPLLFLzlKkzaRV5KqfxVvP%2BC%2BMdKWAXMEcPY9%2BmRgkuh%2BJUpV9sLbHJQCpv86" rel="nofollow">https://github.com/jiro4989/s...</a><br>大致思路是用 Node.js 脚本形式的 Action, 下载安装 Nim, 从而得到一个环境.<br>看了一下 nodejs 的思路, 也是这样的, 直接下载到当前环境当中安装.</p><p>然后我转变思路也改过来, 最终得到这样一个脚本:<br><a href="https://link.segmentfault.com/?enc=2C8NVnObd7V%2FZc441dhhWg%3D%3D.K%2BKjDgjmBN7Ezp7uKp%2BrRD7j6Q%2Bz8WzJTyQ7XOPSxu1SllVV05ymnLbIbOxNAO7cVFq9iAKCllp8WiXEDovTUkIE%2FgZju6zT0MR1tBB30%2FI%3D" rel="nofollow">https://github.com/calcit-lan...</a></p><pre><code class="yaml">jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: wget
run: mkdir ci-bin/ && wget -O ci-bin/cr_once http://repo.calcit-lang.org/binaries/linux/cr_once
- name: "permission"
run: chmod +x ci-bin/cr_once
- name: "prepare modules"
run: mkdir -p ~/.config/calcit/modules/ && cd ~/.config/calcit/modules/ && echo 'no deps'
- name: "test"
run: env=ci ./ci-bin/cr_once</code></pre><p>这个步骤是:</p><ul><li>通过 <code>wget</code> 下载 <code>cr_once</code> 的可执行文件, 对应 Linux 环境的,</li><li>创建 calcit-runner 使用的模块目录, 然后有依赖模块的话, 直接 clone 代码进去,</li><li>运行可执行文件, 按照需要加上一个 <code>env=ci</code> 的环境变量加以区分.</li></ul><p>最终运行也是成功了, 而且比较方便按照需要改写定制. 满足目前的需要.</p><h3>其他</h3><p>我尝试的时候, Docker 使用 Ubuntu 默认系统是什么都没有的,<br>GitHub 提供的容器倒是方便, git, wget 这些命令都在里边了, 就比较省事了.<br>从原理上说, 安装 <code>cr_once</code> 这一步可以抽出来写成一个 Action,<br>那样使用的实在会方便很多, 而且方便后续升级, 只要升版本号就好了.<br>暂时先这样.</p>
Ternary-tree: 不可变数据结构复用的一个尝试
https://segmentfault.com/a/1190000038390819
2020-12-05T22:26:58+08:00
2020-12-05T22:26:58+08:00
题叶
https://segmentfault.com/u/tiye
2
<p>项目地址 <a href="https://link.segmentfault.com/?enc=2am6C5DUDaFM1WQlDdnF0Q%3D%3D.gmpLX7p0AYmC5MmkdPx5gWVxA9UODUtkVSsDuwHwWVKFUhJwZD9nAJAmdJptCXDR" rel="nofollow">https://github.com/calcit-lan...</a></p><p>这里说的不可变数据结构主要是指 Clojure 的 Persistent Data Structure.<br>有个系列文章介绍得比较详细了: <a href="https://link.segmentfault.com/?enc=zmyKBgj1WwensSdsZcOK%2Fg%3D%3D.TwxmOvUXa62vjwVpj%2BG5arYz1%2Fb1VIfycc%2FlTjsDeoJKSHEA6mGiCIXW3i4xsoLl4uOSfmLJR14PXpTE6SzYQJPt7Hzsqn01RtU0I9584yU%3D" rel="nofollow">Understanding Clojure's Persistent Vectors, pt. 1</a><br>Clojure 具体实现考虑到了很多的事情, 源码可以看到一些细节:<br><a href="https://link.segmentfault.com/?enc=XZvr3AJQIknYF%2FAg9Go6sA%3D%3D.DJxmufsagLGUi31vHJuxbeAR3j9DJod6%2B9MTit0Bcht%2FbxXNooKdd%2Bio4uzcZRNMbBC06fxI6ME%2B8%2Fx2w%2F68D9tb735rI7R7DCXslSRfPehzJkaForOXBaZUWiJ9eaki" rel="nofollow">https://github.com/clojure/cl...</a></p><p>我的主要精力是在 TypeScript 跟 ClojureScript 这边, 对 C 了解很少,<br>我介绍的这个项目是用 Nim 写的, Nim 内置了 GC 功能, 用起来比较顺手.</p><p>Clojure 里用的是 32 分支的 B+ 树来存储数据的.<br>数据都在叶子节点上, 每次要填入数据的时候, 都会展开对应的分支.<br>我看源码的时候, 感觉 Clojure 为了性能上的优势, 具体实现是比较简单粗暴的.<br>没有很精细去做每个操作的结构共享, 所以说只有从尾部写入数据才是比较快的.<br>我当时尝试自己去试验的时候, 想着结构复用方便, 我就用了 3 个分支的树形结构.<br>这样也有好处, 就是从前面后面写入数据, 都是一样的, 而且复用这个思路比较清晰.<br>另外就是考虑 trie 这个结构, 实现 HashMap 的话好像 3 个分支比较容易吧.<br>这个性能上优化估计是不如 32 分支的, 不过简单场景还是可以跑跑的.</p><p>这篇文章里, 主要还是关于试验过程当中遇到的有意思的一些发现.</p><p>这个项目当中的 <code>TernaryTreeList</code> 是用 B+ 树实现的, 叶子节点存储数据.<br>内部节点存储分支包含的数据的大小, 这样索引的时候就能快速查询位置了.<br>Clojure 的实现当中索引是用 <code>i >>> 5</code> 这样查找的, 一层层在 32 分支当中定位, 很快.<br><code>TernaryTreeList</code> 索引查找数据就需要不断计算 size 然一层层查找下去了, 慢一些.</p><p><code>TernaryTreeList</code> 初始化的时候, 会尝试大致均匀分布开来, 至少保证树的深度尽量小.<br>当然这样其中可能会残留很多的空穴, 空间的利用率不是最高的.</p><h3>紧凑记法</h3><p>这里为了快速展示 <code>TernaryTreeList</code> 树的结构, 我用一个记法,<br>比如 3 个数据, <code>[1 2 3]</code> 结构是:</p><pre><code> ^
/ | \
1 2 3</code></pre><p>紧凑的记法就是:</p><pre><code>(1 2 3)</code></pre><p>当中间有空穴的时候, 就会空出对应的位置, 比如 <code>[1 3]</code> 的结构:</p><pre><code> ^
/ | \
1 3</code></pre><p>就记为:</p><pre><code>(1 _ 3)</code></pre><p>然后数据更多有多层的数据 <code>[1 4 5 6]</code>:</p><pre><code> ^
/ | \
1 ^
/ | \
4 5 6</code></pre><p>就记成:</p><pre><code>(1 _ (4 5 6))</code></pre><p>这个紧凑的结构就能够展示出更多的信息了.<br>文章后面, 看到括号就要对应的一个树的分支上去, 而且算上空穴以后分支都是 3.</p><h3>数据创建</h3><p>对于长度为 0 到 20 的序列, 创建出来的数据的结构是这样的:</p><pre><code>(_ _ _)
1
(1 _ 2)
(1 2 3)
(1 (2 _ 3) 4)
((1 _ 2) 3 (4 _ 5))
((1 _ 2) (3 _ 4) (5 _ 6))
((1 _ 2) (3 4 5) (6 _ 7))
((1 2 3) (4 _ 5) (6 7 8))
((1 2 3) (4 5 6) (7 8 9))
((1 2 3) (4 (5 _ 6) 7) (8 9 10))
((1 (2 _ 3) 4) (5 6 7) (8 (9 _ 10) 11))
((1 (2 _ 3) 4) (5 (6 _ 7) 8) (9 (10 _ 11) 12))
((1 (2 _ 3) 4) ((5 _ 6) 7 (8 _ 9)) (10 (11 _ 12) 13))
(((1 _ 2) 3 (4 _ 5)) (6 (7 _ 8) 9) ((10 _ 11) 12 (13 _ 14)))
(((1 _ 2) 3 (4 _ 5)) ((6 _ 7) 8 (9 _ 10)) ((11 _ 12) 13 (14 _ 15)))
(((1 _ 2) 3 (4 _ 5)) ((6 _ 7) (8 _ 9) (10 _ 11)) ((12 _ 13) 14 (15 _ 16)))
(((1 _ 2) (3 _ 4) (5 _ 6)) ((7 _ 8) 9 (10 _ 11)) ((12 _ 13) (14 _ 15) (16 _ 17)))
(((1 _ 2) (3 _ 4) (5 _ 6)) ((7 _ 8) (9 _ 10) (11 _ 12)) ((13 _ 14) (15 _ 16) (17 _ 18)))
(((1 _ 2) (3 _ 4) (5 _ 6)) ((7 _ 8) (9 10 11) (12 _ 13)) ((14 _ 15) (16 _ 17) (18 _ 19)))</code></pre><p>因为元素是大致均匀分散开的, 分支都是 3, 所以初始的时候空穴也是大致平均分散开.<br>可以看到这不是最密的一种堆积方式. 所以在内存占用上也不是最经济的.</p><p>理论上说, 基于此方案可以做一下改良, 把元素尽可能往中间靠拢, 而深度依然尽量最小.<br>这样可以得到一个空穴更少的堆积方式, 大致效果像下面这样子:</p><pre><code>(_ _ _)
1
(1 _ 2)
(1 2 3)
((1 _ 2) 3 4)
((1 _ 2) 3 (4 _ 5))
((1 _ 2) (3 4 5) 6)
((1 _ 2) (3 4 5) (6 _ 7))
((1 2 3) (4 5 6) (7 _ 8))
((1 2 3) (4 5 6) (7 8 9))
(((1 _ 2) 3 4) (5 6 7) (8 9 10))
(((1 _ 2) 3 4) (5 6 7) ((8 _ 9) 10 11))
((1 _ 2) ((3 4 5) (6 7 8) (9 10 11)) 12)
((1 _ 2) ((3 4 5) (6 7 8) (9 10 11)) (12 _ 13))
((1 2 3) ((4 5 6) (7 8 9) (10 11 12)) (13 _ 14))
((1 2 3) ((4 5 6) (7 8 9) (10 11 12)) (13 14 15))
(((1 _ 2) 3 4) ((5 6 7) (8 9 10) (11 12 13)) (14 15 16))
(((1 _ 2) 3 4) ((5 6 7) (8 9 10) (11 12 13)) ((14 _ 15) 16 17))
(((1 _ 2) 3 (4 _ 5)) ((6 7 8) (9 10 11) (12 13 14)) ((15 _ 16) 17 18))
(((1 _ 2) 3 (4 _ 5)) ((6 7 8) (9 10 11) (12 13 14)) ((15 _ 16) 17 (18 _ 19)))</code></pre><p>不多这种方案的话我就需要比较准确找到中间分支满足 3 个某个倍数的大小了,<br>这个反复查找数值的操作, 在二进制的计算机当中还是不那么经济的.</p><h3>插入数据</h3><p>然后是插入数据的时候, 如果数据从零开始一直从尾部写入, 经过优化后的效果是这样的:</p><pre><code>(_ _ _)
0
(0 1 _)
(0 1 2)
((0 1 2) 3 _)
((0 1 2) (3 4 _) _)
((0 1 2) (3 4 5) _)
((0 1 2) (3 4 5) 6)
((0 1 2) (3 4 5) (6 7 _))
((0 1 2) (3 4 5) (6 7 8))
(((0 1 2) (3 4 5) (6 7 8)) 9 _)
(((0 1 2) (3 4 5) (6 7 8)) (9 10 _) _)
(((0 1 2) (3 4 5) (6 7 8)) (9 10 11) _)
(((0 1 2) (3 4 5) (6 7 8)) ((9 10 11) 12 _) _)
(((0 1 2) (3 4 5) (6 7 8)) ((9 10 11) (12 13 _) _) _)
(((0 1 2) (3 4 5) (6 7 8)) ((9 10 11) (12 13 14) _) _)
(((0 1 2) (3 4 5) (6 7 8)) ((9 10 11) (12 13 14) 15) _)
(((0 1 2) (3 4 5) (6 7 8)) ((9 10 11) (12 13 14) (15 16 _)) _)
(((0 1 2) (3 4 5) (6 7 8)) ((9 10 11) (12 13 14) (15 16 17)) _)
(((0 1 2) (3 4 5) (6 7 8)) ((9 10 11) (12 13 14) (15 16 17)) 18)
(((0 1 2) (3 4 5) (6 7 8)) ((9 10 11) (12 13 14) (15 16 17)) (18 19 _))</code></pre><p>我们看一下其中对应 <code>(range 10)</code> 的列表, 包含 <code>9 + 1</code> 个数据:</p><pre><code>(((0 1 2) (3 4 5) (6 7 8)) 9 _)</code></pre><p>可以看到它有两个分支(以及一个空穴), 总共 10 个元素.<br>那么访问这其中的 <code>9</code> 就很快, 因为只有一层, 深度非常小, 不需要跟前面的数据一样查找三层.</p><p>在前方写入数据的话, 效果跟上面类似, 但是反过来一下:</p><pre><code>(_ _ _)
0
(_ 1 0)
(2 1 0)
(_ 3 (2 1 0))
(_ (_ 4 3) (2 1 0))
(_ (5 4 3) (2 1 0))
(6 (5 4 3) (2 1 0))
((_ 7 6) (5 4 3) (2 1 0))
((8 7 6) (5 4 3) (2 1 0))
(_ 9 ((8 7 6) (5 4 3) (2 1 0)))
(_ (_ 10 9) ((8 7 6) (5 4 3) (2 1 0)))
(_ (11 10 9) ((8 7 6) (5 4 3) (2 1 0)))
(_ (_ 12 (11 10 9)) ((8 7 6) (5 4 3) (2 1 0)))
(_ (_ (_ 13 12) (11 10 9)) ((8 7 6) (5 4 3) (2 1 0)))
(_ (_ (14 13 12) (11 10 9)) ((8 7 6) (5 4 3) (2 1 0)))
(_ (15 (14 13 12) (11 10 9)) ((8 7 6) (5 4 3) (2 1 0)))
(_ ((_ 16 15) (14 13 12) (11 10 9)) ((8 7 6) (5 4 3) (2 1 0)))
(_ ((17 16 15) (14 13 12) (11 10 9)) ((8 7 6) (5 4 3) (2 1 0)))
(18 ((17 16 15) (14 13 12) (11 10 9)) ((8 7 6) (5 4 3) (2 1 0)))
((_ 19 18) ((17 16 15) (14 13 12) (11 10 9)) ((8 7 6) (5 4 3) (2 1 0)))</code></pre><p>同样来看 <code>(range 10)</code> 对应的数据, 就是上一个例子反过来:</p><pre><code>(_ 9 ((8 7 6) (5 4 3) (2 1 0)))</code></pre><p>这是经过刻意的优化的, 因为在编程当中列表头部尾部增加数据的情况比较多.<br>这样优化之后, 树当中的空穴就会尽量少.</p><p>那么, 如果在中间某个位置插入数据呢, 随机地插入, 用 <code>assocAfter</code>?<br>可以用这样的一个例子(开头我用数字标记了树的深度):</p><pre><code>2 : (0 1 _)
2 : (0 1 2)
3 : ((0 3 _) 1 2)
3 : ((0 3 _) 1 (2 4 _))
4 : (((0 3 _) 1 (2 4 _)) 5 _)
4 : (((0 3 _) (1 6 _) (2 4 _)) 5 _)
4 : (((0 3 _) (1 6 7) (2 4 _)) 5 _)
4 : (((0 3 _) (1 6 7) (2 4 _)) (5 8 _) _)
5 : (((0 3 _) ((1 6 7) 9 _) (2 4 _)) (5 8 _) _)
6 : (((0 3 _) (((1 10 _) 6 7) 9 _) (2 4 _)) (5 8 _) _)
6 : (((0 3 11) (((1 10 _) 6 7) 9 _) (2 4 _)) (5 8 _) _)
6 : (((0 3 11) (((1 10 12) 6 7) 9 _) (2 4 _)) (5 8 _) _)
6 : (((0 3 11) (((1 10 12) 6 7) 9 _) (2 4 _)) (5 8 13) _)
6 : (((0 3 11) (((1 10 12) 6 7) 9 _) (2 4 14)) (5 8 13) _)
7 : (((0 3 11) ((((1 15 _) 10 12) 6 7) 9 _) (2 4 14)) (5 8 13) _)
7 : (((0 3 11) ((((1 15 _) 10 12) 6 7) 9 _) (2 4 14)) ((5 16 _) 8 13) _)
7 : (((0 3 11) ((((1 15 _) 10 12) 6 7) 9 _) (2 (4 17 _) 14)) ((5 16 _) 8 13) _)
7 : (((0 3 11) ((((1 15 _) 10 12) 6 7) 9 _) ((2 18 _) (4 17 _) 14)) ((5 16 _) 8 13) _)
7 : (((0 3 11) ((((1 15 _) 10 12) 6 (7 19 _)) 9 _) ((2 18 _) (4 17 _) 14)) ((5 16 _) 8 13) _)
7 : (((0 3 11) ((((1 15 _) 10 12) 6 (7 19 _)) 9 _) ((2 18 _) (4 17 20) 14)) ((5 16 _) 8 13) _)
; after balanced
4 : (((0 _ 3) (11 1 15) (10 _ 12)) ((6 _ 7) (19 9 2) (18 _ 4)) ((17 _ 20) (14 5 16) (8 _ 13)))</code></pre><p>随着数据增加, 有时候会生成新的分支, 有时候会填充进已有的空穴当中.<br>树的深度在这个过程当中增加是比较快的, 马上就到了 7 层, 这样访问就会变慢了.<br>当然这个操作的过程也有好处的, 分支是尽量会去复用.</p><p>我在代码里提供了一个 <code>forceInplaceBalancing</code> 函数用来压缩深度.<br>上边的例子当中深度从 7 降到 4. 不过空穴这时候不一定就是减少的.</p><p>可以注意到, 随机插入的情况当中, 分支还是会被复用的, 兄弟节点的分支.<br>而被操作到的位置, 已经全部的父节点, 将被重新生成.</p><p>比如插入数据 <code>13</code> 的这个例子, 位置刚好在尾部, 所以开头的分支是被复用的.<br>这样就是 2 个内部节点被插件, 7 个内部节点被复用了,</p><pre><code>6 : (((0 3 11) (((1 10 12) 6 7) 9 _) (2 4 _)) (5 8 _) _)
6 : (((0 3 11) (((1 10 12) 6 7) 9 _) (2 4 _)) (5 8 13) _)</code></pre><p>再看比如 <code>7</code> 被插入的时候, 2 个内部节点被创建, 2 个内部节点被复用.<br>这个效果就比较一般了..</p><pre><code>4 : (((0 3 _) (1 6 _) (2 4 _)) 5 _)
4 : (((0 3 _) (1 6 7) (2 4 _)) 5 _)</code></pre><p>不过, 比如说在一个平衡分配的列表当中任意位置插入数据的话,<br>比如在 <code>(range 17)</code> 当中用 <code>assocAfter</code> 插入 <code>888</code> 这个数据,</p><pre><code>4 : (((0 888 1) (2 _ 3) (4 _ 5)) ((6 _ 7) (8 _ 9) (10 _ 11)) ((12 _ 13) (14 _ 15) (16 _ 17)))
4 : (((0 1 888) (2 _ 3) (4 _ 5)) ((6 _ 7) (8 _ 9) (10 _ 11)) ((12 _ 13) (14 _ 15) (16 _ 17)))
4 : (((0 _ 1) (2 888 3) (4 _ 5)) ((6 _ 7) (8 _ 9) (10 _ 11)) ((12 _ 13) (14 _ 15) (16 _ 17)))
4 : (((0 _ 1) (2 3 888) (4 _ 5)) ((6 _ 7) (8 _ 9) (10 _ 11)) ((12 _ 13) (14 _ 15) (16 _ 17)))
4 : (((0 _ 1) (2 _ 3) (4 888 5)) ((6 _ 7) (8 _ 9) (10 _ 11)) ((12 _ 13) (14 _ 15) (16 _ 17)))
5 : ((((0 _ 1) (2 _ 3) (4 _ 5)) 888 _) ((6 _ 7) (8 _ 9) (10 _ 11)) ((12 _ 13) (14 _ 15) (16 _ 17)))
4 : (((0 _ 1) (2 _ 3) (4 _ 5)) ((6 888 7) (8 _ 9) (10 _ 11)) ((12 _ 13) (14 _ 15) (16 _ 17)))
4 : (((0 _ 1) (2 _ 3) (4 _ 5)) ((6 7 888) (8 _ 9) (10 _ 11)) ((12 _ 13) (14 _ 15) (16 _ 17)))
4 : (((0 _ 1) (2 _ 3) (4 _ 5)) ((6 _ 7) (8 888 9) (10 _ 11)) ((12 _ 13) (14 _ 15) (16 _ 17)))
4 : (((0 _ 1) (2 _ 3) (4 _ 5)) ((6 _ 7) (8 9 888) (10 _ 11)) ((12 _ 13) (14 _ 15) (16 _ 17)))
4 : (((0 _ 1) (2 _ 3) (4 _ 5)) ((6 _ 7) (8 _ 9) (10 888 11)) ((12 _ 13) (14 _ 15) (16 _ 17)))
5 : (((0 _ 1) (2 _ 3) (4 _ 5)) (((6 _ 7) (8 _ 9) (10 _ 11)) 888 _) ((12 _ 13) (14 _ 15) (16 _ 17)))
4 : (((0 _ 1) (2 _ 3) (4 _ 5)) ((6 _ 7) (8 _ 9) (10 _ 11)) ((12 888 13) (14 _ 15) (16 _ 17)))
4 : (((0 _ 1) (2 _ 3) (4 _ 5)) ((6 _ 7) (8 _ 9) (10 _ 11)) ((12 13 888) (14 _ 15) (16 _ 17)))
4 : (((0 _ 1) (2 _ 3) (4 _ 5)) ((6 _ 7) (8 _ 9) (10 _ 11)) ((12 _ 13) (14 888 15) (16 _ 17)))
4 : (((0 _ 1) (2 _ 3) (4 _ 5)) ((6 _ 7) (8 _ 9) (10 _ 11)) ((12 _ 13) (14 15 888) (16 _ 17)))
4 : (((0 _ 1) (2 _ 3) (4 _ 5)) ((6 _ 7) (8 _ 9) (10 _ 11)) ((12 _ 13) (14 _ 15) (16 888 17)))
5 : ((((0 _ 1) (2 _ 3) (4 _ 5)) ((6 _ 7) (8 _ 9) (10 _ 11)) ((12 _ 13) (14 _ 15) (16 _ 17))) 888 _)</code></pre><p>很多情况下, 12~13 个内部节点当中就 2~3 个内部节点被创建出来, 这效果还是可以的.</p><p>所以这中间的缺陷就是树形结构是做不到自平衡的. 大部分时候, 树都是不平衡的.<br>这样效率也就不是最优的. 不过, 考虑到复用节点的需求, 还是不能经常对树进行平衡.</p><h3>拼接和裁剪</h3><p>然后还有一些常用的操作比如 <code>concat</code> 和 <code>slice</code>.<br>如果不在乎平衡不平衡的话, <code>concat</code> 操作是非常简单的, 只是说深度会每次增加:</p><pre><code>(1 (2 _ 3) 4) ; a
(5 (6 _ 7) 8) ; b
(9 (10 _ 11) 12) ; c
((1 (2 _ 3) 4) _ (5 (6 _ 7) 8)) ; a b
(((1 (2 _ 3) 4) _ (5 (6 _ 7) 8)) _ (9 (10 _ 11) 12)) ; a b c</code></pre><p>实际的代码当中, 有时候会触发逻辑强行进行一下平衡.</p><p>至于 slice, 当前的实现当中还是尝试去复用分支, 只是效果上并不很好.<br>比如我临时生成的一个例子, 从这个结构 slice 出不同的片段:</p><pre><code>; original structure
((1 2 3) (4 _ 5) (6 7 8))</code></pre><pre><code> # part of nim code
for i in 0..<8:
for j in i..<9:
echo fmt"{i}-{j} ", d.slice(i, j).formatInline</code></pre><pre><code>0-0 (_ _ _)
0-1 1
0-2 (1 _ 2)
0-3 (1 2 3)
0-4 ((1 2 3) _ 4)
0-5 ((1 2 3) _ (4 _ 5))
0-6 (((1 2 3) _ (4 _ 5)) _ 6)
0-7 (((1 2 3) _ (4 _ 5)) _ (6 _ 7))
0-8 ((1 2 3) (4 _ 5) (6 7 8))
1-1 (_ _ _)
1-2 2
1-3 (2 _ 3)
1-4 ((2 _ 3) _ 4)
1-5 ((2 _ 3) _ (4 _ 5))
1-6 (((2 _ 3) _ (4 _ 5)) _ 6)
1-7 (((2 _ 3) _ (4 _ 5)) _ (6 _ 7))
1-8 (((2 _ 3) _ (4 _ 5)) _ (6 7 8))
2-2 (_ _ _)
2-3 3
2-4 (3 _ 4)
2-5 (3 _ (4 _ 5))
2-6 ((3 _ (4 _ 5)) _ 6)
2-7 ((3 _ (4 _ 5)) _ (6 _ 7))
2-8 ((3 _ (4 _ 5)) _ (6 7 8))
3-3 (_ _ _)
3-4 4
3-5 (4 _ 5)
3-6 ((4 _ 5) _ 6)
3-7 ((4 _ 5) _ (6 _ 7))
3-8 ((4 _ 5) _ (6 7 8))
4-4 (_ _ _)
4-5 5
4-6 (5 _ 6)
4-7 (5 _ (6 _ 7))
4-8 (5 _ (6 7 8))
5-5 (_ _ _)
5-6 6
5-7 (6 _ 7)
5-8 (6 7 8)
6-6 (_ _ _)
6-7 7
6-8 (7 _ 8)
7-7 (_ _ _)
7-8 8</code></pre><p>这里主要还是数据太少, 完成复用的情况就不那么多了.<br>如果数据大的话, 可以想见, 中间的分支是很可能整个被复用的.</p><h3>其他</h3><p>我另外也试了一下 Persistent Map 用 ternary-tree 这个库实现的效果.<br>用的 trie 结构, 然后用的 hash(实际上 Nim 当中用 int 表示), 具体实现就差不多了.<br>结果 Map 的深度是很容易变得非常深的, 因为 hash 的数值就是设计成非常随机的.<br>虽然我可以强行进行平衡, 但是随着数据插入, 很容易就出现非常多不平衡的情况了.<br>对这部分的数据我的经验比较少, 再看了...</p><p>目前在我其他像是当中引用了一下 ternary-tree 这个库, 并做了一些性能优化.<br>现在主要来说还是自己实现了这样的数据结构, 有了更深的理解.</p>
关于动态类型/静态类型语言对于数据的理解的一些差别的随想
https://segmentfault.com/a/1190000038345761
2020-12-01T18:06:51+08:00
2020-12-01T18:06:51+08:00
题叶
https://segmentfault.com/u/tiye
2
<p>不是严谨的思考, 只是梳理一下感受, 最近在动态类型静态类型之间切换有点多, 对照思考.<br>我的经验基本上是 js, ts 和 ClojureScript 上边, 再有点 Nim 的使用经验.<br>然后 Go 跟 Haskell 都只是简单尝试过, 没有深入进去.<br>这些个语言都是自动内存管理的, 所以内存往下深入的我也不讨论了.</p><h3>数据的表示</h3><p>动态类型, 介绍数据结构时候说数组, 说字典, 然后就是基本的操作.<br>基本上只是有一个类型结构, 运行时才能判断具体的类型, 但这个对思维模型来说限制少.<br>比如考虑一个数组, 就是数组, 什么都可以放进去, 数字, 字符串, 对象, 正则, 嵌套数组也行.<br>在实现层面, 我喜欢叫这个 <a href="https://link.segmentfault.com/?enc=G%2FjqzUZ%2FyOha%2BN7o4OP6Ag%3D%3D.lx6ZOmTlwZS9EXhybZE8qS9zcc6F0El8AojikJMKiMQUUyo4L%2Bz%2FWFUv4fZhjOiiSjkQqpSO%2Fhl2mkXVFt6ASpWD5aZAbvFYi%2BX0RLE3lbc%3D" rel="nofollow">uni-type</a>, 因为内部就是一个结构包含很多信息来表示各种类型.<br>编程的时候, 考虑的就是有这么个结构, 有这么个 value, 至于 value 是什么, 动态的, 可以随意放.</p><p>静态类型, 考虑就很多了, 特别是内存布局方面的,<br>Nim 里边就是 object(类似 struct), seq, array, list, 多种的结构.<br>每个结构会对应的到一个内存的布局的方式. 细节具体怎么布局, 我了解不彻底, 但是模糊地对应上.<br>特别是静态类型编码的时候不同结构之间性能的差别, 比较容易显露出来.<br>我用错了结构的时候, 数组的地方用链表, 访问速度的差距马上就出来了.<br>而且结构对类型限制明确, int 数组就不能有 string 了, 这个也是明显的.</p><p>从业务编码的角度来说, 用动态类型来模拟一些业务逻辑, 通常还是比较轻易的.<br>而要用静态类型, 经常需要有一些思考, 用怎么样的类型来表示, 受到什么样的限制.<br>定义了结构, 还有怎么访问怎么遍历的问题.<br>一般说比如 js 性能优化较全面了, 一般场景都不会去考虑不同写法的性能差别了,<br>而静态类型, 表示的方式, 访问的方式, 都比较容易对性能造成影响.<br>静态类型更贴近机器, 或者说用静态类型, 就更加需要熟悉编码器的各种行为, 以便生成高性能代码.</p><p>Clojure 这边还有个较为特殊的例子, 就是 Vector 类型 HashMap 类型内部是用 B+ 树表示的.<br>一般动态类型语言不会去强调这种内部的实现, 但是 Clojure 出于性能原因高调宣传了一下.<br>React 这边, 不是被 immutablejs 搞得沸沸扬扬的, 之前大家也不太注意内部实现什么样子.<br>我用 Nim 时间长了一点发现这中抽象方式在静态类型当中还是蛮普遍的, 数据结构嘛, 链表, B 树...</p><p>而且有个不小的不适应的点, 在动态类型当中, 一个深度嵌套的数据结构, 直接打印就好了,<br>比如一个大的 JSON, 打印的时候可以就递归地格式化掉. prettier 一下就能看.<br>但是静态类型, Nim 当中的例子, 有时候数据就是不知道怎么打印的,<br>因为复杂数据没有一个 <code>$</code>(表示 <code>toString</code>) 函数, 那么就无法打印.<br>这个在 Haskell 当中也是, <code>show</code> 有时候通过 derive 继承过来, 有时候就不行.<br>由于这一点, 静态类型就比较容易隐藏掉一个数据的内部实现了.<br>而动态类型, js Clojure 的数据, 一般就是直接递归打印出来.<br>js 用 class 的话我记得能重新定义掉, 不过 React 这边也不乐意去用 class.<br>而且 js 程序员大多也习惯了 Console 当中层层展开直接查看数据的.<br>我觉得有封装能力对于程序结构来说是更好的, 虽然便利性确实有折扣.</p><h3>通用性</h3><p>动态类型, 不同的程序通过字符串交换数据的时候, 简单粗暴, JSON stringify/parse 就搞定了.<br>JSON 现在是非常通用的结构了. Clojure 那边也是认为这是动态语言巨大的优点.<br>这就是程序基础的结构, 跨程序跨语言都共通的结构, 这才有广泛的通用性.<br>而 nil 在当中扮演的角色也比较重要, 因为外部来源的数据很可能就是缺失内容的, 大量的 nil.</p><p>静态类型在数据接收传递上就存在限制了, 大量的 nil, 判断起来就很麻烦.<br>protobuf 我没用过, 调研的时候看说的, 二进制数据需要类型文件才能解析,<br>而且结构也是严格定义的, 不能错. protobuf 还额外加上限制, 不能删除字段.<br>从编码器的角度看这边是比较好理解的, 为了方便生成代码, 这些外来的数据都应该尽量准确单一,<br>不单一不稳定的话, 就需要额外生成很多的代码来处理特殊情况, 就很麻烦.<br><a href="https://link.segmentfault.com/?enc=RGAkNem3lP3HruOw58V9Ug%3D%3D.W%2BXIpFiOt5G9P%2BvnP6eXTP5AEOcuWnq%2FZhebxVV%2FamNByuHxSzC27zzL8pRDnha3pVt1%2FCo%2BVI%2Fq73vLsIS20g%3D%3D" rel="nofollow">Nim 的例子</a>我试了, 但这个主要是靠 macro 在简单场景能提供很方便的写法,<br>实际用的话, Nim 处理一大堆的字段, 先用 JSON 获取, 再读取, 也很啰嗦.</p><p>代数类型, 算是另一个方向吧. 按照我前面的思路, 它的表达能力偏向于动态类型,<br>但是相比于硬件的限制, 代数类型感觉更多是收到数学定义的限制,<br>比如 <code>data A = B | C</code> 这种或的逻辑, 硬件上表示会啰嗦不少,<br>而代数类型通过内置的语法, 无论创建, 操作, 还是类型检查, 都比单纯静态类型方便.<br>另外有名的例子就是 <code>Maybe string</code> 这样的类型来替代 <code>nil</code> 了.<br>我没什么使用经验, 不好判断这个实用性.</p><h3>数据校验.</h3><p>动态类型, 默认也没什么校验的, 数据不对, 执行的时候报错, 就这样了.<br>需要校验的话都是动态地按照规则去校验, 比如 Clojure 用的 spec, js schema 之类的.<br>动态有个好处是这中间的逻辑可以用编程语言直接去表达出来, 非常灵活.<br>and or 这样的逻辑操作随便用, if switch 也能用, 其他逻辑也是,<br>这就有了比较大的发挥的空间了, 封装成不同的类库.</p><p>静态类型, 从生成代码的角度, 这个代码能运行就是已经经过校验的,<br>或者应该说, 程序能运行, 就是说编码的数据满足了一些编码器要求的限制的.<br><code>seq[int]</code> 当中是 <code>int</code>, 错的话代码编译就会报错. 除了业务, 一般也没什么需要类型校验的.<br>但这个, 相对来说也就是编码器要的那种程度, 对于业务来说是比较死板的.</p><p>另外有个比较花哨的, TypeScript, 实际运行允许 any, 就显得非常特殊了.<br>我倾向于认为 ts 就是个 type checker, 而不是当做一个 compiler.<br>ts 当中也借鉴了代数类型做了一些 union type, product type, 非常强大,<br>只是说跟前两者相比, 是一个非常奇特的思路了.<br>代数类型, Haskell 有个 quickcheck, 能通过类型自动生成随机数据测试, 听说是非常强大的,<br>Clojure Spec 部分山寨了这样的功能, 依赖的是运行时加上 macro 一些能力.<br>静态类型这边要这么做, 就要反射机制了, 这个我不熟悉... 这整块东西感觉水就比较深了.</p><h3>数据方法</h3><p>动态类型, 还是前面 uni-type 的想法, 数据很多时认为是一个类型, 没得区分的.<br>具体运行的话, 就有个专门的字段比如 <code>.kind</code> 标记, 用来判断类型,<br>然后针对不同类型要调用不同的方法的话, 也就能做针对性的操作了.<br>动态语言, 多态靠的对象继承的方式来实现. 绑在类上边有个函数方法.<br>当然, 这个在 React Clojure 生态里边, 这种就比较少了.<br>特别是数据还以 JSON 形式到处分发, 更多的还是用 <code>.kind</code> 标记, 再调用不同函数.</p><p>静态类型这边, 当我使用的 Nim 的时候, 就发现这里多态就很自然有个方案, 因为有类型,</p><pre><code class="nim">proc f(a: int): bool
proc f(a: string): bool</code></pre><p>实际调用 <code>a.f()</code>(Nim 里边这是 <code>f(a)</code> 的缩写)的时候, 根据 <code>a</code> 的类型就有对应的方法.<br>我联想起来 Haskell 定义 class instance 的时候类似也是这样的,<br>OOP 不是我熟悉的领域我也不好说了, 但是这个场景有类型, 解决方案就很自然.<br>而动态类型, 我定义一个 <code>join</code>, 就要用判断不同类型再针对处理了, 也不方便扩展.</p><p>我觉得这一点对我们理解数据比较有影响, 因为数据往往是要被操作的.<br>我是把数据认为是树状的一大个, 然后需要函数动态地去判断, 然后再操作呢?<br>还是说认为数据一块一块相互拼凑, 但是各自有清晰的类型, 然后有各自的方法?<br>还是说跟原始的面向对象那样, 就是一个对象, 接受消息, 发出消息?<br>我不好说这些观念互斥, 但是比如说你设计一个简单的框架, 别人按照框架来快速完成功能,<br>那么在这个框架当中, 别人怎么去理解数据这个事情, 当然还有状态, 这中间的关系和功能?<br>因为我是 ClojureScript 用得比较习惯, 我就更认为是树状的一大串.</p><h3>抽象能力</h3><p>上边梳理的基本还是零碎的想法, 最终还是会回到抽象能力这个事情上来.<br>当然开发当中有不同的场景, 不同的方案并不是说某个就是不对的, 它最少也是有一个适合的场景.<br>但是这种, 对于思考业务的差别还是挺大的, 反应在不同的编程语言当中.</p><p>比如 Clojure 当中, 动态的, 而且强调不可变数据, 就会导致全局存储树形的数据结构,<br>我看待状态的时候, 在这类场景当中就是一棵可能是很深的树, 然后判断.<br>因为是动态的, 也是树, 我就会非常随意地允许一些配置化的数据随处出现,<br>我也不那么在乎这个结构是不是个单元, 需要的时候, 我就写个函数, 动态处理一下就好了.</p><p>而 Nim 当中, 定义类型, 枚举, 结构体, 这些概念就很基础,<br>我定义一个函数, 也要想要函数的操作对象首先是谁, 哪些可变哪些不可变.<br>这个跟面向对象那种思路也相近一些, 他能被怎样操作有哪些方法, 都会想一想.<br>然后如果有深一点的数据, 很明显就是这些小的结构的组合了.</p><p>我用动态类型的话, 比较容易会淡化掉这种结构性的考虑, 因为创建新的结构成本很小,<br>不用说两个相似的数据, 多一个属性少一个属性, 考虑是不是去复用,<br>本来大家就是 uni-type 了, 大家都是一样的, 只是内部包含的信息动态有差异.<br>这个随意性, 对于性能来说是不好的, 但是编码当中便利是存在的.<br>静态类型, 当然, 性能好.<br>此外, 还有个情况, 就是有类型推断参与的场景,<br>由于类型推断依赖一些假设, 这中间的情形就随着变复杂. 就感觉挺混搭的.<br>比如 TypeScript 依据开头创建的字段和元素简历类型了, 但是后边编码可能又被推翻掉.</p><p>再展开点想, 如果只是为了完成业务, 甚至都不限于使用某个编程语言,<br>那么我们描述的业务用的 DSL 当中, 为了也是专门定制的数据,<br>这个时候数据类型啊, 操作方法啊, 这里的数据该往怎么样的形态上演变?</p><p>我没有个清晰的结论. 本能得我会希望业务上我能动态类型随便上,<br>当然我也知道类型推断提供的类型支持, 对于避免程序漏洞非常强力,<br>而且一些基础的数据结构, 能抽象出来封装好称为一个类型, 好处也很多.<br>大概后边要对 Haskell 那样的体系还是要增加一些了解, 反过来帮助理解动态类型.</p><hr><p>补充...</p><p>后续的学习, 再清理了一些疑惑.<br>关于 Nim 的多态, 属于 ad-hoc polymoprhism, 翻译过来即时多态?<br>Haskell 当中基于 type class 有这样类似的 ad-hoc polymorphism 语法.<br>然而 Haskell 当中更多还是 parametric polymorphism,<br>在 Nim 当中泛型是对应这种 parametric polymorphism 的.<br>Haskell 当中出于研究目的, 设计了很多种类型抽象的能力. Nim 简陋很多.<br>同时有这些类型的约束, 和各种类型的抽象方式, 也就需要更多的 trick 来保证灵活性了.</p>
论前端框架组件状态抽象方案, 基于 ClojureScript 的 Respo 为例
https://segmentfault.com/a/1190000038236466
2020-11-20T12:38:01+08:00
2020-11-20T12:38:01+08:00
题叶
https://segmentfault.com/u/tiye
2
<p>Respo 是本文作者基于 ClojureScript 封装的 virtual DOM 微型 MVC 方案.<br>本文使用的工具链基于 Clojure 的, 会有一些阅读方面的不便.</p><h3>背景</h3><p>Backbone 以前的前端方案在文本作者的了解之外, 本文作者主要是 React 方向的经验.<br>在 Backbone 时期, Component 的概念已经比较清晰了.<br>Component 实例当中保存组件的局部状态, 而组件视图根据这个状态来进行同步.<br>到 React 出现, 基本形成了目前大家熟悉的组件化方案.<br>每个组件有局部状态, 视图自动根据状态进行自动更新, 以及专门抽象出全局状态.</p><p>React 之外还有 MVVM 方案, 不过本文作者认为 MVVM 偏向于模板引擎的强化方案.<br>MVVM 后续走向 Svelte 那样的静态分析和代码生成会更自然一些, 而不是运行时的 MVC.</p><h3>React 历史方案</h3><p>React 当中局部状态的概念较为明确, 组件挂载时初始化, 组件卸载时清除.<br>可以明确, 状态是保存在组件实例上的. Source of Truth 在组件当中.<br>与此相区别的方案是组件状态脱离组件, 存储在全局, 跟全局状态类似.</p><p>组件内存储的状态方便组件自身访问和操作, 是大家十分习惯的写法.<br>以往的 <code>this.state</code> 和现在的 <code>useState</code> 可以很容易访问全局状态.<br>而 React 组件中访问全局状态, 需要用到 Context/Redux connect 之类的方案,<br>有使用经验的会知道, 这中间会涉及到不少麻烦, 虽然大部分会被 Redux 封装在类库内部.</p><p>Respo 是基于 ClojureScript 不可变数据实现的一个 MVC 方案.<br>由于函数式编程隔离副作用的一贯的观念, 在组件局部维护组件状态并不是优雅的方案.<br>而且出于热替换考虑, Respo 选择了全局存储组件状态的方案, 以保证状态不丢失. (后文详述)</p><p>本文作者没有对 React, Vue, Angular 等框架内部实现做过详细调研,<br>只是从热替换过程的行为, 推断框架使用的就是普通的组件存储局部状态的方案.<br>如果有疑点, 后续再做讨论.</p><h3>全局状态和热替换</h3><p>前端由 react-hot-loader 率先引入热替换的概念. 此前在 Elm 框架当中也有 Demo 展示.<br>由于 Elm 是基于代数类型函数式编程开发的平台, 早先未必有明确的组件化方案, 暂不讨论.<br>react-hot-loader 可以借助 webpack loader 的一些功能对代码进行编译转化,<br>在 js 代码热替换过程中, 先保存组件状态, 在 js 更新以后替换组件状态,<br>从而达到了组件状态无缝热替换这样的效果, 所以最初非常惊艳.<br>然而, 由于 React 设计上就是在局部存储组件状态, 所以该方案后来逐渐被废弃和替换.</p><p>从 react-hot-loader 的例子当中, 我们得到经验, 代码可以热替换, 可以保存恢复状态.<br>首先对于代码热替换, 在函数式编程语言比如 Elm, ClojureScript 当中, 较为普遍,<br>基于函数式编程的纯函数概念, 纯函数的代码可以通过简单的方式无缝进行替换,<br>譬如界面渲染用到函数 F1, 但是后来 F1 的实现替换为 F2, 那么只要能更新代码,<br>然后, 只要重新调用 F1 计算并渲染界面, 就可以完成程序当中 F1 的替换, 而没有其他影响.</p><p>其次是状态, 状态可以通过 <code>window.__backup_states__ = {...}</code> 方式保存和重新读取.<br>这个并没有门槛, 但是这种方案, 怕的是程序当中有点大量的局部状态, 那么编译工具是难以追踪的.<br>而函数式编程使用的不可变数据特性, 可以大范围规避此类的局部状态,<br>而最终通过一些抽象, 将可变状态放到全局的若干个通过 reference 维护的状态当中.<br>于是上述方案才会有比较强的实用性. 同时, 全局状态也提供更好的可靠性和可调试性.</p><h3>抽象方法</h3><p>Respo 是基于 cljs 独立设计的方案, 所以相对有比较大的自由度,<br>首先, 在 cljs 当中, 以往在 js 里的对象数据, 要分成两类来看待:</p><ul><li>数据. 数据就是数据, 比如 1 就是 1, 它是不能改变的,<br>同理 <code>{:name "XiaoMing", :age 20}</code> 是数据, 也是不可以改变的.<br>但这个例子中, 同一个人年龄会增加呀, 程序需如何表示年龄的增加呢,<br>那么就需要创建一条新的数据, <code>{:name "XiaoMing", :ago 21}</code> 表示新增加的.<br>这是两条数据, 虽然内部实现可以复用 <code>:name</code> 这个部分, 但是它就是两条数据.</li><li>状态. 状态是可以改变的, 或者说指向的位置是可以改变的,<br>比如维护一个状态 A 为<code><Ref {:name "XiaoMing", :age 20}></code>,<br>A 就是一个状态, 是 <code>Ref</code>, 而不是数据, 需要获取数据要用 <code>(deref A)</code> 才能得到.<br>同理, 修改数据就需要 <code>(reset! A {...})</code> 才能完成了.<br>所以 A 就像是一个箱子, 箱子当中的物品是可以改变的, 一箱苹果, 一箱硬盘,<br>你有一个苹果, 那就是一个苹果, 你有一个箱子, 别人在箱子里可能放苹果, 也可能放硬盘.</li></ul><p>基于这样的数据/状态的区分, 我们就可以知道组件状态在 cljs 如何看到了.<br>可以设置一个引用 S, 作为一个 Ref, 内部存储着复杂结构的数据.<br>而程序在很多地方可以引用 S, 但是需要 <code>(deref S)</code> 才能拿到具体的数据.<br>而拿到了具体的数据, 那就是数据了, 在 cljs 里边是不可以更改的.</p><pre><code class="clojure">(defonce S (atom {:user {:name "XiaoMing", :age 20}}))</code></pre><p>便于跟组件的树形结构对应的话, 就会是一个很深的数据结构来表示状态,</p><pre><code class="clojure">(defonce S (atom {
:states {
:comp-a {:data {}}
:comp-b {:data {}}
:comp-c {:data {}
:comp-d {:data {}}
:comp-e {:data {}}
:comp-f {:data {}
:comp-g {:data {}}
:comp-h {:data {}}}}}}))</code></pre><p>定义好以后, 我们还要解决后面的问题,</p><ul><li>某个组件 C 怎样读取到 S 的状态?</li><li>某个组件 C 怎样对 S 内的状态进行修改?</li></ul><p>基于 mobx 或者一些 js 的方案当中, 拿到数据就是获取到引用, 然后直接就能改掉了.<br>对于函数式编程来说, 这是不能做到的一个想法. 或者说也不可取.<br>可以随时改变的数据没有可预测性, 你创建术语命名为 X1, 可以改的话你没法确定 X1 到底是什么.<br>在 cljs 当中如果是 Ref, 那么会知道这是一个状态, 会去监听, 使用的时候会认为是有新的值.<br>但是 cljs 中的数据, 拿到了就认为是不变了的.<br>所以在这样的环境当中, 修改全局状态要借助其他一些方案. 所以上边是两个问题.</p><p>当然基于 js 的使用经验, 或者 lodash 的经验, 我们知道修改一个数据思路很多,<br>借助一个 path 的概念, 通过 <code>[:states :comp-a]</code> 就可以修改 A 组件的数据,<br>同理, 通过 <code>[:states :comp-c :comp-f :comp-h]</code> 可以修掉 H 组件的数据.<br>具体修改涉及 Clojure 的内部函数, 在 js 当中也不难理解, lodash 就有类似函数.</p><p>本文主要讲的是 Respo 当中的方案, 也就是基于这个 cljs 语言的方案.<br>这个方案当中基本上靠组件 props 数据传递的过程来传递数据的,<br>比如组件 A 会拿到 <code>{:data {}}</code> 这个部分, A 的数据就是 <code>{}</code>,<br>而组件 C 拿到的是包含其子组件的整体的数据:</p><pre><code class="clojure">{:data {}
:comp-d {:data {}}
:comp-e {:data {}}
:comp-f {:data {}
:comp-g {:data {}}
:comp-h {:data {}}}}</code></pre><p>尽管 C 实际的数据还是它的 <code>:data</code> 部分的数据, 也还是 <code>{}</code>.<br>不过这样一步步获取, 组件 H 也就能获取它的数据 <code>{}</code> 了.</p><p>在修改数据的阶段, 在原来的 <code>dispatch!</code> 操作的位置, 就可以带上 path 来操作,</p><pre><code class="clojure">(dispatch! :states [[:comp-c :comp-f :comp-h], {:age 21}])</code></pre><p>在处理数据更新的位置, 可以提取出 path 和 newData 在全局状态当中更新,<br>之后, 视图层重新渲染, 组件再通过 props 层层展开, H 就得到新的组件状态数据 <code>{:age 21}</code> 了.</p><p>从思路上说, 这个是非常清晰的. 有了全局状态 S, 就可以很容易处理成热替换需要的效果.</p><h3>使用效果</h3><p>实际操作当中会有一些麻烦, 比如这个 <code>[:comp-c :comp-f :comp-h]</code> 怎么拿到?<br>这在实际当中就只能每个组件传递 props 的时候也一起传递进去了. 这个操作会显得比较繁琐.<br>具体这部分内容, 本文不做详细介绍了, 从原理出发, 办法总有一些, 当然是免不了繁琐.<br>cljs 由于是 Lisp, 所以在思路上就是做抽象, 函数抽象, 语法抽象, 减少代码量.<br>写出来的效果大体就是这样:</p><pre><code class="clojure">(defonce *global-states {:states {:cursor []}})
(defcomp (comp-item [states]
(let [cursor (:cursor states)
state (or (:data states) {:content "something"})]
(div {}
(text (:content state))))))
(defcomp comp-list [states]
(let [cursor (:cursor states)
state (or (:data states) {:name "demo"})]
(div {}
(text (:name "demo"))
(comp-item (>> states "task-1"))
(comp-item (>> states "task-2")))))</code></pre><p>其中传递状态的代码的关键是 <code>>></code> 这个函数,</p><pre><code class="cirru">(defn >> [states k]
(let [cursor, (or (:cursor states) [])]
(assoc (get states k)
:cursor
(conj cursor k))))</code></pre><p>它有两个功能, 对应到 states 的传递, 以及 cursor 的传递(也就是 path).<br>举一个例子, 比如全局拿到的状态的数据是:</p><pre><code class="clojure">{:data {}
:comp-d {:data {}}
:comp-e {:data {}}
:comp-f {:data {}
:comp-g {:data {}}
:comp-h {:data {:h 0}}}}</code></pre><p>我们通过 <code>(>> states :comp-f)</code> 进行一层转换, 获取 F 组件的状态数据,<br>同时 path 做了一次更新, 从原来的没有(对应 <code>[]</code>) 得到了 <code>:comp-f</code>:</p><pre><code>{:data {}
:cursor [:comp-f]
:comp-g {:data {}}
:comp-h {:data {:h 0}}}</code></pre><p>到下一个组件传递参数时, 通过 <code>(>> states :comp-h)</code> 再转化, 取得 H 的状态数据,<br>同时对应给 H 的 cursor 也更新成了 <code>[:comp-f :comp-h]</code>:</p><pre><code class="clojure">{:data {:h 0}
:cursor [:comp-f :comp-h]}</code></pre><p>通过这样的方式, 至少在传递全局状态上不用那么多代码了.<br>同时也达到了一个效果, 对应组件树, 拿到的就是对应自身组件树(包含子组件)的数据.</p><p>当然从 js 用户角度看的话, 这种方式是有着一些缺陷的,<br>首先代码量还是有点多, 初始化状态写法也有点怪, 需要用到 <code>or</code> 手动处理空值,<br>而 React 相比, 这个方案的全局数据, 不会自动清空, 就可能需要手动清理数据.<br>另外, 这个方案对于副作用的管理也不友好, 譬如处理复杂的网络请求状态, 就很麻烦.<br>由于 cljs 的函数式编程性质, 本文作者倾向于认为那些情况还会变的更为复杂, 需要很多代码量.</p><p>就总体来说, 函数式编程相对于 js 这类混合范式的编程语言来说, 并不是更强大,<br>当然 Lisp 设计上的先进性能够让语言非常灵活, 除了函数抽象, macro 抽象也能贡献大量的灵活度,<br>但是在数据这一层来说, 不可变数据是一个限制, 而不是一个能力, 也就意味着手段的减少,<br>减少这个手段意味着数据流更清晰, 代码当中状态更为可控, 但是代码量会因此而增长.<br>那么本文作者认为最终 js 的方式是可以造出更简短精悍的代码的, 这是 Lisp 方案不擅长的.<br>而本文的目的, 限于在 cljs 方案和热替换的良好配合情况下, 提供一种可行的抽象方式.</p>
用 Lilac Parser 代替正则来抓取文本的例子
https://segmentfault.com/a/1190000023443256
2020-07-31T01:16:24+08:00
2020-07-31T01:16:24+08:00
题叶
https://segmentfault.com/u/tiye
2
<p><a href="https://link.segmentfault.com/?enc=0P9yz7urabxDkrWl5j715Q%3D%3D.fmI%2BICyU17wOM4Q4nXCZdewweOtT5VWJxLtHDMao4NcuGpAd8bXVB20mEfiVjTXu" rel="nofollow">lilac-parser</a> 是我用 ClojureScript 实现的一个库, 可以做一些正则的功能.<br>看名字, 这个库设计的时候更多是一个 parser 的思路,<br>从使用来说, 当做一个正则也是比较顺的. 虽然不如正则简短明了.<br>正则的缺点主要是基于字符串形态编写, 需要转义, 规则长了就不好维护了.<br>而 lilac-parser 的方式, 就挺容易进行组合的, 我这边举一些例子</p><p>首先是 <code>is+</code> 这个规则, 进行精确匹配,</p><pre><code class="clojure">(parse-lilac "x" (is+ "x")) ; {:ok? true, :rest nil}
(parse-lilac "xyz" (is+ "xyz")) ; {:ok? true, :rest nil}
(parse-lilac "xy" (is+ "x")) ; {:ok? false}
(parse-lilac "xy" (is+ "x")) ; {:ok? true, :rest ("y")}
(parse-lilac "y" (is+ "x")) ; {:ok? false}</code></pre><p>可以看到, 头部匹配上的表达式, 都返回了 true.<br>后边是否还有其他内容, 需要通过 <code>:rest</code> 字段再去单独判断了.</p><p>当然精确匹配比较简单, 然后是选择匹配,</p><pre><code class="clojure">(parse-lilac "x" (one-of+ "xyz")) ; {:ok? true}
(parse-lilac "y" (one-of+ "xyz")) ; {:ok? true}
(parse-lilac "z" (one-of+ "xyz")) ; {:ok? true}
(parse-lilac "w" (one-of+ "xyz")) ; {:ok? false}
(parse-lilac "xy" (one-of+ "xyz")) ; {:ok? true, :rest ("y")}</code></pre><p>反过来, 可以有排除的规则,</p><pre><code class="clojure">(parse-lilac "x" (other-than+ "abc")) ; {:ok? true, :rest nil}
(parse-lilac "xy" (other-than+ "abc")) ; {:ok? true, :rest ("y")}
(parse-lilac "a" (other-than+ "abc")) ; {:ok? false}</code></pre><p>在此基础上, 增加一些逻辑, 表示判断的规则可以不存在,<br>当然允许不存在的话, 任何时候都可以退回到 true 的结果的,</p><pre><code class="clojure">(parse-lilac "x" (optional+ (is+ "x"))) ; {:ok? true, :rest nil}
(parse-lilac "" (optional+ (is+ "x"))) ; {:ok? true, :rest nil}
(parse-lilac "x" (optional+ (is+ "y"))) ; {:ok? true, :rest("x")}</code></pre><p>也可以设定规则, 判断多个, 也就是大于 1 个(目前不能控制具体个数),</p><pre><code class="clojure">(parse-lilac "x" (many+ (is+ "x")))
(parse-lilac "xx" (many+ (is+ "x")))
(parse-lilac "xxx" (many+ (is+ "x")))
(parse-lilac "xxxy" (many+ (is+ "x")))</code></pre><p>如果允许 0 个的情况, 就不是 many 了, 而是 some 的规则,</p><pre><code class="clojure">(parse-lilac "" (some+ (is+ "x")))
(parse-lilac "x" (some+ (is+ "x")))
(parse-lilac "xx" (some+ (is+ "x")))
(parse-lilac "xxy" (some+ (is+ "x")))
(parse-lilac "y" (some+ (is+ "x")))</code></pre><p>相应的, or 的规则可以写出来,</p><pre><code class="clojure">(parse-lilac "x" (or+ [(is+ "x") (is+ "y")]))
(parse-lilac "y" (or+ [(is+ "x") (is+ "y")]))
(parse-lilac "z" (or+ [(is+ "x") (is+ "y")]))</code></pre><p>而 combine 是用来顺序组合多个规则的,</p><pre><code class="clojure">(parse-lilac "xy" (combine+ [(is+ "x") (is+ "y")])) ; {:ok? true, :rest nil}
(parse-lilac "xyz" (combine+ [(is+ "x") (is+ "y")])) ; {:ok? true, :rest ("z")}
(parse-lilac "xy" (combine+ [(is+ "y") (is+ "x")])) ; {:ok? flase}</code></pre><p>而 interleave 是表示两个规则, 然后相互间隔重复,<br>这种场景很多都是逗号间隔的表达式的处理当中用到,</p><pre><code class="clojure">(parse-lilac "xy" (interleave+ (is+ "x") (is+ "y")))
(parse-lilac "xyx" (interleave+ (is+ "x") (is+ "y")))
(parse-lilac "xyxy" (interleave+ (is+ "x") (is+ "y")))
(parse-lilac "yxy" (interleave+ (is+ "x") (is+ "y")))</code></pre><p>另外当前的代码还提供了几个内置的规则, 用来判断字母, 数字, 中文的情况,</p><pre><code class="clojure">(parse-lilac "a" lilac-alphabet)
(parse-lilac "A" lilac-alphabet)
(parse-lilac "." lilac-alphabet) ; {:ok? false}
(parse-lilac "1" lilac-digit)
(parse-lilac "a" lilac-digit) ; {:ok? false}
(parse-lilac "汉" lilac-chinese-char)
(parse-lilac "E" lilac-chinese-char) ; {:ok? false}
(parse-lilac "," lilac-chinese-char) ; {:ok? false}
(parse-lilac "," lilac-chinese-char) ; {:ok? false}</code></pre><p>具体某些特殊的字符的话, 暂时只能通过 unicode 范围来指定了.</p><pre><code class="clojure">(parse-lilac "a" (unicode-range+ 97 122))
(parse-lilac "z" (unicode-range+ 97 122))
(parse-lilac "A" (unicode-range+ 97 122))</code></pre><p>有了这些规则, 就可以组合来模拟正则的功能了, 比如查找匹配项有多少,</p><pre><code class="clojure">(find-lilac "write cumulo and respo" (or+ [(is+ "cumulo") (is+ "respo")]))
; find 2
(find-lilac "write cumulo and phlox" (or+ [(is+ "cumulo") (is+ "respo")]))
; find 1
(find-lilac "write cumulo and phlox" (or+ [(is+ "cirru") (is+ "respo")]))
; find 0</code></pre><p>或者直接进行字符串替换, 这就跟正则差不多了.</p><pre><code class="clojure">(replace-lilac "cumulo project" (or+ [(is+ "cumulo") (is+ "respo")]) (fn [x] "my"))
; "my project"
(replace-lilac "respo project" (or+ [(is+ "cumulo") (is+ "respo")]) (fn [x] "my"))
; "my project"
(replace-lilac "phlox project" (or+ [(is+ "cumulo") (is+ "respo")]) (fn [x] "my"))
; "phlox project"</code></pre><p>可以看到, 这个写法就是组合出来的, 写起来比正则长, 但是可以定义变量, 做一些抽象.</p><p>简单的例子可能看不出这样做有什么用, 可能就是觉得搞得反而更长了, 而且性能更差.<br>我的项目当中有个简单的 JSON 解析的例子, 这个用正则就搞不定了吧...<br>直接搬运代码如下:</p><pre><code class="clojure">; 判断 true false 两种情况, 返回的是 boolean
(def boolean-parser
(label+ "boolean" (or+ [(is+ "true") (is+ "false")] (fn [x] (if (= x "true") true false)))))
(def space-parser (label+ "space" (some+ (is+ " ") (fn [x] nil))))
; 组合一个包含空白和逗号的解析器, label 只是注释, 可以忽略
(def comma-parser
(label+ "comma" (combine+ [space-parser (is+ ",") space-parser] (fn [x] nil))))
(def digits-parser (many+ (one-of+ "0123456789") (fn [xs] (string/join "" xs))))
; 为了简单, null 和 undefined 直接返回 nil 了
(def nil-parser (label+ "nil" (or+ [(is+ "null") (is+ "undefined")] (fn [x] nil))))
; number 的情况, 需要考虑前面可能有负号, 后面可能有小数点
; 这边偷懒没考虑科学记数法了...
(def number-parser
(label+
"number"
(combine+
; 负号.. 可选的
[(optional+ (is+ "-"))
digits-parser
; 组合出来小数部分, 这也是可选的
(optional+ (combine+ [(is+ ".") digits-parser] (fn [xs] (string/join "" xs))))]
(fn [xs] (js/Number (string/join "" xs))))))
(def string-parser
(label+
"string"
(combine+
; 字符串的解析, 引号开头引号结尾
[(is+ "\"")
; 中间是非引号的字符串, 或者转义符号的情况
(some+ (or+ [(other-than+ "\"\\") (is+ "\\\"") (is+ "\\\\") (is+ "\\n")]))
(is+ "\"")]
(fn [xs] (string/join "" (nth xs 1))))))
(defparser
value-parser+
()
identity
(or+
[number-parser string-parser nil-parser boolean-parser (array-parser+) (object-parser+)]))
(defparser
object-parser+
()
identity
(combine+
[(is+ "{")
(optional+
; 对象就比较复杂了, 主要看 interleave 部分吧, 外边只是花括号的处理
(interleave+
(combine+
[string-parser space-parser (is+ ":") space-parser (value-parser+)]
(fn [xs] [(nth xs 0) (nth xs 4)]))
comma-parser
(fn [xs] (take-nth 2 xs))))
(is+ "}")]
(fn [xs] (into {} (nth xs 1)))))
(defparser
array-parser+
()
(fn [x] (vec (first (nth x 1))))
(combine+
[(is+ "[")
; 数组, 同样是 interleave 的情况
(some+ (interleave+ (value-parser+) comma-parser (fn [xs] (take-nth 2 xs))))
(is+ "]")]))</code></pre><p>可以看到, 通过 lilac-parser 构造规则的当时, 比较容易就生成了一个 JSON Parser.<br>虽然支持的规则比较简单, 而且性能不大理想, 但是比起正则来说, 这个代码可读很多了.<br>相信可以作为一种思路, 用在很多文本处理的场景当中.<br>为了也许可以提供简化一些的版本, 在 JavaScript 直接使用, 代替正则.</p>
ClojureScript core.async 丰富的语义和示例
https://segmentfault.com/a/1190000023312457
2020-07-21T01:11:21+08:00
2020-07-21T01:11:21+08:00
题叶
https://segmentfault.com/u/tiye
4
<p>这篇笔记主要是基于文档展开一下 core.async 在 ClojureScript 当中的基本用法.<br>具体的内容可以看原文章, 已经比较详细了, 很多在 API 文档的 demo 当中.<br>关于基础知识跟 cljs 跟 clj 的区别, 这篇文章就不涉及了.</p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=Y6Nq6cd4vw76x0WH6w4PDA%3D%3D.GafRB63oSYj6vrS2l1iptD0yktfW0ayjUc%2B4naWL1HHvt8Ic5g4soszl2RPC4Y3w" rel="nofollow">https://clojuredocs.org/cloju...</a></li>
<li><a href="https://link.segmentfault.com/?enc=l6kRP%2BlhYWK9E%2BYHvxUBDA%3D%3D.9gIJuVB81%2FCefABqhj6pNkyA%2BwZWa19josJQHPEPvtK0i4mBB%2FTnzn7ChG2Seytm1esqHZ4LS35RVjFHH1zJu4jDu2eWqk3vn1OSSaxFt6A%3D" rel="nofollow">http://clojurescriptmadeeasy....</a></li>
<li><a href="https://link.segmentfault.com/?enc=e1ZBpX608WkeoVNtyh3vZQ%3D%3D.3EXuz3tK9E04Aj9EP%2FAloSsNp%2Bsh7wvWdYH0ARkBY6TbTJrhh0mQiSHXn5%2BwERsMPh7DnTzyqiIVtrcjuh9pQCpnGWyLqNcdvsi6sbqk5ADDg65fmfsHhtYTT0QG5HbX2f6UrfPTNBG%2FwS3MTJ7Vew%3D%3D" rel="nofollow">https://yobriefca.se/blog/201...</a></li>
<li><a href="https://link.segmentfault.com/?enc=eZr3zFkFin9pFfwi7AdNxg%3D%3D.%2FLi9CT7KZ%2F07mCIaEhvz0fxOfLlzzjrsuLLjG58yj%2FJyqyarYFSdj8M6VvVAT3fq1xDbCisr1wT6saewu6GzN7fE%2BD6TvGIFVS61R%2F9ol8SbhF%2FC2uptWZjNNMELonHf" rel="nofollow">https://eli.thegreenplace.net...</a></li>
</ul>
<p>之前用到 core.async , 发现自己中间很多理解缺失了, 趁有时间赶紧看一下.<br>从 API 文档可以看到 core.async 的函数语义是比较丰富的, 几十个函数,<br>我顺着看了一圈, 整理下来大致是几个功能, 大致分成几块.</p>
<ul>
<li>
<p>多对一</p>
<ul>
<li>类似 <code>Promise.all</code>
</li>
<li>多个 channel 的数据进行 map</li>
<li>通过 merge 合并多个 channel 到一个</li>
<li>通过 mix 控制多个 channel 具体合并/解开的情况</li>
</ul>
</li>
<li>
<p>多选一</p>
<ul>
<li>alts! 的多选一, 对应 <code>Promise.race</code>
</li>
<li>alt! 的语法套路</li>
</ul>
</li>
<li>
<p>一拆二/过滤</p>
<ul>
<li>split 直接拆成两个</li>
<li>用 pipeline 搭配 filter 进行过滤</li>
<li>用 transducer 写法进行过滤</li>
</ul>
</li>
<li>
<p>一对多</p>
<ul><li>通过 mult 发送给多个接收端</li></ul>
</li>
</ul>
<p>本来我的触发点是想看 core.async 是都能对应到 Promise 常用的功能,<br>这样看下来, core.async 功能是过于丰富了, 反而有些用不上.<br>由于我对 Go 熟悉度有限, 不好跟 Go Channel 做对比了.</p>
<p>后面逐个看一下示例. 为了方便演示, 我增加了两个辅助函数,</p>
<ul>
<li>
<code>fake-task-chan</code>, 生成一个随机定时任务, 返回一个 channel</li>
<li>
<code>display-all</code>, 打印一个 channel 所有返回的非 nil 数据, nil 表示结束.</li>
</ul>
<h3>多对一</h3>
<h4>类似 <code>Promise.all</code>
</h4>
<pre><code class="clojure">(defn demo-all []
(go
; 首先 tasks 得到向量, 内部函数多个 channel
(let [tasks (->> (range 10)
(map (fn [x] (fake-task-chan (str "rand task " x) 10 x))))]
(println "result"
; loop 函数逐个取 tasks 的值, 从头取, 一次次 recur, 直到取完, 结束
(loop [acc [], xs tasks]
(if (empty? xs)
acc
; <! 表示从 channel 取数据, 在 go block 内阻塞逻辑
(recur (conj acc (<! (first xs)))
(rest xs))))))))</code></pre>
<p>由于任务在 loop 之前已经开始了, 类似 <code>Promise.all</code> 的效果.<br>一个个等待结果, 最终就是全部的值, 耗时就是最长的那个等待的时间.</p>
<pre><code>=>> node target/server.js
rand task 0 will take 0 secs
rand task 1 will take 1 secs
rand task 2 will take 2 secs
rand task 3 will take 3 secs
rand task 4 will take 9 secs
rand task 5 will take 6 secs
rand task 6 will take 7 secs
rand task 7 will take 1 secs
rand task 8 will take 2 secs
rand task 9 will take 9 secs
rand task 0 finished
rand task 1 finished
rand task 7 finished
rand task 2 finished
rand task 8 finished
rand task 3 finished
rand task 5 finished
rand task 6 finished
rand task 4 finished
rand task 9 finished
result [0 1 2 3 4 5 6 7 8 9]</code></pre>
<p>可以看到最终以数组形式返回了每个 channel 返回的数据了.</p>
<h4>多个 channel 的数据进行 map</h4>
<p>我其实不大清楚这个 map 用在什么样的场景, 就是取两个 channel 计算得到新的数字.</p>
<pre><code class="clojure">(defn demo-map []
(let [<c1 (to-chan! (range 10))
<c2 (to-chan! (range 100 120))
<c3 (async/map + [<c1 <c2])]
(display-all <c3)))</code></pre>
<p>所以就是 <code>0 + 100</code> <code>1 + 101</code>... 得到 10 个数据</p>
<pre><code>=>> node target/server.js
100
102
104
106
108
110
112
114
116
118
nil</code></pre>
<p>总体上还是多个 channel 合并成一个了.</p>
<h4>通过 merge 合并多个 channel 到一个</h4>
<p>merge 就是把多个 channel 的数据合并到一个, 字面意义的意思.<br>从得到的新的 channel, 可以获取到原来 channel 的数据.</p>
<pre><code class="clojure">(defn demo-merge []
(let [<c1 (chan),
<c2 (chan),
<c3 (async/merge [<c1 <c2])]
(go (>! <c1 "a") (>! <c2 "b"))
(display-all <c3)))</code></pre>
<p>所以从 c3 就能拿到写到原来的两个 channel 的数据了,</p>
<pre><code>=>> node target/server.js
a
b</code></pre>
<h4>通过 mix 控制多个 channel 具体合并/解开的情况</h4>
<p>mix 跟 merge 很相似, 区别是中间多了一个控制层, 定义成 <code>mix-out</code>,<br>通过 <code>admix</code> <code>unmix</code> 两个函数可以调整 <code>mix-out</code> 上的关系,<br>这个例子当中</p>
<pre><code class="clojure">(defn demo-mix []
(let [<c0 (chan)
<c1 (async/to-chan! (range 40))
<c2 (async/to-chan! (range 100 140))
mix-out (async/mix <c0)]
; mix 过来两个 channel
(async/admix mix-out <c1)
(async/admix mix-out <c2)
(go
; 先取 20 个数据打印
(doseq [x (range 20)] (println "loop1" (<! <c0)))
(println "removing c2")
; 去掉那个数字特别大的 channel
(async/unmix mix-out <c2)
; 再取 20 个数据打印
(doseq [x (range 20)] (println "loop2" (<! <c0))))))</code></pre>
<p>得到结果,</p>
<pre><code>=>> node target/server.js
loop1 0
loop1 100
loop1 1
loop1 101
loop1 2
loop1 102
loop1 3
loop1 103
loop1 104
loop1 4
loop1 105
loop1 5
loop1 106
loop1 6
loop1 107
loop1 108
loop1 109
loop1 110
loop1 7
loop1 8
removing c2
loop2 111
loop2 9
loop2 10
loop2 11
loop2 12
loop2 13
loop2 14
loop2 15
loop2 16
loop2 17
loop2 18
loop2 19
loop2 20
loop2 21
loop2 22
loop2 23
loop2 24
loop2 25
loop2 26
loop2 27</code></pre>
<p>可以看到刚开始的时候, 从返回的 channel 可以获取到两个来源 channel 的数据,<br>进行一次 unmix 之后, 大数的来源不见了, 后面基本上是小的数字.</p>
<p>这个顺序看上去是有一些随机性的, 甚至 unmix 还有一次大数的打印, 后面稳定了.<br>注意 <code>mix-out</code> 只是用于控制, 获取数据在代码里还是要通过 <code>c0</code> 获取的.</p>
<h3>多选一</h3>
<h4>alts! 的多选一, 对应 <code>Promise.race</code>
</h4>
<p>这个比较清晰的</p>
<pre><code class="clojure">(defn demo-alts []
(go
(let [<search (fake-task-chan "searching" 20 "searched x")
<cache (fake-task-chan "looking cache" 15 "cached y")
<wait (fake-task-chan "timeout" 15 nil)
; 数组里边三个可选的 channel
[v ch] (alts! [<cache <search <wait])]
(if (= ch <wait ) (println "final: timeout")
(println "get result:" v)))))</code></pre>
<p>就是随机的时间, 取返回最快的结果. 我多跑几次</p>
<pre><code>=>> node target/server.js
searching will take 3 secs
looking cache will take 14 secs
timeout will take 9 secs
searching finished
get result: searched x
^C
=>> node target/server.js
searching will take 10 secs
looking cache will take 1 secs
timeout will take 4 secs
looking cache finished
get result: cached y
timeout finished
searching finished
^C
=>> node target/server.js
searching will take 19 secs
looking cache will take 4 secs
timeout will take 1 secs
timeout finished
final: timeout
looking cache finished
^C
=>> node target/server.js
searching will take 0 secs
looking cache will take 6 secs
timeout will take 1 secs
searching finished
get result: searched x
timeout finished
looking cache finished
^C</code></pre>
<p>可以看到打印的结果都是最短时间结束的任务对应的返回值.<br>timeout 是这种情况当中比较常用的一个定时器, 控制超时.</p>
<h4>alt! 的语法套路</h4>
<p>alt! 跟 alts! 就是类似了, 主要是语法比较丰富一点,</p>
<pre><code class="clojure">(defn demo-alt-syntax []
(let [<search1 (fake-task-chan "search1" 10 "search1 found x1")
<search2 (fake-task-chan "search2" 10 "search2 found x2")
<log (chan)
<wait (fake-task-chan "timeout" 10 nil)]
(go
(loop []
(let [t (rand-int 10)]
(println "read log waits" t)
(<! (timeout (* 1000 t)))
(println "got log" (<! <log))
(recur))))
(go
(println "result"
(async/alt!
; 匹配单个 channel 的写法
<wait :timeout
; 这个是往 channel 发送消息的写法, 发送也是等待对方读取, 也受时间影响
; 这个两层数组是挺邪乎的写法...
[[<log :message]] :sent-log
; 这个匹配向量包裹的多个 channel, 后面通过 ch 可以区分命中的 channel
[<search1 <search2] ([v ch] (do (println "got" v "from" ch)
:hit-search)))))))</code></pre>
<p>直接多跑几次了, 效果跟上边一个差不多的,</p>
<pre><code>=>> node target/server.js
search1 will take 3 secs
search2 will take 7 secs
timeout will take 3 secs
read log waits 8
search1 finished
timeout finished
got search1 found x1 from #object[cljs.core.async.impl.channels.ManyToManyChannel]
result :hit-search
search2 finished
^C
=>> node target/server.js
search1 will take 2 secs
search2 will take 0 secs
timeout will take 4 secs
read log waits 2
search2 finished
got search2 found x2 from #object[cljs.core.async.impl.channels.ManyToManyChannel]
result :hit-search
search1 finished
timeout finished
^C
=>> node target/server.js
search1 will take 9 secs
search2 will take 0 secs
timeout will take 9 secs
read log waits 6
search2 finished
got search2 found x2 from #object[cljs.core.async.impl.channels.ManyToManyChannel]
result :hit-search
search1 finished
timeout finished
^C
=>> node target/server.js
search1 will take 2 secs
search2 will take 2 secs
timeout will take 2 secs
read log waits 9
search1 finished
search2 finished
timeout finished
got search1 found x1 from #object[cljs.core.async.impl.channels.ManyToManyChannel]
result :hit-search
^C
=>> node target/server.js
search1 will take 6 secs
search2 will take 3 secs
timeout will take 1 secs
read log waits 6
timeout finished
result :timeout
search2 finished</code></pre>
<h3>一拆二/过滤</h3>
<h4>split 直接拆成两个</h4>
<p>看文档好像就是直接这样拆成两个的, 对应 true/false,</p>
<pre><code class="clojure">(defn demo-split []
(let [<c0 (to-chan! (range 20))]
(let [[<c1 <c2] (async/split odd? <c0)]
(go (display-all <c2 "from c2"))
(go (display-all <c1 "from c1")))))</code></pre>
<p>然后得到数据就是分别从不同的 channel 才能得到了, 奇书和偶数,</p>
<pre><code>=>> node target/server.js
from c2 0
from c1 1
from c1 3
from c2 2
from c2 4
from c2 6
from c1 5
from c1 7
from c1 9
from c2 8
from c2 10
from c2 12
from c1 11
from c1 13
from c1 15
from c2 14
from c2 16
from c2 18
from c1 17
from c1 19
from c1 nil
from c2 nil</code></pre>
<h4>用 pipeline 搭配 filter 进行过滤</h4>
<p>这个 pipeline 就是中间插入一个函数, 例子里是 filter, 直接进行过滤.</p>
<pre><code class="clojure">(defn demo-pipeline-filter []
(let [<c1 (to-chan! (range 20)),
<c2 (chan)]
(async/pipeline 1 <c2 (filter even?) <c1)
(display-all <c2)))</code></pre>
<p>效果就是从 c2 取数据时, 只剩下偶数的值了,</p>
<pre><code>=>> node target/server.js
0
2
4
6
8
10
12
14
16
18
nil</code></pre>
<h4>用 transducer 写法进行过滤</h4>
<p>transducer 比较高级一点, 用到高阶函数跟比较复杂的抽象,<br>但是简单的功能写出来, 主要发挥作用的函数那个 <code>(filter even?)</code>,<br>柯理化的用法, 返回函数, 然后被 comp 拿去组合,</p>
<pre><code class="clojure">(defn demo-transduce-filter []
(let [<c1 (to-chan! (range 20)),
<c2 (chan 1 (comp (filter even?)))]
(async/pipe <c1 <c2)
(display-all <c2)))</code></pre>
<p>得到的结果跟上边是一样的, 都是过滤出偶数,</p>
<pre><code>=>> node target/server.js
0
2
4
6
8
10
12
14
16
18
nil</code></pre>
<h3>一对多</h3>
<h4>通过 mult 发送给多个接收端</h4>
<p>就是把一个数据变成多份, 供多个 channel 过来取数据,</p>
<pre><code class="clojure">(defn demo-mult []
(let [<c0 (async/to-chan! (range 10)),
<c1 (chan),
<c2 (chan),
; mult-in 也是一个控制, 而不是一个 channel
mult-in (async/mult <c0)]
(async/tap mult-in <c1)
(async/tap mult-in <c2)
(display-all <c1 "from c1")
(comment "need to take from c2, otherwise c0 is blocked")
(display-all <c2 "from c2")))</code></pre>
<p>可以看到运行以后就是 c1 c2 分别拿到一份一样的数据了,</p>
<pre><code>=>> node target/server.js
from c1 0
from c2 0
from c1 1
from c1 2
from c2 1
from c2 2
from c1 3
from c1 4
from c2 3
from c2 4
from c1 5
from c1 6
from c2 5
from c2 6
from c1 7
from c1 8
from c2 7
from c2 8
from c1 9
from c1 nil
from c2 9
from c2 nil</code></pre>
<p>大概的场景应该就是一个数据发布到多个 channel 去吧.<br>不过这个跟监听还有点不一样, 监听广播时发送者是非阻塞的, 这边是 channel 是阻塞的.</p>
<h3>结尾</h3>
<p>代码后续会同步到 github 去 <a href="https://link.segmentfault.com/?enc=ninJM1g1dkJLSCzbs11WEA%3D%3D.E5CYi4bAB7ZViICKh3FI1ZCYnz4Ktg%2Fw5S4jguAmF68v0L8VbzjxcTe8HxpgB6sy" rel="nofollow">https://github.com/worktools/...</a> .</p>
<p>这边主要还是 API 的用法, 业务场景当中使用 core.async 会复杂一些,<br>比如 debounce 的逻辑用 timeout 搭配 loop 处理就比较顺,<br>具体代码参考, <a href="https://link.segmentfault.com/?enc=Sl6GNqybYfDt1LNHQ0Dnlw%3D%3D.r0oxDOd944wsna8cw37i2%2Bj4D6z2lm0sdmcnriPZFXMk%2F4l24niKyIfd9Y4%2FwZfZ" rel="nofollow">https://zhuanlan.zhihu.com/p/...</a><br>但一般都是会搅尾递归在里边, 进行逻辑控制和状态的传递.<br>网上别人的例子加上业务逻辑还会复杂很多很多...</p>
<p>但总的说, Clojure 提供的 API, 还有抽象能力, 应该是可以用来应对很多场景的.</p>
关于 HCL 颜色的一些笔记(更新: 建议切 HSLuv)
https://segmentfault.com/a/1190000023056925
2020-06-30T16:15:56+08:00
2020-06-30T16:15:56+08:00
题叶
https://segmentfault.com/u/tiye
2
<blockquote><p>根据 SO 上的评论, HCL 是一类算法, 包含多个变种,<br><a href="https://link.segmentfault.com/?enc=cNz%2BBxw4a1l9rrB9jX%2BaEQ%3D%3D.sTqMwWuMcesQvz3GAPk4x%2FrbDcomTb4h317mPwvOi7sZqFo0rjR8MaXP%2BU5xerg7%2FFjtjiZML63Pllqwo%2BrfH3VcnqnEBvsSwtqg24OC2qA%3D" rel="nofollow">https://stackoverflow.com/que...</a><br>为方便代码使用, 建议选取 HSLuv 方案,<br><a href="https://link.segmentfault.com/?enc=C536G3O50eavZTE9IuDJdA%3D%3D.yu3fFHaRxJ1sXzE7IEDDyNu5%2FysdjpUzxmEwqcGsy3sK8nD7eXt6bMgEY4JVlYDx" rel="nofollow">https://www.npmjs.com/package...</a><br>细节对比参见 <a href="https://link.segmentfault.com/?enc=St9Vzy0HlO1DZqm%2Bf23orQ%3D%3D.uBry75kN22QR6ZOCObbKj%2FxVyG5CTHGkx8PYgabQXjzoXfNk%2FT8q9mJ4vks1vmKd" rel="nofollow">https://www.hsluv.org/compari...</a></p><blockquote>HSLuv preserves the lightness and hue components of CIELUV LCh and stretches its chroma so that every color has the same range, defined as a percentage.</blockquote></blockquote><p>关于 HCL 颜色的介绍, 之前在有个文章里看到过:<br><a href="https://link.segmentfault.com/?enc=44gRCSAv2QyIcpT4wIg4Zg%3D%3D.UuOh1Hz991xmr63LRSXNyo1Y9VgSE8U%2BsLHV4J9HygBh0HKolumVjYCra%2FnfOFVD" rel="nofollow">产品配色2.0:使用HCL 色彩空间替代HSL 生成配色- 二三事</a><br>之前关于 HSL 的介绍, 说的是 HSL 比 RGB 更符合人们的视觉,<br><a href="https://link.segmentfault.com/?enc=ug%2BuiyV9IYMZrS9LS5q0Mg%3D%3D.JtO%2BWLLXPkq%2F1IF1W0eGeQzHTQImAt0L2IPrqqIlldJGvrQGwul10M9Nuqb96u%2FU1VjRG6LkVoRjLp%2Bgd9RarNLAO%2BsIaFzpe8AsAJu92B8%3D" rel="nofollow">https://cdc.tencent.com/2011/...</a><br>因为 HSL 的几个数值是色相/饱和度/亮度, 容易增减来调色,<br>但是按照开头的 HCL 文章介绍, HSL 颜色也存在问题,</p><h3>颜色的区别</h3><p>为了清楚展示问题, 我做了一个工具来展示色环 <a href="https://link.segmentfault.com/?enc=5Lnd%2FCjEMxI0KQnSozSgcg%3D%3D.wDANZqMElREFzk2ZCyRzkTd7be1ohbhEi3WjRGhMBprlORW3Sfr5Qc%2Bswp0EZOXt" rel="nofollow">http://tools.mvc-works.org/co...</a><br>其中 HSL 模式, S 取, 100, L 取 61, 效果看起来是这样,<br>可以看到黄色的区域非常亮, 蓝色就显得比较暗.</p><p><img src="/img/bVbIUem" alt="download.png" title="download.png"></p><p>当然这个颜色也是合理的, 蓝色本来就是不耀眼的一种颜色.<br>HSL 跟直接跟 RGB 颜色对应的, 对应到蓝色的像素的亮度.<br>用 HSL 的话, 生成颜色也算比较方便, 改变其中一个参数就好.</p><p>不过, HCL 认为, HSL 的亮度 L 其实有问题,<br>当 L 颜色一致的时候, 颜色的亮度应该是基本一致的,<br>也就是蓝色对应的统一个 L, 在相对的区域, 颜色的亮度应该大体一致,<br>用 HCL 渲染出来 L=61 就是这样子, 当然黄色已经看不到了,</p><p><img src="/img/bVbIUeo" alt="download (1).png" title="download (1).png"></p><p>如果要从 HCL 当中显示黄色, 我调了一下, L 需要调整到 91,<br>这个时候蓝色的区域也被提升得很亮, 是接近白色的浅蓝色了,</p><p><img src="/img/bVbIUgo" alt="download (2).png" title="download (2).png"></p><p>如果是 HSL 当中 L=91, 看一下效果, 这些颜色已经都接近白色了,</p><p><img src="/img/bVbIUgt" alt="download (3).png" title="download (3).png"></p><p>这也包含了 HCL 的一个区别, 就是 L=100 对应的并不是白色,<br>在 HCL 当中 C=0, L=100 才能调出白色.</p><h3>代码计算</h3><p>当前 demo 实现参考 <a href="https://link.segmentfault.com/?enc=JYFiYOFwQ1g23A%2B0%2FkbvAg%3D%3D.Ytoa5Qu82kpsJx%2BDJGdQ8RMtqYAbN0HudFPjT1L97zJ4proonX4gVwTIWRLOJilL" rel="nofollow">https://github.com/worktools/...</a> .<br>颜色基色使用的是 <a href="https://link.segmentfault.com/?enc=UZlnDt4XnNk2SYHO8ZAxjQ%3D%3D.EfcpAb0XW56bllU7X5l9msHd%2FK32DqEY%2BwYQAAovYPCBMmjo8Mh9mvi608%2FpGzwq" rel="nofollow">https://www.npmjs.com/package...</a></p><pre><code class="js">d3.hcl(h, c, l_[, opacity])</code></pre><p>可以比较快的创建一个 HSL 颜色, 然后再转换到其他的颜色格式,</p><pre><code class="js">color.formatHex()
color.formatRgb()
color.formatHsl()</code></pre><p>另一个模块还有 <a href="https://link.segmentfault.com/?enc=xkHPb8hWFWwzzhlsdD5wgg%3D%3D.lwazJWCMK6ih5x6R6qYv78J%2BujC46SCRPs7LOSzSPXykHfwhhQI3bF52dWXV3ZC1" rel="nofollow">chroma-js</a>, 也可以进行转换, 但是我这边没有深入用.<br>初步感觉不如 d3-color 方便用.</p><p>D3 也是使用 HCL 比较多的场景, 图表自动生成的颜色用 HCL 更好.<br>我这边遇到的场景是生成的一个折线图的颜色, 发现蓝紫色亮度低很难看清.<br>于是我想到是 HCL 颜色指出的那个问题, 所以把相关的 API 扒出来试了一遍.<br>从效果看, 很明显 HCL 亮度控制得比较好, 只是在色彩明艳程度有区别.<br>因为亮黄色跟深蓝色亮度明显不一致, 所以基本不会出现在同一个图上了,<br>这样的话, 颜色的对比没有原来鲜艳强烈了..</p><h3>原理</h3><p>扒了一些资料, 大部分细节没有弄明白, 具体的换算也没弄清楚.<br>大致搜集了一些资料, 需要的话详查...</p><p><a href="https://link.segmentfault.com/?enc=sdZt7tMWNYzUJfrKuwjWhw%3D%3D.lnWaWOWmTay6xYaNFkibq4S%2BPN13y7CMQV4mxlVhdioxOybwKieadCu1zvGId1QTxSc3fvR7vvtmbJ3Y8c9cqkBUv5d%2BzbxaPPe5g85%2F8cI%3D" rel="nofollow">https://en.wikipedia.org/wiki...</a></p><p><a href="https://link.segmentfault.com/?enc=iMGRd0PXG4FByAICzNaZ%2FA%3D%3D.i%2FkioUwlA02nAZGhm0AyY3WtgmaVotCzreLCViAfOhQ%3D" rel="nofollow">http://hclwizard.org/why-hcl/</a></p><p>原始论文 <a href="https://link.segmentfault.com/?enc=HnswqLkyv4J0iUXKyHzONQ%3D%3D.4EZIYFOg6Yy1QM%2F7DMndztm%2BNp1acLJRj9BGmsXtLACtmEWPUNuqBw7W2f8OFQQLNP6pcK1cufE4ALFB1NT8EPd3Nv9sKyWSsncpe8Aa6M0%3D" rel="nofollow">https://pdfs.semanticscholar....</a></p><p>一个系统介绍了颜色的博客 <a href="https://link.segmentfault.com/?enc=TZxLjK0GOWdbU6Ufm1cvfw%3D%3D.ZxIcXl%2FmoEo3ZKtYYr0BQswjT4AYl%2FLV9DsL4Fo14SZHJfdk1h%2BcgW%2BSnoSFKgkk" rel="nofollow">https://www.jianshu.com/u/9d9...</a></p><p><img src="/img/remote/1460000041882392" alt="" title=""></p>
对于简聊 React 的一些回忆和反思(初稿)
https://segmentfault.com/a/1190000022276512
2020-04-06T13:14:10+08:00
2020-04-06T13:14:10+08:00
题叶
https://segmentfault.com/u/tiye
2
<p>看到钉钉的功能越来越多了, 前段时间突然想起来以前简聊的事情来.<br>当前公司跟钉钉的一些风声, 具体也不清楚, 到很多年后才听到了收购的事情.<br>Slack 具体的玩法我并不清楚, 但是钉钉当前延伸出来的功能给我一些感触,<br>当年简聊在功能的扩展来说缺了太多围绕聊天的扩展创新, 玩法也不温不火,<br>当然如果当时有条件一步一步有条理地往外扩展, 也说不准后面是不是会有机会.<br>我当时也基本上一心在处理 React 的事情, 极少对总体的产品形态提出想法.</p>
<p>单纯从前端代码来说, 我们当时也算很早尝试 React 并且稳定下来了.<br>经过这些年, React 改变也不少, class 组件到今天用的人比较少了,<br>而且随着我到不同的公司遇到不同的场景, 代码的侧重也改变了不少.<br>再当时来说有些东西看着是迷雾, 还要不断前行探索才能知晓,<br>现在回头看的话, 是比起从前清晰了很多, 那些说是弯路说是妥协的东西.</p>
<h3>CoffeeScript, ClojureScript, TypeScript</h3>
<p>CoffeeScript 在当年算是 alt js 当中最成熟的一门语言.<br>就功能来说, 它仅仅是让人更容易些 js 的语法糖而已.<br>跟 ClojureScript 相比, CoffeeScript 没有原生的不可变数据支持,<br>跟 TypeScript 相比, CoffeeScript 没有类型系统加持.<br>上面说的这两个 ReasonML 都有, 可惜到现在 ReasonML 给人感觉都还不够成熟.</p>
<p>就功能来说, CoffeeScript 确实是残缺的,<br>当时虽然简历了代码规范, 但是很大程度还是依靠人自觉把格式统一起来.<br>而现在, 无论 ClojureScript 还是 TypeScript 都是通过自动化格式工具写的,<br>Prettier 的使用基本追平了的 CoffeeScript 带来的编写效率的提升,<br>而且就团队代码习惯来说, Prettier 比起 CoffeeScript 当然是好多了.<br>不可变数据的残缺, 导致了一系列的奇怪问题, 后面再说.<br>没有类型系统确实也不如 TypeScript 适合多人团队.</p>
<p>不过当年 ClojureScript 跟 TypeScript 比较还是很少出现的,<br>我印象里公司倒是有那个人在研究 TypeScript 这些了,<br>而 ClojureScript 在当时工具链也还有各种问题, 即便今天也有一些.<br>不过即便是今天, 由于招人的原因还有工具链特殊, 我还是不觉得能在多人开发当中用起来.<br>ClojureScript 更多的, 还是让我站在 FP 语言的角度审视 js 生态缺失的功能.</p>
<h3>Immutable 和 Immer</h3>
<p>前面说的 CoffeeScript 或者 js 没有原生不可变数据的问题,<br>按照 React 的设计, 不可避免需要用到不可变数据, 否则优化起来会很绕.<br>现在来说, 有了 Hooks API, <code>useState</code> 把数据一份份拆开了,<br>这样每个数据更新的时候, 算是通过替换达到了不可变数据的效果,<br>但是也存在过于分散且触发多次更新的问题. 还是有时候需要不可变的对象来处理.</p>
<p>当时用 Immutable.js 的问题主要就是学习成本,<br>我最开始从了解这个东西到熟悉了敢用在项目当中也花了不短的时间.<br>除了写法比较绕以外, 对于前端来说这也是比较陌生的一种用法.<br>后来即便我用了, 我印象里同事还是有不少的抱怨的, 比较维护着累.<br>这东西并不是不能掌握, 但是当在 js 当中写起来就是挺烦的.<br>而且对于第三方代码来说, 只有 JSON 对象时它们接受的数据格式.<br>那么 Immutable 数据就要在我们代码当中转来转去, 维护就很累.</p>
<p>我后面用 ClojureScript 就不是这样的. Clojure 默认用不可变数据.<br>这样我在开发当中几乎无时无刻不是直接用不可变数据在编写逻辑,<br>就是说没有多少需要转换的场景, 脑子里对数据的理解统一而且清晰.<br>Lodash 当中 <code>updateIn</code> <code>setIn</code> 就是 Clojure 中很平常的用法.<br>这些 API 在 Immutable 当中更是比比皆是, 只是说 Clojure 做到了语言核心中.</p>
<p>现在我们用的是 Immer, 语法比较贴近 JavaScript 原生的操作.<br>就推广使用来说, 得益于语法简单, 而且加上 Hooks API 封装, 容易多了.<br>不过从 review 的结果来看, 还是偶尔会出现遗漏.<br>Immer 毕竟是用了 freeze 强行设定数据不可变, 但看着输出跟 js 又没区别.<br>就没法保证在场景当中串来串去不会漏掉. 当然也还好比较少.</p>
<p>不可变数据这个事情, 四五年了, 萦绕在 React 社区还是没有消散掉,<br>我觉得这个作为长期的一个方向, 也不会很容易就有完美的结果了.<br>如果说期待, 我希望 ReasonML 到时候能把这事情统一掉.<br>因为 js 总是要兼容老代码的, 不管怎样引入不可变数据, 心理负担总会在.</p>
<h3>Less 和 emotion</h3>
<p>简聊用的 CSS 预编译方案是 LESS, 当时 teambition 统一的习惯.<br>总的来说我对这种层层重置的 CSS 规则也没留下多好的印象.<br>当初 <a href="https://link.segmentfault.com/?enc=44JqHMaZEEV3pRStmeukIw%3D%3D.H3smE6%2BR65%2Fle%2FWWF%2FmE%2BXrwcWyaSlRO0yqiEB3R5s%2F7rmrfIofLW%2Flh4GBu2cOXdkQOSryZ5%2FFlsChfWY6X9Q%3D%3D" rel="nofollow">Vjeux</a> 那个 CSS in JS 的演讲发布的时候我是深有感触的,<br>用了两三年的预编译, 大部分的局限性基本上也碰到了,<br>我是很期待直接用上 CSS in JS 的方案, 做细粒度的包含代码逻辑的控制.</p>
<p>就当时来说, 我觉得方案是不够成熟的, styled-components 也觉得走得太怪.<br>在我自己 cljs 的方案里边, 我后来用的是 inline styles, 局部场景够用,<br>而 inline styles 最主要的问题是浏览器前缀和伪类的控制, 没法用在产品当中.</p>
<p>后来在朋友当中了解到 emotion 的方案相对成熟, 我就尝试了一下,<br>我最后还是没有用 styled-components 那样的写法,<br>对于 emotion 我只是当做定义 className 写法来用, 倒不是新版本官网推荐的用法.<br>如果参考官网, 反而我们的写法显得并不规范, 也不好配合官方 Babel 的优化.<br>只能说堪堪到了一个解决掉我认为痛点问题的状态,</p>
<pre><code class="jsx">let styleA = css`
color: red;
`
let styleB = css`
background-color: blue;
`
<div className={cx(styleA, styleB)}>red</div></code></pre>
<p>但是这也却是帮我规避了很多的问题, 也因为是变量所以很好用代码进行控制.</p>
<p>问题主要在团队当中, 以往 LESS 的写法是大家都熟悉的, 有一套习惯.<br>加上 emotion 官网推荐的是 styled-components, 就造成了不统一.<br>而我的话没有多少动力去推动这方面的改变了, 这些涉及到不止一个改变的地方.</p>
<h3>Mixin 和 Hooks</h3>
<p>简聊遗留的代码中组件复用的代码使用的是 Mixin.<br>实际上能抽取出来的复用的方法并不多, 而且维护性并不算好.<br>在 Class 上动态注入的方法, 维护当中基本靠约定, 不容易排查.<br>Mixin 的用法后来对着 createClass 被废弃也跟着废弃了, 改为单继承.</p>
<p>中间用高阶组件进行逻辑抽象的事情大家都经历过了,<br>现在的结果也都知道了, 随着 Hooks 出来, HOC 基本没人说了.<br>当初 HOC 我们也试着写过, 我还是参考着同事的代码写的,<br>但我真心觉得 decorator 的代码很容易写出问题来, 而且我很容易漏掉各种东西.<br>就逻辑复用来说, HOC 主要是类库作者用, 业务开发几乎不会去乱搞.</p>
<p>现在有了 Hooks 再去审视当初的 Mixin, 就觉得很原始.<br>Mixin 几乎只是粗暴地讲方法一个个剥离到单个的文件, 事情只是做了一半.<br>饭馆 Hooks 封装的逻辑较为完整, 复用的场景也多, 也认为较为可靠.<br>就我们现在来说已经大量使用 Hooks 用在业务抽象当中了,<br>所以 Hooks 是实实在在对于产品业务有很多帮助的, 而不限于类库开发者.<br>回顾简聊的代码, Mixin 也算抽了二十多个吧, 这数量是不如 Hooks 的.</p>
<h3>Props 和类型</h3>
<p>简聊代码当中主要用 PropTypes 进行的组件参数的管理.<br>后来这些东西渐渐废弃掉了, 有点不知不觉的.<br>现在应该大部分人都使用的 TypeScript 声明组件的类型了吧,<br>有了类型的辅助, 即便没用动态的参数校验, 也很难出现那方面的错误.<br>就这一点来说 TypeScript 为前端带来了不小的改变.</p>
<h3>actions-recorder 和 respo-reel</h3>
<p><a href="https://segmentfault.com/a/1190000003879041">action-recorder</a> 我在前面有<a href="https://segmentfault.com/a/1190000003863338">留文章</a>详细讲了, 是我改出来的名字.<br>原型的话, 是 Elm 的 hot code swapping 吧, 还有 Redux 的调试工具.<br>当时具体时间我记不清了, Elm 那些 Demo 大家应该都看过, Redux 也有些风声,<br>加上当时社区有不少人在追这方面的工具链, 我也就自己做了一些尝试.<br>actions-recorder 最初因为实现的问题, 在内部试用性能还挺差, 不过还好马上修复了.<br>actions-recorder 是为了简聊定制的类库, 基于 Immutable.js , 算是耦合了.<br>后来 Redux DevTools 大概做了更完整的版本, 我不清楚用的人有多少.</p>
<p>actions-recorder 的核心原理是所有的 actions 都存储下来,<br>后续可以通过切换 actions 和 updater 重新计算, 回放整个应用的状态.<br>这同时也推导出一个要求, 就是 didMount 等等操作时, 不可以发出 actions.<br>这个要求当时对我造成了不小的影响, 毕竟在 didMount 时 dispatch actions 挺常见的.<br>为了迎合这一套方案, 我花了不小的心思对相关逻辑进行了不小的改造.<br>特别大的一块就是切换也没请求数据的行为, 都跟 didMount 脱离了.</p>
<p>actions-recorder 这套重构当时也是也没有太多的难点, 逐渐就完成了.<br>从效果来说, 我认为还是不错的, 请求从 didMount 分离, 就是从 render 分离,<br>数据在路由切换前就开始请求, 时间上提早了一些, 界面上也避免了多个分离的 loading.<br>但是从坏的一面来说, 这种约束对于后续的开发维护来说是不友好的.<br>我印象当中我同事后面补代码的时候比较容易会破坏这个规则,<br>而且有了这些限制, 设计逻辑也累了许多. 谁不喜欢在 didMount 直接请求数据啊.</p>
<p>站在 actions-recorder 的角度, 回放 actions 好处比较明显,<br>当时有几个不好排查的 bug, 在 actions-recorder 的工具当中很容易定位,<br>因为每个 action 前后的数据状态都在, diff 可以直接看, 很明确.<br>但是一旦 didMount 时会发送 actions, 就导致回溯时总会有重复的 actions 发出,<br>这些 actions 也不好追踪, 那么调试的方便就被破坏掉了.<br>这些好处对我而言有用, 但是对团队来说却未必真的好, 现在看看确实问题太多.</p>
<p>后面我用 ClojureScript 写的代码当中, 对于 Respo 的小应用, 时间起来比较轻松.<br>由于 Respo 我在设计实现时基本上杜绝了 didMount 发 actions 的可能性,<br>所以天然就的是满足前面的约束, 而我用 respo-reel 类库很容易做到这一点.<br>另外 respo-reel 跟 cljs 纯函数的特性配合, 更新 updater 的效果也比较好.<br>某种程度说这个就是另一套独立的技术路线了. </p>
<p>就目前公司中而言, 我完全放弃了 actions-recorder 的方案(也没有用 Redux 那套).<br>当存在大量的伴随 didMount 进行局部的 actions dispatching 的时候,<br>actions-recorder 的方案就显得没有多少意义, 特别是还跟 Hooks 相关联.<br>actions-recorder 也要求数据尽量存储在全局, 这一点在当前的场景也不合适.<br>特别是个人开发大量的子页面, 各自有各自的数据, 跟简聊的场景就很不一样了.<br>虽然我最初有考虑复用部分 actions-recorder 的思路过来, 但仔细想总没有多少好处,<br>只能说是场景不同, 加上那些前车之鉴吧.</p>
<p>脱离 Elm 的全局状态的方案的话, actions 对我来说就没有太大的意义了.<br>如果是 ReasonML 那样通过类型能定义 action 方便做 switch 的话倒还好,<br>在 TypeScript 中用 Action 效果并不多, 而且还多一层抽象.<br>反而不如直接用方法去操作那少有的全局状态来得好维护了.</p>
<h3>全局数据的使用</h3>
<p>简聊的场景比较特殊一些, group 和 messages 是全局的数据, user 也是.<br>一方面 WebSockets 会推送新数据过来, 这些默认就是全局处理的, 而不是局部,<br>另一方面聊天应用就是围绕消息和用户展开的, 消息全局存储也有意义.</p>
<p>目前公司的场景是各种页面表单图表基本上是各自从服务器请求数据,<br>虽然形态上是单页面应用, 实际上子页面之间或者全局复用的数据可以说少得可怜.<br>这样大量的状态也就是在各个组件自己去做维护了, 并且状态也非常多.<br>大相径庭的场景. 当然状态很多对于调试来说也不大友好. 可现状就是这样.</p>
<h3>router-view 和 ruled-router</h3>
<p>由于前面说的 actions-recorder 的限制, 就要去所有全局状态都要在 store 统一维护,<br>比较重要的一块状态, 就是路由状态, 当时我也走到了 redux-router 的前面.<br>我设计了 <a href="https://segmentfault.com/a/1190000003953353">router-view</a> 模块, 改变了路由在 React 中的角色, 使用全局数据控制.<br>这是个定制化的方案, 也没必要展开说了.<br>另外这个 router-view 跟后来 Vue 说的 router-view 还不一样, 具体也没看.</p>
<p>不过有一点是后面影响我比较深的, 就是我认为 react-router 界面和路由耦合很有问题,<br>router-view 是定义 JSON 的路由规则对路径进行解析, 然后交给页面渲染,<br>这根 react-router 通过 Context 层层处理的方式完全不一样.<br>router-view 应该说更接近于后端解析和处理路由的方式, 先解析, 再当做数据处理.<br>解析完成以后, 处理数据对于 React 来说是非常清晰也很好排查问题的.<br>这个在 react-router 那样的耦合严重的方案当中就显得分散而且琐碎了.</p>
<p>这个思路导致后面为了解决公司面对的嵌套层级过深难以维护的问题时, 我直接选择了改换路由.<br><a href="https://segmentfault.com/a/1190000019143914">ruled-router</a>是从 router-view 延伸的对于嵌套路由做更深层适配的方案.<br>后面发现配合 TypeScript 的类型还能做很多的定制, 就继续深入做了<a href="https://segmentfault.com/a/1190000020845238">代码生成</a>.<br>关于细节, 前面发文章讲过, 不再展开了.</p>
<p>但这边也还有个问题, 就是定制过深之后, 整个方案完全是自成体系了,<br>这个对于招聘进来的新人来说, 首先有一个学习门槛的问题,<br>再者虽然跟 TypeScript 做了配合, 但是有些地方并不很直观, 有一些思考的成本.<br>这个毕竟不是社区大量的投入打磨的方案, 也没有大量的文档教程支持, 导致局面有点奇怪.<br>如果是 Facebook 或者阿里, 搞出一套方案, 是有能力往整个社区推广的,<br>而我们作为小厂, 自己用就是自己的培训成本, 完全需要自己操心了.</p>
<h3>请求的抽象</h3>
<p>请求这个坑也算是针对简聊的场景专门设计的方案, 来源是 Netflix 的 data graph.<br>Netflix 具体细节我记不清了, 当时有个演讲专门说的.<br>思路跟 GraphQL 类似, 也是方案自动处理请求的依赖关系, 前端把相关依赖整合起来.<br>当年 GraphQL 非常不完善, 而且没有跟对简聊做数据推送的场景,<br>经过这么多年了, 社区很多公司跟进了 GraphQL 的方案, 做了很多探索.<br>我所在的公司后端是 Go, 而且业务较重, 我这边也就失去了跟进 GraphQL 的可能.<br>当前使用的方案较为原始, 但是也尽量在用生成 API 代码的方案配合 Hooks 做一些抽象.</p>
<p>简聊当时的数据依赖的抽象很稚嫩的, 也是因为场景简单, 所以走下去了. 可维护性也一般般.<br>从后面 GraphQL 的路线进行对比, 很明显, 数据抽象这个事情需要后端帮忙.<br>后端微服务之间很容易通过多个请求进行数据聚合, 性能优化办法也很多.<br>这些等到前端再想办法做抽象的, 已经算太晚了, 限制也很多, 效果也很弱.</p>
<p>但总体说我认为数据自动处理依赖在未来也还会是会被经常触及和深入挖掘的场景.<br>GraphQL 不是最终方案, 至少我们的场景就不够用. 当然更多还是靠后端了.</p>
<h3>Form 场景</h3>
<p>简聊因为是聊天室, 当时对于 Form 的需求很少, 或者说只有特定的一块,<br>当时的需求是后端想要通过 JSON 配置表单, 前端渲染就好了.<br>所以当时给出的也只是 React 根据 JSON 做简单的 Form 响应的方案.</p>
<p>后来到了别的公司, 接触到后台管理大量使用 Form 的场景, 我发现自己忽视了这整理一大块.<br>我这边后来开始了 <a href="https://link.segmentfault.com/?enc=Nq%2BBY031etRbLmu6KGVZTg%3D%3D.7PqCzQtRn6%2FNveHDejPUj8mHXYXM7SVyR%2FTflKFZuuiQz6nlTjH9s6%2BiLtGQzMgS" rel="nofollow">meson-form</a> 模块继续针对业务场景定制 Form 的方案.<br>这就跟简聊当时面对的简单的场景很不一样了, 而且 Form 后面还会有各种重逻辑的变种.</p>
<h3>匆匆忙忙和代码重构</h3>
<p>简聊那段时间让我形成了一个观念, 就是架构调整很少会被分配出时间来做,<br>所以基本上就经常要我们自己私下调研方案还有见缝插针一点点去做了.<br>在执行层面上就基本上这样了, 而且要一点点拆成小的任务, 不然不好推进.<br>而大的重构并不是说没有, 但是时间上只能尽量自己去匀自己去控制了.<br>而当时由于 React 生态各种东西不成熟, 而且有时候直接破旧立新, 就比较被动,<br>我为了能争取到主动, 花了很多心思, 但是最终反而开始脱离大部队的倾向.<br>现在的借我就是我倒向了 ClojureScript, 很多方案脱离社区更加严重了.</p>
<p>我在饿了么的精力, 现在我也不太确定, 大公司对技术研究和投入能做到怎样的节奏.<br>就我在小公司的话, 由于场景和业务相对单一, 我要花很长时间面对同样几个问题,<br>所以我可以花很多零碎的精力定制出相对深入的方案, 考虑的场景也单一一些,<br>而在具体操作的要尽量考虑和已有方案短期共存长期尽量替换的思路,<br>在后续的开发当中穿插着逐步把老的方案和实现一点点替换掉, 或者干脆不替换.<br>这个在具体实行的时候是非常琐碎的, 但涉及的代码也不会少.</p>
<p>后面我看到社区别人给出的方案, 特别是大厂给的方案, 封装做得较为完善甚至到多余.<br>就技术方案来说, 我很少能给出那么完善的方案, 因为打磨的时间和需求都相对少,<br>我的方案服务的人群也没有那么多, 公司也匀不出那么多时间让我专心去做.<br>但换个角度说这样持续演进而且封装不完整的方案, 对于他人接手来说显然并不好.<br>这个方案需要我持续 review 和提醒保证执行不出错, 而不是别人比较容易加入维护和扩展的.<br>这些事情说起来是要我作出转变, 规范和严谨, 但在我的角度条件又不够充足的.<br>将来说如果我去了大厂的话, 是否会有足够精力在一块东西上打磨呢, 我心里是有问号的.</p>
<p>但现在再换个角度来说由于我大量基于 ClojureScript 的研究和展开, 可以说自成一体了,<br>这些方案和 JavaScript 内在的那些偏离还有纠葛, 也让我操很多的新,<br>操心的结果对于 ClojureScript 受众以外的人群可能还不大被理解,<br>就这个事情算下来也是让我比较有的头疼的, 也不大乐意到被纠结在这个上边.</p>
<h3>其他</h3>
<p>简聊的代码多年前我就没参与了, 而且公司转向其他方向, 后面大家都知道了.<br>我当初离开的时候除了原来几个原因, 也因为当时 teambition 还是 Backbone 为主,<br>我心理上比较反对在享受了 React 的方案之后再去体验一遍 Backbone 开发的痛苦,<br>而且在 FP 路线走得太远特别是孤立行走太远的情况下,<br>加上 leader 们也不会给我足够支持去推动 React 方面的事情, 我也有点觉得无力.<br>后来 teambition 转 React 的事情知道的人很多, 中间的坎坷也不少,<br>当时我在一家用 Vue 的公司自己心思也在 ClojureScript 上边, 只能远远围观了下,<br>虽然事后有打听过, 但是也没机会跟前同事深入去谈那些东西, 后面关联的少了.</p>
<p>平心而论虽然简聊场景脱胎于 teambition, 但是后者数据规模复杂太多,<br>原先积累的那些工具链和方法论, 是否能在其中应用, 我心里是没底的,<br>而且 Lisp 程序员这种跟着场景做定制的心态, 到了不一样的规模给出的方案变化也会不小.<br>虽然参与的有不少当时简聊的成员, 但我也不认为我的方法论有留下多少.<br>从后面的研究看, 那些方法论跟 ClojureScript 的契合度只多不少,<br>即便我是在快离开的时候才渐渐整个人倒向 ClojureScript 阵营的...<br>现在很多的思路在 Respo 相关工具链中还存留着, 个人项目也还在, 但毕竟变化不少了.</p>
<p>另外那些延续到了后面公司的用法, 随着配合 TypeScript 的努力, 也改变了不少,<br>而且从后面接触的大量后台管理的表单的场景看, 简聊的单页面才是比较特殊的场景.<br>再说后面我也更多接触到前端场景的复杂度了, 甚至移动端和小程序我都还没开始...<br>除了小程序, WebAssembly 那边的事情才开了个口.. 前端真是好复杂.</p>
Respo Hooks 写法的起因和示例
https://segmentfault.com/a/1190000022107907
2020-03-23T01:51:39+08:00
2020-03-23T01:51:39+08:00
题叶
https://segmentfault.com/u/tiye
1
<p>近期对 Respo 的状态管理方案进行了一次更新,<br>具体的代码可以看 <a href="https://link.segmentfault.com/?enc=y5sYSVFLeQ3%2BRyJltNDnRg%3D%3D.mwYsd0QSbmEB%2BtFu06nDgM54545tBI9sKbDEphBfCxQnwMcxpho%2FPsI92LG5t0Tu" rel="nofollow">https://github.com/Respo/resp...</a></p>
<h3>起因</h3>
<p>Respo 的 States 方案为了方便热替换时保存状态, 做了一些限制,</p>
<ul>
<li>组件挂载的时候以及渲染过程当中不能 dispatch 事件,</li>
<li>states 以树形的方式存储, 需要手动调用 <code>cursor-></code> 在组件之间传递分支,</li>
<li>修改状态使用的路径, 也就是 cursor, 跟组件对应, 方便简写.</li>
</ul>
<p>React Hooks 出来的时候, 我当然意识到了 Respo 功能的不足,<br>之前比较久了, 我为 Respo 加上了基础的 Effects, 可以加一些 DOM 操作,<br>React Hooks 可以把部分的组件状态抽到插件里去, Respo 不行.<br>Respo 的 cursor 是跟组件绑定的, 插件写法无法传递 cursor.</p>
<p>另一方面, 我在 <a href="https://link.segmentfault.com/?enc=zI%2BwmALJpY%2Bi6CwWyNfDQw%3D%3D.Six%2Bd1axVl0R%2By62NNzx7%2FJsIxC3J5zzqWI%2Bw%2F9KMX%2BRLmSgbgRUacJkpRqBPg1i" rel="nofollow">Phlox</a> 项目当中由于需要, 设计了个简单的 states tree 方案,<br>这个方案里 cursor 是需要用户手工传递的, 比较啰嗦, 但是勉强够用.<br>后来我仔细想想, 这个方案对于 Respo 来说, 也是够用的.<br>虽然写起来会啰嗦, 但是用户可以手动传递 cursor, 也就意味着可以拆成插件复用.</p>
<h3>代码示例</h3>
<p>文档上写得也不详细, 这边稍微再描述一下.<br>首先 Respo 的状态树, 大致上是这样一个结构,<br>其中 <code>:data</code> 专门用于存储节点的数据, <code>:cursor</code> 作为保留字段,<br>这就是一个树形节奏, 跟组件直接对应, 但是大致跟状态分支对应上:</p>
<pre><code class="clojure">{
:cursor []
:files {
:ns {
:add {
:data {:show? false, :failure nil}
:modal {
:data {:text nil, :failure nil}
}
}
"app.rude" {
":rmapp.rude" {
:data {:show? false}
}
}
}
"app.rude" {
:add {
:data {:show? false, :failure nil}
:modal {
:data {:text nil, :failure nil}
}
}
}
}
}</code></pre>
<p>需要更新状态时, 需要一个 cursor, 可以是 <code>[:file :ns "app.rude" ":rmapp.rude"]</code>,<br>后面还有个 <code>:data</code>, 有了 cursor, 就能定位到数据做更新了.<br>后面大致想象, 组件用的就是一个分支的数据, 而 cursor 对应分支的路径.<br>具体在代码当中比较啰嗦, 不过比较容易可以维护两者的对应关系.<br>最终得到类似组件局部状态的一个效果.</p>
<p>为了方便书写, 我增加了一个 <code>>></code> 函数, 把 states 和 cursor 一起传递.<br>经过杂七杂八的抽象以后, 最终得到这样效果的代码:<br><a href="https://link.segmentfault.com/?enc=nnrNdC7kvRnQVO44NXoVpA%3D%3D.%2FRpbtrVA4Vky6bbdJxRrEz%2FaFo0NNk%2FRcJiFssbxgjrT%2B9MiUD7gWjXJvJe4s1eN8et%2F0oDVTjR8aU1EVqporgRiE5MBvW1%2FF9vvAXrbj9qgpEiv3S8ikTjQcFlVh1V4" rel="nofollow">https://github.com/Respo/aler...</a><br>这中间是省略了好多的过程, 也不打算很详细描述了, 具体要看文档(还没补好).</p>
<p>相应地, 更新状态的部分我加了 <code>update-states</code> 函数, 作为一个简写.<br>状态更新作为 dispatch action 的一种特殊情况, 跟 dispatch 一起被处理.<br>如果没看之前, 我明确一下, Respo 当中 states 是跟 store 一起存储在全局的.<br>能分支读取, 能维护 cursor 做更新, store 当中能响应, 整个流程串起来了.</p>
<h3>延伸的影响</h3>
<p>增加这块功能主要的目标, 跟 React Hooks 类似, 为了逻辑的复用,<br>Respo 的组件跟 React 类似, 不允许从外部操作状态,<br>这就意味着我封装出来的 Modal 组件显得比较奇怪了, 或者说死板,<br>要么我外边维护一个 visible 状态, 传进去, 并且加上 on-change 做切换,<br>要么我把触发打开关闭的部分也放进组件里边, 这样使用起来就有点僵化了.</p>
<p>而 Hooks 形态的写法, 开始允许状态被抽取到一个独立的函数当中,<br>比如我调整过的 prompt 用法, 就可以抽出的一个插件当中,<br><a href="https://link.segmentfault.com/?enc=vu0ok1SSLQXvMPKa5sMEPg%3D%3D.wMHFJvsf73uzuTnnyMUSeYruuImTDEmdLixyCDeV84e41%2FyoNbxoaN41guxWbUKvdspCunrbapV%2BCwRGBB%2FnK6GSbgwLVLV6u%2B6o5z5olSfaUl7A%2BP3q3J562gIGTGbS" rel="nofollow">https://github.com/Respo/aler...</a></p>
<pre><code class="clojure">(defn use-prompt [states options]
(let [cursor (:cursor states), state (or (:data states) {:show? false, :failure nil})]
{:ui (comp-prompt-modal
(>> states :modal)
options
(:show? state)
(fn [text d!]
(if (some? @*next-prompt-task) (@*next-prompt-task text))
(reset! *next-prompt-task nil)
(d! cursor (assoc state :show? false)))
(fn [d!] (d! cursor (assoc state :show? false)) (reset! *next-prompt-task nil))),
:show (fn [d! next-task]
(reset! *next-prompt-task next-task)
(d! cursor (assoc state :show? true)))}))</code></pre>
<p>插件暴露 <code>ui</code> 和 <code>show</code> 方法两部分, <code>ui</code> 用于渲染, <code>show</code> 方法用户更新状态.<br>这样, 以往代码当中相当多的弹层的逻辑就可以抽出做复用了.</p>
<p>后续代码会继续更新. 目前也认识到 Respo Hooks 相比 React Hooks 比较局限比较多,<br>特别是 Effects 那块, React 做得比较强大了, Respo 这方面功能很弱.<br>希望目前来说这个功能够用, 这样我能对 Calcit Editor 遗留的代码做一些整理.</p>
关于 React Hooks 的一些使用经验和换角度反思
https://segmentfault.com/a/1190000022099106
2020-03-22T01:54:48+08:00
2020-03-22T01:54:48+08:00
题叶
https://segmentfault.com/u/tiye
4
<blockquote>本文基于 <a href="https://link.segmentfault.com/?enc=6ajrj6Onc7oLUDRsyMJ7ng%3D%3D.wMVrtk%2Bu9rYUWCa%2F3ApVj3Tw%2F%2F6o63FrF3pe4qX1e7TKk0%2BqeC%2BBXuIP%2Bafs8%2BtN" rel="nofollow">https://reactjs.org/docs/hook...</a> 功能展开</blockquote>
<p>算算时间都要一年半了, React 在 2018 年推出 Hooks, 引发了热议.<br>印象里就是在群里面, 我就很纠结里边的黑魔法太奇怪了.. 看得小心翼翼的.<br>然后看着别人研究代码, 提出类似的实现之类的, 或者各种解释.<br>慢慢地很多不同的声音也发出来, 特别是迷之闭包, 很多人都中招了出来吐槽.<br>再后来, React Hooks 蔓延开来, 连 Vue 社区也开始模仿.. 看来是真重要了.<br>由于我没有动力去深入 React 完整的实现, 所以对细节也只是处在一个大致的了解的状态.</p>
<p>对于 Hooks 想要解决的问题, 我大致是认同的, React 此前的扩展功能太僵化了.<br>高阶组件, 虽然在 FP 里面常见的用法, 实际引入 React 搞成了 decoration 满天飞,<br>..太奇怪了, 一个 FP 里引入的概念, 用大量的 OOP 和 mutable 方法来实现.<br>而且去掉了 mixin 机制之后, React 复用逻辑的问题感觉就是个坑, 重复代码挺多的.<br>虽然我是不信任 React 搞这种黑魔法推翻以前的一些宣传口号的, 但是...<br>Hooks 确认可以帮我解决 class based 组件难以做好的逻辑复用的问题.</p>
<p>我在公司里推 Hooks 的时间比较晚, 已经是在活动听到别人折腾 Hooks 之后了.<br>最开始是 Ajax 代码复用, 网上当初那个例子很明显, 现在我们也抄了很多这种.<br>但是后来, 让我感受到最大变化的, 还是发现 Hooks 对我们的 Form 组件的改善,<br>可以先看例子 <a href="https://link.segmentfault.com/?enc=0lBJAHVcC0pfdQwklZztLA%3D%3D.RXWg%2FmhQcOEjCfjAM9b5dYqqmYxzYfFM4qsDDuxWkiC9qsMuxMpckgKzzIHY3TJP" rel="nofollow">http://fe.jimu.io/meson-form/...</a><br>大致上说, 就是用 Hooks 抽离状态的话, 复用场景更加灵活, 超出组件层面...</p>
<p>这篇文章是我几个跟 Hooks 相关的想法梳理, 线索有点乱, 看章节吧.</p>
<h3>如果能用 Macros 实现 Hooks?</h3>
<p>我们知道 Hooks 最开始就明确说了, <code>useState</code> 等 API 调用, 跟依赖相关, 且不能用包在 if 里.<br>原因也不难理解, React 组件运行过程中要逐个追踪, 条件语句会破坏这个逻辑.<br>我用 ClojureScript 的时间挺长了的, FP 当中一般的玩法我是知道的,<br>一般来说, 为了代码的"引用透明", FP 当中会避免到处存在内部状态.<br>一个用 Hooks API 维护状态的函数组件, 本身居然有这样的状态, 很脱离 FP 常规的玩法.<br>而且这个, 虽然现在是习惯了, 但不能用 if 总归是在有一些限制的, 令人忌惮.</p>
<p>换个角度的话, 也不是说 FP 里面就没有这种状态的性的东西.<br>而是说, FP 当中状态是习惯于显式跟普通的计算区分开的,<br>你要用状态, 就明确声明这边有个状态, 大家调用都注意区分. 不然, 那就是纯的计算.<br>或者, 切换一个层次说, 你这不是代码, 而是 DSL, 这个 DSL 提供新的一层抽象.<br>DSL 相当于构建一套原先的代码之上的一层新的语义, 那无所谓 FP 不 FP 了.<br>可以把深层的逻辑通过复杂的手段约束在 DSL 语义内部, 上层就是使用 DSL 描述.<br>应该说我理解 React Hooks 就是这样的一个状态, 语言之上的 DSL.</p>
<p>作为 DSL 说的话, 我觉得 Svelte Vue 那样倒是更自然了.<br>倒不是说它们提供的方案一定比 React 好, 但是有一个编译阶段, DSL 就更完整,<br>首先 DSL 某个语义未必是一个函数就能实现的, 可能需要增加比较绕的代码,<br>其次, 提供语义也就意味着对用户的术语有约束, 就需要编译阶段做检查做语义的验证,<br>但 React Hooks 这边, 这就是 runtime 插入功能, 剩下让人们自己去约束了.</p>
<p>当然, 我持有的观点, 有一点是 JavaScript 没有 Macros, 限制了 Hooks 的设计.<br>我用 ClojureScript 作为 Lisp, 比较容易修改语法树, 展开简单的代码,<br>(应该说 Lisp 这种, 能力也远不如 Babel 甚至 Svelte 那么强大, 只能说方式很廉价.)<br>Hooks 的设计, 在增强功能的同时, 很大程度是想着保留 API 的简洁.<br>如果有 Macros 可以用的话, Hooks 可以考虑的写法还有很多,<br>比如说定义组件的时候写成,</p>
<pre><code class="clojure">(defcomponent c-demo [x y] (div {} (str x y)))</code></pre>
<p>然后由 Macros 系统展开成函数, 并且由执行环境诸如几个变量(影响到 ts 什么我就不管了),</p>
<pre><code class="clojure">(defn c-demo [x y]
(fn [use-state use-effect internals]
(div {} (str x y))))</code></pre>
<p>我们目前需要全局引用 <code>useState</code> <code>useEffect</code> 然后心里去想着那是运行时的东西如何如何,<br>但是从上面的展开例子, 从 Macros 的角度理解, 很容易知道这些是运行环境控制的操作,<br>这样就没有原先 React Hooks 那种让人产生些许错觉的写法了.<br>(有可能语法展开跟函数执行的区别对非 Lisp 程序员比较难区分... 大致按 Babel 去想吧.)</p>
<p>另一个是状态追踪的问题 Hooks API 依赖的是 <code>useState</code> 的顺序.<br>我个人比较倾向于用名称去控制这多个状态, 在用户端有更多控制能力.<br>而这部分在跟 Vue 和 Svelte 对比会发现 React 这边能设计出更多的花样.(没去了解具体实现)<br>而 React 使用函数作为唯一的手段, 就显得非常, 或者还是逃不脱被 DSL 要求存在的限制.<br>当然话说回来, React Hooks 单调的方式能被设计出来满足这么多功能, 也是超乎我想象了的.<br>同时也由于抽象手段单一, 就非常依赖 runtime 内部实现奇奇怪怪的手法去支持功能.</p>
<h3>Respo 拙劣的山寨</h3>
<p><a href="https://link.segmentfault.com/?enc=TcvhiMCnJxN%2FlMeX9Kb9jw%3D%3D.ne2H4%2BIs2A8iQm9LpP94STftWosuFQOnDSNcmog4JK0%3D" rel="nofollow">Respo</a> 是我个人项目使用的 cljs 之上的 Virtual DOM 方案. 并不基于 React.<br>这样我就有机会试验我自己思考的一些组件和状态的抽象方案.<br>Respo 里边, 为了保存热替换过程中组件的状态, 设计了用户态的"状态树"的概念,<br>用户在定义组件的时候, 大致上对一个 Todolist 会在全局构建和存储这样的树形结构:</p>
<pre><code class="clojure">{
:data {}
:todolist {
:data {:input "xyz..."}
"task-1-id" {:data {:draft "xxx..."}}}
"task-2-id" {:data {:draft "yyy..."}}}
"task-2-id" {:data {:draft "zzz.."}}}
}</code></pre>
<p>组件代码当中需要比较啰嗦的声明,</p>
<pre><code class="clojure">; for app
(comp-todolist (>> states :todolist))
; for single task
(comp-task (>> states (:id task)) task)</code></pre>
<p>另外具体使用还要比较啰嗦的声明,</p>
<pre><code class="clojure">(let [state (or (:data states) {:input ""}]) "TODO whole list")
(let [state (or (:data states) {:draft ""}]) "TODO Task")</code></pre>
<p>以及在组件层级之间传递 <code>cursor</code> 的路径位置, 以便发起更新...<br>相比 React 任何一种多了很多的代码, 只能说为了热替换稳定做了非常拙劣的模仿,<br>然后, 再次之后, 我也能在这个方案上, 也提供类似 Hooks API 的状态抽象,</p>
<pre><code class="clojure">(defn use-menu-feature [states options]
; TODO
{:ui "TODO",
:effect-fn "TODO",
:edit-fn "TODO"})
(menu-A (use-menu-feature (>> states :menu-a) {}))
(menu-B (use-menu-feature (>> states :menu-b) {}))</code></pre>
<p>这是一个脱离了 Macros 和编译方案, 也脱离了 React 内部状态黑魔法的方案,<br>而这样简单啰嗦的方案, 切切实实也能模仿出 Hooks 核心的一些功能来.<br>(当然在整体功能上, 跟 React 不能比, 而且要实现的话也依赖 runtime 做.)</p>
<p>这个例子对于 React Hooks 本身没有什么帮助或者阐释,<br>主要是从另一个角度去看, 作为一个前端 MV* 方案, 怎么看待其中的核心需求和实现.<br>各种方案在各种需求点的路径探索以及取舍, 脱离一下限定的视角, 会有其他主意.<br>或者说, 没有 Hooks 甚至没有 React 时, 这种逻辑复用你通过何种方式达成?</p>
<h3>业务当中内秉的状态</h3>
<p>回到 Hooks 本身, 我目前在的业务当中探索使用的方案, 大致可以参考 Form 这个例子,</p>
<pre><code class="js">let formItems: IMesonFieldItem[] = [
{ type: 'input', name: "name", label: "名字", required: true },
{ type: 'select', name: "city", options: selectItems, label: "城市" },
];
let [formElements, onCheckSubmit, formInternals] = useMesonItems({
initialValue: {},
items: formItems,
onSubmit: (form) => {
console.log('After validation:', form);
},
});
// 返回的 formElements 用于 UI 渲染,
// formInternals 包含几个方法, 比如 resetForm 重置组件状态</code></pre>
<p>或者也参考弹出提示的这个例子,</p>
<pre><code class="js">let [ui, waitConfirmation] = useConfirmModal();
let onClick = async () => {
let result = await waitConfirmation({
text: "节点可能包含子节点, 包含子元素, 删除节点会一并删除所有内容.",
});
console.log("result", result);
}
// 点击调用 onClick 时, 修改 confirmModal 的内部状态, 打开/关闭
return <div>
<button onClick={onClick}>Confirm</button>
{ui}
</div></code></pre>
<p>相较于 React 组件以往的抽象方案来说, Hooks API 暴露出来了一个从前我们熟悉的功能.<br>就是: <strong>可以修改抽象模块的内部状态了</strong>.<br>以往 React 宣传的, 组件作为抽象方式, 内部的状态是封闭的, 外部不应该去操作.<br>比如就一个弹窗的 <code>Modal</code>, 你就需要外边有个 <code>visible</code> 的状态去控制, 传进 props, 显示, 还是不显示,<br>但是有了 Hooks, 这时候你可以把状态封进 Hooks API 内部, 然后暴露回调函数去操作.<br>可是你又明显能知道, 这边封装了一个状态.<br>至少从前 React 并不鼓励这种状态, 或者应该说, 此前并不存在简单的这样的功能.</p>
<p>反观我们实际业务当中, 局部状态封装却是很常见的东西,<br>比如 <code>visible</code>, 以往我们通过插件去实现的时候, 不就是把状态藏在插件内部的吗,<br>然后得到一个 <code>.toggle()</code> 的方法, 然后可能还会多个地方去 <code>if (modal.visible) {}</code> 一次次探测.<br>当 React 提出需要一个父组件存一个 <code>visible</code> 还有加一个 <code>onChange</code> 回调的时候, 让人觉得很怪异,<br>最终我们希望暴露给业务当中使用的时候, 明显 <code>.toggle()</code> 才是极为简短清晰的方案.</p>
<p>顺着这个思路, 怪异的事情来了, jQuery 时代, 我们欢快地 <code>.toggle()</code>,<br>React 来说, 说 rethink, 然后我们加了 <code>visible</code> 和 <code>onChange</code>, 然后 React 真香,<br>现在基于 Hooks 方案, 封装局部状态的方案又开启了... 历史又陷入螺旋了.</p>
<p>回过头去说 jQuery 插件形式的抽象, 是不是好呢, 显然当时使用的体验并不好,<br>由于有状态, 可能多个位置都想要去控制这个状态, 就需要多次判断状态, 要开还是关,<br>React 所做的, 首先有一点, 状态集中数据这边来, 尽量控制单向自动流动,<br>同时通过 data-driven 的思路和实现, 减少手动同步的状态的工作量.<br>这一点来说, React Hooks 比起当初 jQuery Plugin, 是要方便很多的.</p>
<h3>其他</h3>
<p>我一直有种感觉, React 选择 FP 这条路的时候, 其实没那么清晰,<br>探索了那么多方案, 强行把函数式的一些说法套用到前端这边来, 可能很多人都不能理解准确吧,<br>你说是 stateless 还是 state isolation, 细小的差异, React 这边并没有梳理清楚.<br>真的函数式语言, PureScript Elm 怎么写的前端, 真的是 React 这样子的吗?<br>我为了概念稍微准确点, 转向了 ClojureScript, 实际用下来 React 跟它还是差异挺大的.<br>在状态这个事情上, React 有了这次的调整, 未来谁说得准要不要再调整一次呢.</p>
<p>或者就是问一句, 当我们拿 React 往 FP 去套的时候, 是否要把 Component 往 Function 去套呢,<br>让 Components 跟 Functions 那样能高阶组合, 传递闭包之类的?<br>我觉得从现在看, Component 不是对应 Function, Component 是 DSL 中的一个抽象,<br>这个抽象可以继续扩展出 states, effects 等等, 比起 Function 复杂太多.<br>我们用 FP 手段去提升整个 MV* 方案, 是用在具体实现和优化的层面, 并非取代组件这个观念.</p>
<p>就我在 ClojureScript 的使用经验而言, FP 能提供非常强大的表达能力, 便于开发,<br>但是代价是大量的内存申请, 而这也是 React 不得不努力去做性能优化的原因.<br>对 FP 语言来说, 这样的优化要大量再语言层面实现, 编译器, runtime, 打包, 到处优化,<br>对于 JavaScript 来说, 把自己优化成另一门语言, 显然是不切实际的事情.<br>所以我对 JavaScript 生态之上的 React, 始终认为是存在隐患而且越走路越窄的.</p>
<p>至于 Virtual DOM 带来的 Model->View 那种 date-driven 的模式,<br>换个装逼的说法 "DOM 更新方案的自动化", 从 Angular/React 真的普及到了整个前端领域.<br>这个是实打实的提升. 现在混坑爹很难想象谁还会去用主打手动更新 DOM 的前端框架了.<br>而在此之上的状态管理, 简直是一片混战, 即便在 React 生态内部, Mobx 也是横生枝节.<br>基于不可变数据的, 基于 observable 的, 可能大家就是相互看不惯吧...</p>
Clojars 用 depstar 和 deps-deploy 发包的记录
https://segmentfault.com/a/1190000021716720
2020-02-10T14:50:28+08:00
2020-02-10T14:50:28+08:00
题叶
https://segmentfault.com/u/tiye
0
<p>因为考虑 CI 做部署的事情, 在网上问了一下, 有社区的高手给了个方案:</p>
<p><a href="https://link.segmentfault.com/?enc=6jno5IVRW1a7SE%2F4l2lm4g%3D%3D.K77ibfryCWpnFAge9cwukri4%2FqVyv74qD9mnASmSf4D8RVdC1jUk6MaX1krOj9GtQQcGpNizrk1hcmQviPXj8ufynzA96mJcFwUAnFrHbAb55MQUgFouTrHEvO4ttv28" rel="nofollow">https://clojureverse.org/t/gi...</a></p>
<blockquote>I would expect this to be possible since lein, boot, and CLI/deps.edn all have ways to build an uberjar and publish it to Clojars. (for the latter, see <a href="https://link.segmentfault.com/?enc=3WcYIhG1s0bFiJYESlsL9g%3D%3D.2ZQIVWaP7yAsOQEqlwRXoaECQEbQmkO0saQqoxPVX1rneyUD81FQJ7iZtqSKqTlVteBZAUlmgOCWKyFg3E%2FqbA%3D%3D" rel="nofollow">https://github.com/clojure/to...</a> 3 – I use <a href="https://link.segmentfault.com/?enc=6LCcLFaVWC5Bup42XOuCyQ%3D%3D.Xp9lT1tSdpKV0s7jeGUvI63NhJsmNM6f32HEnXnQeNMPVgxVdEOuKUfEf%2FpKuF31" rel="nofollow">https://github.com/seancorfie...</a> 2 and <a href="https://link.segmentfault.com/?enc=7n2Ka%2F6Fq%2FSjTyimACOTEQ%3D%3D.G4eQjFxVSfLWjljsRfRX%2FihqyEYwK2rQ0%2FXfjCp0%2BqiLPGm0z9GsKqbljyRAAhrJ" rel="nofollow">https://github.com/slipset/de...</a> 4 for build/deploy)</blockquote>
<p>配置 Meyvn 的时候, 安装过程询问是否要证书, 选了 NO, 然后就退出了.<br>后来又尝试了下 <a href="https://link.segmentfault.com/?enc=%2FXVjwR16kdum0M1qZ02Cig%3D%3D.G4sOiwgx%2FvagHL3vUGSs0C9ek2Tya7%2BHZ8TQiL%2BWkwa3imB9JKmpt7V%2BuawwKK%2Bl" rel="nofollow">https://github.com/juxt/pack....</a> 安装过程就出错了.</p>
<p>高手提供的方案是有两个脚本做打包和发布, 他使用的, 比较稳定的,</p>
<p><a href="https://link.segmentfault.com/?enc=%2BmiDRu7%2BfHDS8yMsuGoeMA%3D%3D.ZALG7ZrZLuMZpjvLZnuszceGKZE0%2FQHQpnRTCwTfYtA0OWDIxKA4Ov%2BpCZ%2BwfwyR" rel="nofollow">https://github.com/seancorfie...</a><br><a href="https://link.segmentfault.com/?enc=JxJozEn6Xu2jyvFjRNEvHg%3D%3D.atQvESmQksWfgJi5SxXicoLwHXFCGoiUMoQQsXyhlRoH5uhASwXsjLoHZwmCbyQ2" rel="nofollow">https://github.com/slipset/de...</a></p>
<p>具体的打包发布步骤基本对应 <a href="https://link.segmentfault.com/?enc=D9flqUMtD183YmWhd8Zi1w%3D%3D.vOG7huKNQlIWCbsNlAQ7qcj1eKw5yUvEN8xjZqPCZwcv5IouyZBJk6iPR8GamWDX" rel="nofollow">https://juxt.pro/blog/posts/p...</a></p>
<p>跑通以后目前的 <code>deps.edn</code> 配置:</p>
<pre><code class="edn">{:paths ["src"]
:aliases {:depstar {:extra-deps {seancorfield/depstar {:mvn/version "0.5.2"}}
:main-opts ["-m" "hf.depstar.jar" "target/lilac.jar"]}
:deploy {:extra-deps {deps-deploy {:mvn/version "RELEASE"}}
:main-opts ["-m" "deps-deploy.deps-deploy" "deploy" "target/lilac.jar"]}
:install {:extra-deps {deps-deploy {:mvn/version "RELEASE"}}
:main-opts ["-m" "deps-deploy.deps-deploy" "install" "target/lilac.jar"]}}}</code></pre>
<p>启动命令:</p>
<pre><code> "m2": "clojure -A:depstar && clojure -A:install",
"deploy": "clojure -A:depstar && clojure -A:deploy",</code></pre>
<p><code>pom.xml</code> 文件是 <code>clojure -Spom</code> 生成的, 但是中间内容要修改, 包括增加部分字段,</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>mvc-works</groupId>
<artifactId>calcit-workflow</artifactId>
<version>0.0.1-a1</version>
<name>calcit-workflow</name>
<url>https://github.com/mvc-works/calcit-workflow</url>
<description>TODO</description>
<scm>
<url>https://github.com/mvc-works/calcit-workflow</url>
</scm>
<dependencies>
<dependency>
<groupId>org.clojure</groupId>
<artifactId>clojure</artifactId>
<version>1.10.1</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>src</sourceDirectory>
</build>
<repositories>
<repository>
<id>clojars</id>
<url>https://repo.clojars.org/</url>
</repository>
</repositories>
</project></code></pre>
<p>单纯 pom.xml 的内容结构是很复杂的, 参考:<br><a href="https://link.segmentfault.com/?enc=0sDEonmTyKtcpMspfMSoHw%3D%3D.QwpjHbHOrL%2FGFiGGZUs1LuWcInb8WpbQOAYDLNisX9wlHuWUTCIVswMlB%2FuSSLywa3VmubvrUpf4WD0Q5qKl6d6%2BJHIOf8LTPPSWmixOQy2fE6TqHgliMSDPuXagurM9" rel="nofollow">https://github.com/seancorfie...</a><br><a href="https://link.segmentfault.com/?enc=fKT620ROGzBPM0xW0UsleQ%3D%3D.q2wNtLVNae5nH9YtTm4qM%2BxevvLtvKPmzT%2BqSzyWpUx4lP%2BmjNh071ZN14oM9kgHg2EoOpahy31bWzivJo7rLQ%3D%3D" rel="nofollow">https://github.com/clojure/cl...</a></p>
<p>没有打算提供完整的 pom 文件了. Clojars 上显示的内容也不完整.<br>目前只是想做到能发包和使用, 维护尽量简单可重复.<br>毕竟影响到的项目太多了.</p>
<p>另外 deps-deploy 账号密码是通过 env 提供的, 具体看 README.<br>对应 CI 来说基本还是可以配的, 管理来说却是不如 token 合适.</p>
九章编程: 文言文编程的 Cirru 实现的一种试验
https://segmentfault.com/a/1190000021561635
2020-01-12T23:46:21+08:00
2020-01-12T23:46:21+08:00
题叶
https://segmentfault.com/u/tiye
1
<blockquote>本文是对于 <a href="https://link.segmentfault.com/?enc=1ElGMB3UNwgUNkdMLIK%2FCg%3D%3D.7NfNPIj9qvAQo4dTzKylaBlmquH4uA40ybmm%2FXt66aBJ%2FWMerMETRFJ17jKvHIJ3" rel="nofollow">wenyan-lang</a> 方向在的一些尝试, 利用 Cirru 的工具链, 做简化的方案.<br>代码实现看<a href="https://link.segmentfault.com/?enc=EhMJh1lBVvdksP3S%2FImLfA%3D%3D.tQBDEcesYr8ALPY3iBqUmu%2BdJC6oagitni%2FdXf1jjyyaGVc1hBSOpS18NNI2UeSh" rel="nofollow">九章编程</a>, 以及对应的 <a href="https://link.segmentfault.com/?enc=RTN6m5Sy0XETACLIKPWsdQ%3D%3D.qaXJ6XII2dgF1RlA4WER8vARLht8A7PyfkwK6OjtRxE%3D" rel="nofollow">Demo</a> 页面.</blockquote>
<p>基于九章编程的方案, 最终实现一个 Fibonacci 函数, 代码是这样的,</p>
<pre><code class="cirru">术曰 菲氏数 (甲)
若 (少于 甲 三) 一
并 (菲氏数 (减 甲 一)) (菲氏数 (减 甲 二))
得 (菲氏数 一)</code></pre>
<p>对比一下<a href="https://link.segmentfault.com/?enc=xQuOr1QbB6dzV2dkC8faTA%3D%3D.Jp7Irk3VbL884uFVxgr3icUIsDFZgzmic5ySYeXZ4wJFB%2B7AecsOzKDGOdFhaI%2Fio1ij%2FlXtLIh1LfQvhXLj%2Fs1gpNYUUrYDwC690hCuCDs%3D" rel="nofollow">文言编程</a>的例子, 会显得后者啰嗦很多.</p>
<pre><code class="wenyan">吾有一術。名之曰「斐波那契」。欲行是術。必先得一數。曰「甲」。乃行是術曰。
若「甲」等於零者乃得零也
若「甲」等於一者乃得一也
減「甲」以一。減「甲」以二。名之曰「乙」。曰「丙」。
施「斐波那契」於「乙」。名之曰「丁」。
施「斐波那契」於「丙」。名之曰「戊」。
加「丁」以「戊」。名之曰「己」。
乃得「己」。
是謂「斐波那契」之術也。
施「斐波那契」於十二。書之。</code></pre>
<p>当然了, 九章编程方案只是取巧地把 Lisp 的写法翻译成中文而已, 少了很多.<br>首先这东西挺好玩的. 再就是自己仔细看下来也有不少自己的想法.</p>
<h3>古文用法的一些想法</h3>
<p>翻了代码里的问题, 文言编程几个作者古文词汇和句式都比我丰富挺多的.<br>不过因为现在的人都不是常用古文, 其实也挺不正宗的.<br>大致能感受到例子里很多用法, 是混杂了不同朝代的措辞, 所以有点怪.<br>而且有些无法解决的问题, 西方传入的数学和计算机术语, 免不了要用现代的词汇.</p>
<p>当然, 按照我的偏好, 如果说有古文编程的话, 我首先想到用古代典籍作为模板.<br>比如说<a href="https://link.segmentfault.com/?enc=cfybHmPH3AlfH3OPvgohzg%3D%3D.BCvq6SmJFGWs7xcewxItf7GF3AequdePPIPFC4ub0F5eVCI3FEpMm5OOsIUFKyoNh0PIrCRv8%2FmkADK5X8qebg%3D%3D" rel="nofollow">九章算术</a>, 至少汉代的著作了, 这应该能充分代表古人对于数学的表达习惯.</p>
<blockquote>〔三七〕今有環田,中周九十二步,外周一百二十二步,徑五步。問為田幾何?<br>荅曰:二畝五十五步。<p>〔三八〕又有環田,中周六十二步、四分步之三,外周一百一十三步、二分步之一,徑十二步、三分步之二。問為田幾何?<br>荅曰:四畝一百五十六步、四分步之一。<br>術曰:并中外周而半之,以徑乘之為積步。<br>密率術曰:置中外周步數,分母、子各居其下。母互乘子,通全步,內分子。以中周減外周,餘半之,以益中周。徑亦通分內子,以乘周為實。分母相乘為法,除之為積步,餘積步之分。以畝法除之,即畝數也。</p>
</blockquote>
<p>以及元代的<a href="https://link.segmentfault.com/?enc=R6hUxZE%2BTR9r%2BDloOkVWTw%3D%3D.G6TJEtGON5z7tFaE83%2FW1m40qo6sHGEPGOHIFybsIEsL81dp3JGB%2Buen4b5GtFUfb4kz0%2BFaN0QWlDRriWlO263ujCOWngZxsqlLSt9EGnA%3D" rel="nofollow">四元玉鉴</a>当中也有类似的表达习惯.(没有句读太难读了..)<br>我觉得如果这些数学家当年发明编程语言的话, 怕是跟这差不了多少.<br>不过也还好现在的编程语言有各种标点符号, 不然真是有的受了.</p>
<p>另外比较明显的一个麻烦是, 代码当中必然会有较多的抽象, 或者说定义函数,<br>即便大家用的不是 Lisp 这样的前缀表达式语言, 也少不了会遇到这样的代码,</p>
<pre><code class="cirru">(f1 p1 p2)
(f2 q1 q2 q3 q4 (f3 a5 q6) (f4) q7 q8)</code></pre>
<p>算了我还是换个你们好接受一些的写法:</p>
<pre><code class="coffee">f1(p1, p2)
f2(q1, q2, q3, q4, f3(a5, q6), f4(), q7, q8)</code></pre>
<p>在文言编程当中, 可以看到 wenyan 用了 <code>名之曰「丁」</code> 来定义操作,<br>而实际上对应这种枯燥的抽象, 基本上很难也古文自然得表达出来.<br>或者说代码, 作为给机器执行的语言, 本身就有着特殊性.</p>
<p>当然, wenyan 能定义出这么一整套来, 还是挺厉害的.</p>
<p>在九章编程当中, 我出于省事的考虑, 直接基于已有的 Lisp 风格直接做了.<br>也就是说, 九章编程基本上就是基于前缀表达式实现的. 不像自然语言.<br>但是具体的术语, 我基于九章算术的文本做了简单的统计, 选取了一些词汇,<br>总得来说只是借了一层九章算术花样, 比如"对象", 九章算术里压根就没这东西.</p>
<h3>中文数字和变量名的一些处理</h3>
<p>我看 wenyan 当中用的中文数字表示, 在源码里有自己去解析和搜集.<br>翻了一下代码, 大概是自己进行了解析吧, 先转成阿拉伯数字表示, 就很快了.<br>九章编程里面直接找了个模块 <a href="https://link.segmentfault.com/?enc=8%2FqI3e5o4h%2Fzut5wsUIXog%3D%3D.WUyWmO0W8YcbZpR%2BNGYSAVPL2S3qb0GrZGS6O23oJfE%3D" rel="nofollow">nzh</a> 进行转化的, 做 Demo 也够用.</p>
<p>另一个是变量名的问题, 九章编程直接用中文字符串做的.<br>因为九章编程实际上是 interpreter, 不是转义 js 的, 没这个限制.<br>wenyan 的实现当中我看到有转成拼音的操作, 不确定具体情况.<br>按说中文, <code>甲</code> <code>假</code> 都是 <code>jia3</code>, 虽然有字典, 但很容易会重名的.</p>
<h3>JavaScript 方言的实现方案</h3>
<p>wenyan 大致上提供了 js, py, ruby 的方案, 大致看了一下 js 的部分.<br>首先 tokenize, 再 parser 解析代码, 然后用 compiler 拼接 js.<br>拼接 js 的部分是直接用的字符串拼接, 相对来说不那么完善, 但是够用.<br>另外手动拼接得到的代码, 一般格式都是乱的, 需要用 Prettier 或者 Babel 重新格式化.</p>
<p>另外比较省事而且可靠点的方案是用 <a href="https://link.segmentfault.com/?enc=RTqZw1GVE7I3mH%2BeuB71hw%3D%3D.f0JqcTDJq0pDLKsmYD0ZD%2BGQhcTIe%2BqruHHMq0K15Cm3s9tsDBkSo1dJwPO2XjQA" rel="nofollow">babel/generator</a> 去实现.<br>就是说用 Babel 来处理 JavaScript 代码生成的具体实现,<br>这样几方的工作只要做到能生成 <a href="https://link.segmentfault.com/?enc=GZLldkKPtwZZ5kU22fNZ6w%3D%3D.M6F2TBCziz7nlspFWi0ksg3yDsZCMJWAKb2GC9laahE%3D" rel="nofollow">AST</a> 就好了, 这就安全很多.<br>比较熟悉是因为我的 <a href="https://link.segmentfault.com/?enc=EghW3sfsO%2FxzvD3Qwycswg%3D%3D.sbsJvHVVZVhQQSmUnHGP66%2FVoQWv1h6w%2FyouDwijp%2FmoVacFSz7h8LHKdJmz93UF" rel="nofollow">CirruScript</a> 用的就是这套方案, Babel 工具链真挺丰富的.</p>
<p>九章编程用的方案是 Interpreter, 解释执行, 没有生成 js 代码.<br>这也就意味着执行计算都是在 JavaScript 运行环境内部的,<br>单纯 JavaScript 执行, 可以有 V8 优化, 最终甚至可能以汇编的形式运行,<br>那样来说性能就好很多了. 解释执行的问题就是性能会很差.<br>不过另一方面, 解释执行不需要满足 js 语法, 也就没七七八八的限制了. 直接跑.</p>
<h3>Cirru 提供的方案</h3>
<p>虽然对于编译器来说, 生成代码的优化是最难的部分, 但玩具项目的话...<br>要写个 Parser 把整个代码结构解析出来也是相当要命的工作量.<br>wenyan 光是 <code>parser.js</code> 就八百多行了, 还不算各种工具函数和关键字定义的,<br>没看明白 <code>typechecker.js</code> 具体逻辑, 校验结构么, 也快七百行了.<br>反而 compiler 生成 js 部分三百多行就搞定了...</p>
<p>我...毕竟是写着玩的, 如果 Parser 也要这么风风火火折腾一遍, 枯燥啊.<br>不过我有 Cirru 这边的工具链, 加上语法, 直接用 Lisp 风格套上去了.<br>Cirru 大致是是一套把缩进语法(或者数据)生成一个树结构的方案,<br>比如这样一段代码, 直接用 Cirru 的模块进行解析,</p>
<pre><code class="cirru">得 (菲氏数 一)</code></pre>
<p>就能直接得到一个树形的结构:</p>
<pre><code class="json">[
[
"得",
[
"菲氏数",
"一"
]
]
]</code></pre>
<p>前面函数定义的部分, 代码复杂一些, 有缩进, 也对应解析出来:</p>
<pre><code class="cirru">术曰 菲氏数 (甲)
若 (少于 甲 三) 一
并 (菲氏数 (减 甲 一)) (菲氏数 (减 甲 二))</code></pre>
<p>得到着要一个树形的结构:</p>
<pre><code class="edn">[
[
"术曰"
"菲氏数"
["甲"]
[
"若"
["少于" "甲" "三"]
"一"
[
"并"
[
"菲氏数"
["减" "甲" "一"]
]
[
"菲氏数"
["减" "甲" "二"]
]
]
]
]
]</code></pre>
<p>有这样一个结构, 后面的部分就相对容易了, 如果不校验的话, 直接就能算, 比如:</p>
<pre><code class="edn">["减" "甲" "一"]</code></pre>
<p>经过简单的变换就能得到对应的 JavaScript 代码:</p>
<pre><code class="js">(甲 - 1)</code></pre>
<p>或者更加复杂一些的结构,</p>
<pre><code class="json">[
"并"
[
"菲氏数"
["减" "甲" "一"]
]
[
"菲氏数"
["减" "甲" "二"]
]
]</code></pre>
<p>其实就是判断一下, 对中间的数组进行递归计算, 也很容易完成求值.<br>当然, 具体到函数定义方面, 以及一些动态长度(或者复杂节够)的语句, 会麻烦一些.<br>原理上可以参考 <a href="https://link.segmentfault.com/?enc=bmnXi5GY6PCxuaSyMBUM7g%3D%3D.bE3ZIqpxTCZB4xB2baZ3R10%2F9zhp1Ij7r1qazGe4hF8%3D" rel="nofollow">http://norvig.com/lispy.html</a> 提供的例子.</p>
<p>这套方案用了 Cirru 的 Parser, 同时也就继承了 Cirru 语法的约束,<br>比如用 <code>(</code> <code>)</code> <code>$</code> <code>,</code> <code>"</code> 以及空格进行语法结构控制的事情.<br>放在九章编程里面, 主要是在古文编程当中插入了大量的英文符号...<br>或者说这些如何其实除了空格换成中文笔画符号.. 可能效果也是类似的, 总之有些奇怪的东西.<br>不过总体上讲, 直接省去了大量工作量.</p>
<h3>其他</h3>
<p>wenyan 高亮做得比较充分, 渲染图挺漂亮的. 九章这边没有专门做.<br>不过倒也不是一点高亮都没有, 可以看到<a href="https://link.segmentfault.com/?enc=vIe5FKUBcE%2F3n9wOguWuVA%3D%3D.gZG7jgrV8jY005DiGV3MzmD7c%2F0aOnmsa9eHAt76Ntv7TB5S0QovyyZ94hvCEBFy" rel="nofollow">文档里直接用 Cirru 进行了基础的高亮.</a><br>看源码那边, 好像 wenyan 用的是 SVG 渲染的图, 效果确实不错.</p>
<p>因为是个玩具项目, 九章编程试验到能求 Fibonacci 然后, 有点玩不动了.<br>我知道后面的工作量挺多的, 比较我之前 Cirru 项目当中就在尝试.<br>有兴趣过来 watch 一下 <a href="https://link.segmentfault.com/?enc=jgh2Kumm4pxhc8S7koa4BQ%3D%3D.es3aLHLPPmUVvc5euEdsgJWZRjUrD6Vs4w%2BAzAJ6aaABjegzi1cpdIBybu5XCXUr" rel="nofollow">CirruScript</a> 跟 <a href="https://link.segmentfault.com/?enc=rb35M2uDAlwhZNt4ixEc5Q%3D%3D.Lhhj7d9Q9gDrpOUZenjWjcDvIVcACJynS6QFoBi3MM%2BWhXP%2F4s33oZjpmafBJzkb" rel="nofollow">interpreter.nim</a> 这边的工作.<br>虽然暂时没有经历深入开发, 但是断断续续会王中间追加功能的.</p>
<p>另外我在微博上也有提到, 中文的表达能力其实是非常强的,<br>可以想象, 同个含义, 在古文当中可以得到多个表述, 排列组合下来, 不比英文少...<br>以往看到的<a href="https://link.segmentfault.com/?enc=Q7%2FdKhMzlyFwpEc7uUmw5A%3D%3D.TuLPQazDBufmUZr1IfzF6qEdzrBYh%2Bqd2iXH%2BIFcgpPWkVAoKGSkHMHnYj1Juo13fmcGaB2cii6Ac0Sw0lVC2Q%3D%3D" rel="nofollow">中蟒</a>算是做的比较完整的一个范例吧.<br>完全有很多可能, 可以脑补一个少儿编程的场景, 写一段代码,</p>
<pre><code class="cirru">李雷的数 是 1
韩梅梅的数 是 2
(李雷的数 加上 韩梅梅的数) 是多少</code></pre>
<p>这个代码<a href="https://link.segmentfault.com/?enc=jbNR0PZoHByk9Ns7GuS1WA%3D%3D.Y0Tw4hUgVC9bgSIzl%2BmoGGjw0o94CArmBy76zDgkuzabj3jepLTlvt2NuvRYgz%2BP" rel="nofollow">用 Cirru 能解析出来</a>一个简单的结构,</p>
<pre><code class="json">[
[
"李雷的数",
"是",
"1"
],
[
"韩梅梅的数",
"是",
"2"
],
[
[
"李雷的数",
"加上",
"韩梅梅的数"
],
"是多少"
]
]</code></pre>
<p>做一下语法转换, 就得到一串很熟悉的前缀表达式了,</p>
<pre><code class="json">[
[
"是",
"李雷的数",
"1"
],
[
"是",
"韩梅梅的数",
"2"
],
[
"是多少",
[
"加上",
"李雷的数",
"韩梅梅的数"
]
]
]</code></pre>
<p>然后求一下值, 拿去忽悠一零后有没有效果...</p>
<p>换成中文门槛也低一些吧, 应该有不少可以尝试的.<br>九章编程的 Demo 是可以执行的, 欢迎试玩 <a href="https://link.segmentfault.com/?enc=sxVJFHFhaKReG6TWK2OAeQ%3D%3D.hjg0D85%2BeFT%2F6hI2rxZufvSpTCizBJAcx65R4a9tG%2Fk%3D" rel="nofollow">http://jiuzhang.cirru.org/</a></p>
折腾前端条形码(Barcode)扫描识别, 笔记
https://segmentfault.com/a/1190000021450639
2019-12-31T11:32:07+08:00
2019-12-31T11:32:07+08:00
题叶
https://segmentfault.com/u/tiye
5
<h3>
<code>@zxing/library</code> 方案(不推荐)</h3>
<p>本地勉强把 Demo 在 React 里面跑通, 但是不好控制开始结束, API 不明确.<br>实际识别率很低. 我是用手机屏幕放的条形码, 大概也有影响.</p>
<p><a href="https://link.segmentfault.com/?enc=2dxCq3KgafDeMChDOB2bXg%3D%3D.yllEXFF7F3JCGcXLtyRKqtd8171HNtvnnOIOkCqwdBKr6tSfYDiCRvGZHi3EG2b5" rel="nofollow">https://github.com/aleris/zxi...</a><br><a href="https://link.segmentfault.com/?enc=liXFtLf4mVwPpp9d4ajnxw%3D%3D.Ghy%2F87W2ZvDrB59ckHFbdOuVxibielxMjHVl2mAxwrrl4ivx%2FZuANKHDgKzjlYrZjNKzD6CDJjWkqJwIrLwXG6Xfeb4P7c9vjntRRABAUFsIwK15B8sGoOE3%2FiJ%2BIIBW" rel="nofollow">https://github.com/zxing-js/l...</a><br><a href="https://link.segmentfault.com/?enc=%2BThCB4Y5cTqpmrXvlt6tbA%3D%3D.8Af%2FOb8w9oUgNIj%2Fo8Bli6b%2BMwErT1%2BkxHJejZVjPUnzU%2F%2BkJ%2BtYwSu6Fz1iJe8EWaK3zDcwKNZBjI2F30N5%2FxxzUIOmaZ9YNREol861z7MgGD439cn7rS4NDEG1ij%2Bk" rel="nofollow">https://github.com/zxing-js/l...</a></p>
<h3>Quagga2 方案(推荐)</h3>
<ul><li>QuaggaJS</li></ul>
<p>应该是 GitHub 上星星最多的, 但是<a href="https://link.segmentfault.com/?enc=SWOusCozxfU1n%2Fxv2HVFhw%3D%3D.UzrACCEpBdaYDmM2IzrnL1OwbcMTqxnozaAfCZ7W8yO7SyJi0H%2F5CrCT%2FJpqs39%2F" rel="nofollow">没有人维护了</a>.</p>
<p><a href="https://link.segmentfault.com/?enc=%2F%2FjFtGNdOEnliVyFPpKCsw%3D%3D.v%2FfH0pJLLaNyBXBzmNUOB42JBLOhOwh2VNZl0CqWcmf0pCsm%2Fp6YWZ8eSgWsP%2B8TNiRo%2B19Mg01SX6DPbXvva6EGVpysdg%2BqxZCDQ7zcqC8%3D" rel="nofollow">https://serratus.github.io/qu...</a></p>
<ul><li>Quagga2</li></ul>
<p>fork 版本的 QuaggaJS. 一直有更新, 但是没有维护全部的细节, 我在运行 examples 遇到了问题.</p>
<p>初步认为是传入到 Worker 里面执行的代码不完整. 没有把 Quagga 的源码提前传送过去.<br>后面作者先把 worker 功能关掉了.. 至少界面不报错了.</p>
<p>有一个用了旧版本的 React 组件的 demo <a href="https://link.segmentfault.com/?enc=9BpIvnrUTaBdUNGtEUOiUg%3D%3D.p06Eav6z17jLKt%2Bnq73FpwchgY%2FABMaetShizTtq8jtuALKpMlr1bBojtVVl9bXVe%2B0OIvfTwyUEQ2BVo113sTlBUbrSTeFY56ux1k2vGPI%3D" rel="nofollow">https://github.com/ericblade/...</a></p>
<p>自己封装了一个版本<br><a href="https://link.segmentfault.com/?enc=SLkKpbdE4keXi1NPDXa3%2Fg%3D%3D.bvFb9MUM71DrC6qJVRvPaPrDkB1R56Xe2MjUo545Vtme2%2B5Swv%2B4jO8HSC8VjLkY" rel="nofollow">https://github.com/jimengio/q...</a></p>
<h3>Barcode Detector 方案(兼容性问题)</h3>
<p>Google 提供的方案, 内置 API, 但是只有 Chrome 支持, 而且手动测试发现需要 80 版本.</p>
<p><a href="https://link.segmentfault.com/?enc=tSyK6YyZaJcX4gV%2BYSChxg%3D%3D.zG07uoxvEkVJQVRM7Bdy2bWvS8o0AZhFnkZTOpTmAt8bS4NS9UDeLgKe0ZGdclRJ" rel="nofollow">https://web.dev/shape-detection/</a><br><a href="https://link.segmentfault.com/?enc=I98ScSGczdB4eVeJHGyklw%3D%3D.zMdP0D25rXJH%2BZYMSMONEMbMKXk6wAasvZ%2F8beZjAlyLQ5tA3ZkCSe9gXNGwHaFNhVYMMQCtrXIca5eMiLvZ4vKw7djZKIDAQram%2F298wpg%3D" rel="nofollow">https://wicg.github.io/shape-...</a></p>
<p>这个方案不完整, 需要添加 API, 手动处理前面抓取图片的部分, API 已经比较完善的,</p>
<p><a href="https://link.segmentfault.com/?enc=yr1UjAjwHMM7jPxFqo6pIw%3D%3D.dnGMJPtCW%2BrIqi6wsU7TiXho%2BjFojag4jc2SkU9H8NR%2FATzaV8bgxScW%2BonKK5mHpiV9%2FbfeHi74TFgbDNOfhbJsed2LCFUeNP%2BZ4atFcTc%3D" rel="nofollow">https://developers.google.com...</a><br><a href="https://link.segmentfault.com/?enc=qy6H0OhQwM%2BfezsC667HuA%3D%3D.xTxau88d57SjxtdAz1Gs0LDCwGVmu9ckXOWJKtoCZNB7mdJCbT6%2BNCmOpdxTNmA1CdjcJw1JbcaI0JOAQeyLrUoy2%2FZiNZsf%2FYKWWFV%2FEbr3zQxVFNghVabyfsPVFhxP" rel="nofollow">https://medium.com/@immanubha...</a></p>
<p>整理了一个 Demo<br><a href="https://link.segmentfault.com/?enc=zrcoZG%2BWThWP9%2BlaOAlwWQ%3D%3D.cKInP7xysQSNLe55PMJxXZDck80%2FjaqUbI8557RRpcBptpJckA6LYiPkWLdRnQopILFxTkw0X%2Ft9tFXDs%2FAQ1Q%3D%3D" rel="nofollow">https://github.com/jimengio/j...</a></p>
<h3>zbar + WebAssembly 方案(继续调研)</h3>
<p><a href="https://link.segmentfault.com/?enc=BNPmCUmot%2BaVrAdBLJW1Og%3D%3D.pco9FY%2F8AFGpWQuMtrx4HT2gUbE7Kfru67mMSAnnUjqoIRSb8v21JyjaGrqifVFjamejCf6NI%2BahQiWEYB%2BVM32MgePedtp1gtYd1OqxMKA%3D" rel="nofollow">https://barkeywolf.consulting...</a><br><a href="https://link.segmentfault.com/?enc=wUH5rqNpIQTBxyOFlwZDZA%3D%3D.aBpOcsaOz9eO1p3OqEq%2BSpw3gjwzwNL0TSUxKaJ8of64hetFhgJEvWfeNfkG%2FEAcvT3sEJTMbthzyzJVI%2BeajA%3D%3D" rel="nofollow">https://github.com/jjhbw/barc...</a></p>
<p>这个方案同时能识别条形码跟 QR Code, 这一点比较好.</p>
<p><a href="https://link.segmentfault.com/?enc=LK%2BmRBNEyPckeux%2BLNrx6A%3D%3D.CgpvaimvQ3WIdXzxyq6HgwDnxlFE6lPBY1JwKmwagwJo6r%2BwLULyW5mKT9n7%2BifCBnFEEipCtBbAct4hKTYALjMGm0gRspGG%2Fw5GnyIAUrk%3D" rel="nofollow">https://barkeywolf.consulting...</a></p>
<p>不过 issue 里的问题似乎文章作者也没给修掉, 是否在维护也要考虑.</p>
<p><a href="https://link.segmentfault.com/?enc=Qi9%2FzmSkY1Hdzp5%2FM7kmqw%3D%3D.ODwlU8NVAPTLsiw4uj4aE60sWK1ps5H%2Fe9WYsnmfXoHgqrk7PU1X0%2B32X2AnsIbpWo9O893LtbmfmoIJvmcaPw%3D%3D" rel="nofollow">https://github.com/jjhbw/barc...</a></p>
<p>按着教程试了一下, 需要 docker 里跑 C 编译. 距离打包到 Webpack 里用还有距离.</p>
<h3>其他</h3>
<p>暂时先 Quagga2 吧. C 比较陌生, 要不然应该试一下 zbar 的, 远期看需要.</p>
<hr>
<p>关于积梦前端的模块和工具可以查看我们的 GitHub 主页 <a href="https://link.segmentfault.com/?enc=yB41h1ZMawXec5deizgqBg%3D%3D.0b8Diz9lFk4nFvNgEpVmggmGh5GPLRlIW34%2FGY7Vgps%3D" rel="nofollow">https://github.com/jimengio</a> .<br>招聘的计划和条件也在 GitHub 上有给出 <a href="https://link.segmentfault.com/?enc=CNt8QFyyVOzQUitQYUNU%2Bg%3D%3D.GSVzvkx3Vn2KV7aa49F4%2BFKyTp7FjeJmU%2BGixZ5Qefjf4S%2F%2BF6SplHVsEqrxVzxE" rel="nofollow">https://github.com/jimengio/h...</a> .</p>
ruled-router 生成路由类型细节记录
https://segmentfault.com/a/1190000020845238
2019-10-29T12:39:46+08:00
2019-10-29T12:39:46+08:00
题叶
https://segmentfault.com/u/tiye
1
<p><a href="https://link.segmentfault.com/?enc=En7v31SOWGED0mIw7ELBUg%3D%3D.T%2FJ5NyhKzQ3vmZuZFYFJn7zO5LkzHJrKZ1PHl092A62hkYQO5VAiIvmxtTjBAvCy" rel="nofollow">ruled-router</a> 是我们(积梦前端)定制的路由方案, 另外强化了类型方面,<br>之前的介绍可以看文章: <a href="https://segmentfault.com/a/1190000019143914">积梦前端的路由方案 ruled-router</a>.</p>
<h3>关于跳转方法的类型</h3>
<p>路由生成部分, 大致上就是对于规则:</p>
<pre><code class="json">[
{
"name": "a",
"path": "a",
"next": [
{
"name": "b",
"path": "b/:id"
}
]
}
]</code></pre>
<p>会通过脚本生成路由的调用方法, 现在的生成结果是:</p>
<pre><code class="ts">export let genRouter = {
a: {
name: "a",
raw: "a",
path: () => `/a`,
go: () => switchPath(`/a`),
b: {
name: "b",
raw: "b",
path: (id: string) => `/a/b/${id}`,
go: (id: string) => switchPath(`/a/b/${id}`),
},
},
};</code></pre>
<p>这样可以通过调用方法来进行路由跳转,</p>
<pre><code class="ts">genRouter.a.b.go(id)</code></pre>
<p>这个步骤, 是有类型支持的. TypeScript 会检查整个结构, 不会有错误的调用.<br>也就是说, 所有的调用, 按照这个写法, 不会导致出现不符合路由规则的路径.<br>整个实现模块维护在 <a href="https://link.segmentfault.com/?enc=p9wLV%2FSc3t87Y5w2VCE6Ww%3D%3D.DJDK%2FLxLk%2BX8EoKqo%2B6A29Hs7FsctjSdEF67JQiRcGROAMccwS0Ft2MtlThk8I1hsX2VCCioATMLhriYHyCnKw%3D%3D" rel="nofollow">https://github.com/jimengio/r...</a> .</p>
<h3>解析结果的类型问题</h3>
<p>现在的短板是在解析解析结果的类型上面, <a href="https://link.segmentfault.com/?enc=s7D8QHY27Dvw3ofHharuTQ%3D%3D.jAPzPJg7aiE0Wg4fyoKK058ZBOVJAe1YMsHNTMEkjqtXcFqrXJJNUV1XIF%2Fy6YVX" rel="nofollow">回顾一下 ruled-router 解析的结果</a>,<br>对于路径:</p>
<pre><code class="ts">/home/plant/123/shop/456/789</code></pre>
<p>按照路由规则做一次解析,</p>
<pre><code class="ts">let pageRules = [
{
path: "home",
next: [
{
path: "plant/:plantId",
next: [
{
path: "shop/:shopId/:corner"
}
]
}
]
}
];
</code></pre>
<p>会得到一个 JSON 结构,</p>
<pre><code class="json">{
"raw": "home",
"name": "home",
"matches": true,
"restPath": ["plant", "123", "shop", "456", "789"],
"params": {},
"data": {},
"next": {
"raw": "plant/:plantId",
"name": "plant",
"matches": true,
"restPath": ["shop", "456", "789"],
"params": {
"plantId": "123"
},
"data": {
"plantId": "123"
},
"next": {
"raw": "shop/:shopId/:corner",
"name": "shop",
"matches": true,
"next": null,
"restPath": [],
"data": {
"shopId": "456",
"corner": "789"
},
"params": {
"plantId": "123",
"shopId": "456",
"corner": "789"
}
}
}
}</code></pre>
<p>这个 JSON 结构当中部分字段是固定的, 部分是按照规则定义的参数,<br>如果用一个类型来表示, 就是:</p>
<pre><code class="ts">interface IParsedResult<IParams, IQuery></code></pre>
<p>这也是我们以往的写法. 这个写法比较稳妥, 但是问题就是书写麻烦,<br>路由比较多, 需要手写的 <code>IParams</code> <code>IQuery</code> 比较多, 也难以维护.</p>
<h3>当前尝试生成路由的方案</h3>
<p>对于这个问题, 我想到的方案, 主要是能不能像前面一样把类型都生成出来,<br>大致想到的是这样一个方案, 生成一棵嵌套的路由的树,<br><a href="https://gist.github.com/chenyong/473b1f34302492adc2d73d81d180667f">https://gist.github.com/cheny...</a><br>我需要这棵树满足两个需求,</p>
<ul>
<li>能得到一个完整的路由, 其中的 <code>next: A | B | C</code> 能罗列所有子路由类型,</li>
<li>我能通过 <code>x.y.z.$type</code> 来获取其中一棵子树, 因为子组件需要具体一个类型,</li>
</ul>
<p>这个方案最重要的地方就是需要 VS Code 能推断出类型进行提示,<br>经过调整以后, 得到一个可用的方案, 基于这样的规则,</p>
<pre><code class="json">[
{
"path": "a",
"queries": ["a"],
"next": [
{
"path": "b",
"queries": ["a", "b"]
},
{
"path": "d"
}
]
}
]</code></pre>
<p>生成的类型文件的是这样:</p>
<pre><code class="ts">export type GenRouterTypeMain = GenRouterTypeTree["a"];
export interface GenRouterTypeTree {
a: {
name: "a";
params: {};
query: { a: string };
next: GenRouterTypeTree["a"]["b"] | GenRouterTypeTree["a"]["d"];
b: {
name: "b";
params: {};
query: { a: string; b: string };
next: null;
};
d: {
name: "d";
params: {};
query: { a: string };
next: null;
};
};
}</code></pre>
<ul><li>顶层的路由</li></ul>
<p>页面首先会被解析, 得到一个 <code>router</code> 对象</p>
<pre><code class="ts">let router: GenRouterTypeMain = parseRoutePath(this.props.location.pathname, pageRules);</code></pre>
<p><code>router</code> 的类型是 <code>GenRouterTypeMain</code>, 这个类型是顶层的类型,<br>这个例子当中只有一个顶级路由,</p>
<pre><code class="ts">export type GenRouterTypeMain = GenRouterTypeTree["a"];</code></pre>
<p>实际当中更可能是多个可选值, 就像这样</p>
<pre><code class="ts">type GenRouterTypeMain = GenRouterTypeTree["a"] | GenRouterTypeTree["b"] | GenRouterTypeTree["c"];</code></pre>
<ul><li>组件使用的子路由</li></ul>
<p>子组件当中, <code>props.router</code> 的类型对应的是子树的某一个位置,<br>这里的 <code>next</code> 因为用了 Union Type, 不能直接引用其中某个 case,<br>就需要通过另一个写法, 从数据的路径上直接通过类型访问, 比如:</p>
<pre><code class="ts">GenRouterTypeTree["a"]</code></pre>
<p>更深层的子组件的类型, 比如嵌套的第二层, 就需要用:</p>
<pre><code class="ts">GenRouterTypeTree["a"]["b"]</code></pre>
<p>不过这个在组件定义当中并不直接是拿到, 因为在 props 可能无法确定类型,<br>就需要通过父级的 <code>next</code> 来访问, 具体是一个 Union Type:</p>
<pre><code class="ts">let InformationIndex: FC<{
router: GenRouterTypeTree["a"]["next"] }
// next type
// GenRouterTypeTree["a"]["b"] | GenRouterTypeTree["a"]["d"]
> = (props) => {
// TODO
}</code></pre>
<ul><li>配合 VS Code 做类型推断</li></ul>
<p>为了能让 VS Code 从 <code>next</code> 推断出类型, 需要同 switch 语句判断,</p>
<pre><code class="ts">if (props.router) {
switch (props.router.name) {
case "b": // TODO, router: GenRouterTypeTree["a"]["b"]
case "d": // TODO, router: GenRouterTypeTree["a"]["d"]
}
}</code></pre>
<p>效果大致上,</p>
<ul>
<li>
<code>case</code> 后面的字符串在一定程度上可以自动补全和类型检查,</li>
<li>
<code>case</code> 后面, router 类型确定了, <code>params</code> 和 <code>query</code> 就能有字段的提示和检查了,</li>
<li>如果内部有子组件 <code><A router={router.next} /></code>, <code>router.next</code> 会被类型检查.</li>
</ul>
<p>当然这些主要还是提示的作用, 并不是完全跟 router 对应的类型, 不然结构会更复杂,<br>我试着在已有的组件项目当中做了尝试, 包括比链接更大的项目, 基本是可用的,<br><a href="https://link.segmentfault.com/?enc=N8ZkId0QIxOOUoTu2Ta3cw%3D%3D.iwqoV7u7OCZJ%2F1TL8LJdiLmJ90EH7HH%2B9opcJHSvmAN2eeoSP%2FzC7N0cFe6a11h1wrXLX%2BBKSPBOQd5FF8fmPg%3D%3D" rel="nofollow">https://github.com/jimengio/m...</a></p>
<h3>其他</h3>
<p>目前来说, 能对项目路由进行检查, 就算是达到了最初的类型的目标,<br>至少能够保证, 开发当中, 使用生成的路由, 能提示和检查 <code>params</code> <code>query</code> 中的字段,<br>并且提交到仓库的代码, CI 当中能检查到参数, 做一些质量的保证.</p>
<p><code>case</code> 当中能够提示字符串, 算是意料之外的一个好处吧.<br>不过这个也要注意, VS Code 推断的能力有限, 只能用 switch 这个简单的写法,<br>再复杂一些, 比如嵌套了表达式, 或者往子路由数据再判断, 就推断不出来了.</p>
<p>当前比较担心的是项目当中出现深度嵌套的路由, 加上字段名称长, 整体会非常长:</p>
<pre><code class="ts">GenRouterTypeTree["a"]["d"]["e"]["f"]["g"]</code></pre>
<p>由于我们最大的项目当中曾在深达 6 层的路由, 不能不担心会出现超长的单行路由...<br>后面再想想有没有什么办法继续做优化..</p>
<hr>
<p>其他关于积梦前端的模块和工具可以查看我们的 GitHub 主页 <a href="https://link.segmentfault.com/?enc=aBUXEAZNxR9yxvXBUcVq4Q%3D%3D.9PqYSgE8mcyfDE6Sh%2B2VHNNlEKK5hF6dq6oJtW4wRGg%3D" rel="nofollow">https://github.com/jimengio</a> .<br>目前团队正在扩充, 招聘文档见 GitHub 仓库 <a href="https://link.segmentfault.com/?enc=%2BFyOH%2FfudadHkUDZZrlm4g%3D%3D.WMdt1Q474e6uO2IpGY4sQ8FSldcqcP19Y3Qqlq%2FVnMuu3DHmoI3YlAaD6pz6elUp" rel="nofollow">https://github.com/jimengio/h...</a> .</p>
Respo 增加 Effects 功能支持
https://segmentfault.com/a/1190000020806364
2019-10-26T15:27:35+08:00
2019-10-26T15:27:35+08:00
题叶
https://segmentfault.com/u/tiye
2
<p>补了一些关于 Respo Effects 的文档, 英文细节有点吃力,<br><a href="https://link.segmentfault.com/?enc=NfBqoD8cOPsw3VhZi4HA2w%3D%3D.6eN6NG%2Fn8YgAA4e8EPwhQh%2Bg7r%2FbY0Y30kVgL5l34A%2FZZOY%2FJA7sWLkGhHnAIGwR" rel="nofollow">https://github.com/Respo/respo/wiki/defeffect</a><br>关于 Respo 的设计思路和功能取舍, 这边可以再描述详细一些.</p>
<h3>新增的写法</h3>
<p>比方说有个组件要增加副作用,</p>
<pre><code class="clojure">(defcomp comp-a [x y z]
(div {}))</code></pre>
<p>这次更新以后, <code>respo.core</code> 当中新增了一个 <code>defeffect</code> 的宏用来定义副作用,<br><code>defeffect</code> 需要的不单单是多个参数, 而且是很多组参数.</p>
<pre><code class="clojure">(defeffect effect-a [x y] [action el *local]
(println "effects"))</code></pre>
<p><code>[x y]</code> 当然就是参数了. 框架渲染过程当中会自动插入参数的值,<br>另外框架会插入 <code>action</code> 表示 <code>:mount</code> <code>:update</code> <code>:unmount</code>,<br>以及 <code>el</code> 是组件根节点, 也是由框架获取.</p>
<p>这个宏的实现, 就是把代码转换成一个函数, 函数返回的是个 HashMap,</p>
<pre><code class="clojure">(defmacro defeffect [effect-name args params & body]
`(defn ~effect-name [~@args]
(merge respo.schema/effect
{:name ~(keyword effect-name)
:args [~@args]
:coord []
:method (fn [[~@args] [~@params]]
~@body)})))</code></pre>
<p>上面定义得到的 <code>effect-a</code> 就是一个函数, 可以通过 <code>(effect-a x y)</code> 调用,<br>在组件当中使用的时候, 就是把返回值变成数组, 在数组当中加上副作用</p>
<pre><code class="clojure">(defcomp comp-a [x y z]
[
(effect-a x y)
(div {})
])</code></pre>
<p>后面就依靠 Respo 的渲染代码, 内部进行判断, 对 effect 进行处理.</p>
<p>由于 effect 没有直接区分开不同的生命周期, <code>action</code> 使用时需要自行判断,</p>
<pre><code class="clojure">(case action
:mount (do)
:update (do)
:unmount (do)
(do))</code></pre>
<p><code>*local</code> 的存在, 是为了应付可能存在的存储局部状态的需求.<br>比如在 mount 的时候创建的数据, 如果在 update 和 unmount 需要用到,<br>目前的设计当中, 就需要组件提供私有的状态用于传递.<br>需要注意, 这个 <code>*local</code> 实际上对应的 React 当中的 ref,<br>也就是说, 在 <code>*local</code> 上修改数据, 不会出发 rendering 的行为.</p>
<h3>以往的纯组件</h3>
<p>React 当中组件定义的方式比较简单,</p>
<pre><code class="clojure">(defcomp comp-a [x y]
(div {}
(div {} (<> "DEMO"))))</code></pre>
<p>然后会经过一次宏展开, 宏的实现是</p>
<pre><code class="clojure">(defmacro defcomp [comp-name params & body]
`(defn ~comp-name [~@params]
(merge respo.schema/component
{:args (list ~@params) ,
:name ~(keyword comp-name),
:render (fn [~@params]
(defn ~(symbol (str "call-" comp-name)) [~'%cursor] ~@body))})))</code></pre>
<p>上面的组件经过 <code>(comp-a x y)</code> 这样的调用之后, 会得到一个 HashMap,</p>
<pre><code class="clojure">{:name :comp-a
:args '(x y)
:render (fn [x y]
(defn call-comp-a [%cursor]
(div {}
(div {} (<> "DEMO")))))}</code></pre>
<p>可以看到其中没有实现生命周期的信息.<br>这个高阶函数在运行时会继续被处理, 添加所需的参数, 再被计算.</p>
<p>这个结构当中并没有预留跟 React 相似的组件生命周期,<br>而且也不适合用方法进行扩展, 所以比较难直接有 React class 组件那种写法.</p>
<h3>想法和尝试</h3>
<p>如果需要在组件当中支持副作用的, 至少要在组件的表上加上 effects 的位置,</p>
<pre><code class="edn">{:name :comp-a
:args '()
:render (fn [])
; add
:effects [(fn [])]}</code></pre>
<p>原先的 <code>defeffect</code> 的 API 写出来, 我大致确定了需要哪些参数,<br>比如 <code>[a b]</code> 参数, 界面渲染和更新当中使用,<br>然后是 <code>action</code> 用来判断生命周期, <code>el</code> 对应 React 当中的 DOM Ref 用.</p>
<p>有了 <code>defeffect</code> 之后我在考虑, 都是把 effect 插入在 DOM 树当中,<br>类似 <code>(div {} (effect-a x y) (div {}))</code> 这样,<br>但具体看了实现, 涉及到 DOM Diff 的实现有很多坑, 也就作罢了.<br>于是想怎样才能以兼容已有的写法的方式吧副作用插入进去.. 最简单就是数组了.<br>用数组的话, 可以插入多个 effect, 并且后续也有些许继续扩展的能力.</p>
<p>这套写法跟 React Hooks 比起来, 有不少的功能缺失.<br>特别是 Respo 当中, 基本没有运行渲染过程再 dispatch actions 的可能.<br>React 当中频繁有 componentDidMount 或者 useEffect, 在任何时候修改组件状态,<br>而且也没有限制在这种生命周期时 dispatch actions.<br>Respo 里不认可这样的做法, 这样会持续衍生出 actions 来.<br>特别是在 Time Traveling 的场景当中, 这种 actions 就是破坏性的,<br>一旦切换到旧的某个 action 导致新的 actions 被触发, 状态就未必一致了.</p>
<p>整体考虑为了热替换方便, 组件局部状态的变化, 是不鼓励的.<br>目前 Respo Effects 算是出现在早期状态, 后面也可能再调整.</p>
<h3>其他</h3>
<p>不管怎样, 此前 Respo 为了实现纯的渲染, 没有做 effects,<br>导致跟 JavaScript 生态已有的一些用法不能轻松衔接.<br>现在加上了 Effects, 那些东西终于可以进行尝试了.</p>
<p>Respo 最初版本是 2016 年初开始的, 年中基本完成,<br>这么多年了, 用的人少, 这方面的需求也没有太大的问题, 因为场景也有限.<br>我个人觉得 Effects 不会有太多的需要. 还是以小范围扩展功能位置.</p>
[小组分享] React 当中性能优化手段整理
https://segmentfault.com/a/1190000020769066
2019-10-22T19:35:11+08:00
2019-10-22T19:35:11+08:00
题叶
https://segmentfault.com/u/tiye
12
<blockquote>内部小组分享底稿.</blockquote>
<h4>回顾一下 React</h4>
<ul>
<li>class 组件的优化</li>
<li>useMemo 提供的优化</li>
<li>React.memo 优化</li>
<li>useCallback 优化</li>
<li>避免 render 当中的 DOM 操作</li>
</ul>
<hr>
<h4>class 组件的优化</h4>
<p>通过判断减少数据变化触发的重新渲染, 以及之后的 DOM diff</p>
<pre><code class="ts">shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}</code></pre>
<hr>
<h4>JavaScript 对象引用问题</h4>
<p>函数式语言当中, 语言设计允许两个对象一样, 举例 Clojure:</p>
<pre><code class="clojure">(= {:a 1} {:a 1}) ; true
(identical? {:a 1} {:a 1}) ; false</code></pre>
<p>递归匹配, 性能并不高.</p>
<p>JavaScript 对象基于引用传值, 比较单一</p>
<pre><code class="ts">{a: 1} === {a: 1} // false</code></pre>
<p>大体方案, 通过手动维护, 让相同的数据尽量保证引用一致, 控制性能.</p>
<pre><code class="ts">function updateColorMap(colormap) {
return {...colormap, right: 'blue'};
}</code></pre>
<hr>
<h4>useMemo 优化</h4>
<p>每个函数体当中生成的对象都会有新的引用, <code>useMemo</code> 可以保留一致的引用.</p>
<pre><code class="ts">const myObject = useMemo(() => ({ key: "value" }), [])</code></pre>
<p>注意: 用花括号直接写对象基本上就是新的引用了,</p>
<pre><code class="ts">{}
{a: 1}
{...obj}</code></pre>
<p>一般组件内部不变的对象, 都是从 state, ref, 再或者组件外全局有一个引用.</p>
<hr>
<h4>React.memo 优化</h4>
<p>判断参数是否改变, 如果没有改变, 就直接复用已有的组件, 不重新生成:</p>
<pre><code class="ts">const MyComponent = React.memo(function MyComponent(props) {
/* only rerenders if props change */
});</code></pre>
<p><code>React.memo</code> 有第二个参数, 用于自定义判断的规则:</p>
<pre><code class="ts">const MemoItem = React.memo(Item, (prevProps, nextProps) => {
if (prevProps.item.selected === nextProps.item.selected) {
return true;
}
return false;
});
</code></pre>
<hr>
<h4>useCallback 优化</h4>
<p>使用 <code>React.memo</code> 包裹组件:</p>
<pre><code class="ts">let Inner: FC<{
onClick: () => void
}> = React.memo((props) => {
return <div>
<span>inner</span>
</div>;
});</code></pre>
<p>使用 <code>useCallback</code></p>
<pre><code class="ts">let Outer: FC<{}> = React.memo((props) => {
const [counter, setCounter] = useState(0);
const onClick = useCallback(()=>{
setCounter(prevState => ++prevState)
},[]);
return <div>
<span>outer: {counter}</span>
<Inner onClick={onClick} />
</div>;
});</code></pre>
<hr>
<h4>避免 render 当中的 DOM 操作</h4>
<pre><code class="ts">let NewComponent: FC<{}> = React.memo((props) => {
let elRef = useRef<HTMLDivElement>()
// 错误写法
if (elRef.current) {
elRef.current.style.color = 'red'
}
return <div ref={elRef}></div>;
});</code></pre>
<p>DOM 发生改变的时候, 一般会有比较多后续的布局和 compose 计算去绘制新的界面.</p>
<p>特别是在脚本执行过程当中发生的话, 会对性能有明显影响.</p>
<p>脚本执行完再执行, 让浏览器自动处理(合并, 避免频繁 DOM 操作).</p>
<hr>
<h4>业务相关</h4>
<ul>
<li>immer 对优化方案的影响</li>
<li>Rex 组件当中优化的坑</li>
<li>路由相关的优化</li>
<li>性能调试</li>
</ul>
<hr>
<h4>Immer 对优化方案的影响</h4>
<pre><code class="ts">let a = {}
let b = produce(a, draft => {
draft.b = 1
})
a === b // false</code></pre>
<p>如果数据不发生改变, 直接用原始数据.</p>
<p>(Hooks API 之后, 数据被拆散了, 可以减少 immer 的使用.)</p>
<hr>
<h4>Rex 当中优化的相关</h4>
<p>class 组件, 高阶组件当中自动做了基础的优化.</p>
<pre><code class="js">shouldComponentUpdate(nextProps: IRexDataLayerProps, nextState: any) {
if (!shallowequal(nextProps.parentProps, this.props.parentProps)) return true;
if (!shallowequal(nextProps.computedProps, this.props.computedProps)) return true;
return false;
}</code></pre>
<p>Hook API, 没有中间一层组件, 直接触发当前组件更新, 存在性能问题.(还要考虑优化方案)</p>
<pre><code class="ts">let contextData = useRexContext((store: IGlobalStore) => {
return {
data: store.data,
homeData: store.homeData,
};
});</code></pre>
<p>业务当中一般可以接受, 因为数据通常都是在更新的. 新能敏感场景需要额外考虑.</p>
<hr>
<h4>ruled-router 提供的优化</h4>
<pre><code>/home/plant/123/shop/456/789</code></pre>
<p>解析为</p>
<pre><code class="json">{
"raw": "home",
"name": "home",
"matches": true,
"restPath": ["plant", "123", "shop", "456", "789"],
"params": {},
"data": {},
"next": {
"raw": "plant/:plantId",
"name": "plant",
"matches": true,
"restPath": ["shop", "456", "789"],
"params": {
"plantId": "123"
},
"data": {
"plantId": "123"
},
"next": {
"raw": "shop/:shopId/:corner",
"name": "shop",
"matches": true,
"next": null,
"restPath": [],
"data": {
"shopId": "456",
"corner": "789"
},
"params": {
"plantId": "123",
"shopId": "456",
"corner": "789"
}
}
}
}</code></pre>
<p>生成对象保存起来, 路由发生变更时再重新解析. 这样对象引用一般保持一致.</p>
<hr>
<h4>性能优调试</h4>
<p>DevTools</p>
<p><a href="https://link.segmentfault.com/?enc=AvNKuC4IcfWtIuWRAjC2Ew%3D%3D.2OEwlQTIN9PRVFq71ej%2FSlsR71mRF6Ti8tP8WgISRaVhDzUZRNozobHENh3%2B5SoHaw8AJ8iEHYWgE3LczRN4b6FZlYYJ5thkqT9lAZE6Yxw%3D" rel="nofollow">https://developers.google.com...</a></p>
<p>React DevTools</p>
<p><a href="https://link.segmentfault.com/?enc=JSIxsnIB%2BlUOHAO857mDsQ%3D%3D.01FHrc9RuBLU88BimkTfoJ0nBvupZY7FmzxwH339R9jbvrVX2wUjJTQdq50szWR8Y7kzqQS%2BKb7R%2BgXihqijHQ%3D%3D" rel="nofollow">https://www.debugbear.com/blo...</a></p>
<hr>
<h4>其他</h4>
<p>官方推荐性能优化方案...</p>
<p><a href="https://link.segmentfault.com/?enc=IGIYQ%2B7zUoj9ScmCCTzUdg%3D%3D.d4U3EwV%2BZIw4wIkDPcB4xO5OL%2BYJ61piYlKkKsZuk94UaRArIFAKBd%2FowJVSpoZbbC1w4VDPMoGK6l3W9wBSyw%3D%3D" rel="nofollow">https://reactjs.org/docs/opti...</a></p>
<hr>
<h4>实际遇到</h4>
<p>树形组件: 隐藏子树, 定制减少更新. (个人建议看情况自己实现, 通用组件一般都不好优化).</p>
<p><em>略</em></p>
<p>useMemo</p>
<p><em>略</em></p>
<p>Dropdown 的替换, 老版本 antd 的 bug(升级 <code>rc-select@9.0.3</code>).</p>
<p><em>略</em></p>
<p><a href="https://link.segmentfault.com/?enc=Fm1Uotyx1UrmZqLeU7GzuA%3D%3D.AhBZT40BW2w4VHRrBH3KYPW8BoYp%2FXfyAR8M1uNiN6t%2BiD9D2QAuHoWOSZDiZNVht4%2Fqtz55CPjabulR0X6Y0w%3D%3D" rel="nofollow">https://github.com/react-comp...</a></p>
<hr>
<h4>需要优化</h4>
<ul>
<li>form</li>
<li>table</li>
<li>...</li>
</ul>
<hr>
<h2>THX. QA.</h2>
<hr>
<p>其他关于积梦前端的模块和工具可以查看我们的 GitHub 主页 <a href="https://link.segmentfault.com/?enc=Nj%2BAnphYK7b9%2BWshdHCt%2Bw%3D%3D.3wId7Au84GRy5eZnKVBX5iM9Dq%2F8yDTRZDlIot7g4aI%3D" rel="nofollow">https://github.com/jimengio</a> .<br>目前团队正在扩充, 招聘文档见 GitHub 仓库 <a href="https://link.segmentfault.com/?enc=dL6ChQ908qULJFYVZo0JnA%3D%3D.Qpr3xc%2BJhPrAlgJwY%2B6HoTsl7AidHhqtwLnmlAtqMNPKoxL3l5cfQigQhLP6MavM" rel="nofollow">https://github.com/jimengio/h...</a> .</p>
TypeScript 用 Webpack/ts-node 运行的配置记录
https://segmentfault.com/a/1190000020650656
2019-10-11T15:04:51+08:00
2019-10-11T15:04:51+08:00
题叶
https://segmentfault.com/u/tiye
1
<p>公司项目代码是用 TypeScript 写的, 中间遇到有些代码不要放到 Node 里面去跑.<br>具体场景一些路由配置, 比较大的一块 JSON 数据定义在 TypeScript 里.<br>我另外有增加脚本, 基于这些 JSON 数据用来生成切换路由的函数.<br>这就需要运行 TypeScript 了, 而且可能包含一些额外的业务代码.</p>
<p>首先 Node 运行 TypeScript 有提供 ts-node 用来处理.<br>ts-node 会先编译 TypeScript 代码到 JavaScript, 再调用 Node 运行.<br>不过这个办法有一些问题, 一个是 TypeScript 定义的路径配置不成功,<br>另一个问题更麻烦点, 就是引用到的其他的浏览器端代码因为触发运行而引起报错.</p>
<h3>Webpack 打包 TypeScript Node 代码</h3>
<p>我先想到了一个相对省事的方案, 就是用 Webpack 对 TypeScript 进行打包.<br>打包完成以后输出 JavaScript 代码. 而浏览器代码打包进去, 但不一定运行.<br>由于 TypeScript 配置在 Webpack 当中引用有比较成熟的方案, 整个配置也写好:</p>
<pre><code class="js">module.exports = {
mode: "development",
target: "node",
entry: ["./example/gen-router.ts"],
output: {
filename: "gen-router.js",
path: path.join(__dirname, "../", distFolder),
},
devtool: "none",
module: {
rules: [
// 正常的 TypeScript 编译方式, 我这份是拷贝的.
{
test: /\.tsx?$/,
exclude: [/node_modules/, path.join(__dirname, "scripts")],
use: [
{ loader: "cache-loader" },
{
loader: "thread-loader",
options: {
workers: require("os").cpus().length - 1,
},
},
{
loader: "ts-loader",
options: {
happyPackMode: true,
transpileOnly: true,
},
},
],
},
],
},
// Node 模块, 写在 external 里面表明不需要进行打包. 注意 commonjs 前缀
externals: {
prettier: "commonjs prettier",
"@jimengio/router-code-generator": "commonjs @jimengio/router-code-generator",
fs: "commonjs fs",
path: "commonjs path",
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
modules: [path.join(__dirname, "example"), "node_modules"],
// 引用 Plugin 用于读取 tsconfig.json 文件的配置
plugins: [new TsconfigPathsPlugin({ configFile: path.join(__dirname, "../tsconfig.json") })],
},
};</code></pre>
<p>基于这个配置打包以后, TypeScript 的代码被打包好, 并且引用响应的 Node 模块.<br>运行就满足需求了.</p>
<p>这个方式对于其他的服务端渲染的 TypeScript 代码打包也是类似的.<br>一些特殊的依赖如果不好处理, 可以放在 Webpack 当中进行打包和映射, 得到 js.</p>
<h3>ts-node 运行</h3>
<p>Webpack 配置相对直接运行 TypeScript 来说会复杂一点, 所以还是 ts-node 简单.<br>在依赖少的项目当中, 我改成了用 ts-node 来进行编译运行. 配置如下</p>
<pre><code class="js">{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"sourceMap": true,
"noImplicitAny": false,
"noImplicitThis": true,
"strictNullChecks": false,
"moduleResolution": "node",
// Node 当前还没有支持直接运行 import/export 语法, 需要编译到 CommonJS
"module": "commonjs",
"target": "es2016",
"jsx": "react",
"lib": ["es2016"],
"types": ["node"],
"baseUrl": "./example/",
"paths": {
"models": ["./example/models"]
},
"plugins": []
}
}</code></pre>
<p>其实主要修改就 <code>commonjs</code> 那一行, 然后就是加上参数运行</p>
<pre><code class="ts">ts-node -P tsconfig-node.json -r tsconfig-paths/register example/gen-router.ts</code></pre>
<p>注意命令当中的 <code>tsconfig-paths</code>. 这里的 <code>-r</code> 是指定 register.<br>ts-node 是先进行编译再运行的, 但是引用的路径没有全都替换掉.<br>比如我在 <code>tsconfig.json</code> 里设置了 <code>baseUrl</code> 然后内部引用是简写的, <code>a/b/c</code>,<br>拿到 Node 本身去运行的时候是不知道这个 <code>a/b/c</code> 对应到哪里,<br>所以 <code>tsconfig-paths/register</code> 就提供 Node 运行时的方案, 动态查找依赖.<br>至少这样 Node register 改写以后, 查找模块就能正确进行了.</p>
<h3>其他</h3>
<p>另外 TypeScript 编译 import 语法时会产生一个 <code>.default</code> 属性.<br>对于 CommonJS 的模块, 这个 <code>.default</code> 属性是多余的. 所以引用的写法要做调整.</p>
<pre><code class="js">import * as fs from "fs";
import * as path from "path";
import * as prettier from "prettier";</code></pre>
<p>这个可能跟 <code>tsconfig.json</code> 里其他的配置有关联, 我没继续深挖.</p>
<p>整体的代码参考 <a href="https://link.segmentfault.com/?enc=IXuXjQ97vTl%2Fs3eLlX0dMQ%3D%3D.r9B1JZu5RL9AHBMwlS3CY%2Bf9enhu3UJCEgJaUN9bOOlln7zx28vYbdK08%2FUTYtsN8juT0fMKkl6uYtotKgiwfA%3D%3D" rel="nofollow">https://github.com/jimengio/meson-form/pull/62/files</a></p>
<hr>
<p>其他关于积梦前端的模块和工具可以查看我们的 GitHub 主页 <a href="https://link.segmentfault.com/?enc=jVENGDVM%2FTjr%2Fvk3CZqahQ%3D%3D.3TlHyVcxhwMounfa39z2gb%2Fph8PKf8%2FZrmyhwv8TQKc%3D" rel="nofollow">https://github.com/jimengio</a> .<br>目前团队正在扩充, 招聘文档见 GitHub 仓库 <a href="https://link.segmentfault.com/?enc=FWwUJPqwUkLMeobYpORITA%3D%3D.S%2BlmY0iD4OcURls5ON4c%2BmxTpsrwiMmzr0RtF6XqxOqIgH96x6%2BxsZFXwVraoBTv" rel="nofollow">https://github.com/jimengio/h...</a> .</p>
font-carrier 生成字体操作记录
https://segmentfault.com/a/1190000020506440
2019-09-26T19:31:35+08:00
2019-09-26T19:31:35+08:00
题叶
https://segmentfault.com/u/tiye
1
<p>主要是公司内部的 SVG 制作的图标, 提供网页当中使用的字体.<br>代码仓库在 <a href="https://link.segmentfault.com/?enc=7KCTOPShZ4YgjuNU%2B85FoQ%3D%3D.451hEbO6SJ9%2FDowaih8pPv99rZWjPROVPZ5RvRDvENOlWQ8D6WyKJNinmlTRapke" rel="nofollow">https://github.com/jimengio/j...</a></p>
<h3>字体生成步骤</h3>
<p>从 SVG 文件到可以给前端用的 npm 模块, 主要经过:</p>
<ul>
<li>SVG 文件使用第三方模块生成字体文件(ttf, svg, woff...)</li>
<li>生成图标跟 CSS 的映射</li>
<li>生成 TypeScript 组件当中使用枚举类型</li>
<li>打包更新 npm 模块</li>
</ul>
<p>生成字体文件跟 CSS 映射我们之前用的是 <a href="https://link.segmentfault.com/?enc=SkoLwSkBa6s31VL%2FYK4%2F2g%3D%3D.rADNeaAX85KePyNmiaBt46U6seupI5IkxKQbjgobxSrKeIYBoWqxP95hKabTGXn%2Bj%2BpYIXgg22WD6iVnf5Mt5Q%3D%3D" rel="nofollow">webfonts-generator</a>,<br>不过这个模块已经停止维护了, npm 上的版本还是有 bug 的, 主分支的还好一点.<br>考虑到使用不方便, 一致在寻找替代方案. 在一丝的指导下切换到了 <a href="https://link.segmentfault.com/?enc=lwrlWaOVTfT%2FV5lU8vbRwQ%3D%3D.SfOKdLL7uOyI%2Bx4XLJ%2BXwcHYwnJdq5KZxEW%2FFiib86YVrhHwFXTklI2Po2LK0BVI" rel="nofollow">font-carrier</a>.</p>
<p>font-carrier 可以生成字体, 以及基础的 CSS 文件.<br>不过跟 webfonts-generator 不一样的是生成的 CSS 不要 class 来区分图标,<br>而是用 HTML 当中 utf8 字符直接跟图标字体对应... 当然原理跟以前一样..</p>
<pre><code class="html"><i
dangerouslySetInnerHTML={{ __html: `&#${fontsDict[props.name]}` || `NONE:${props.name}` }}
></i></code></pre>
<p>生成字体文件的代码就是调用 font-carrier 的 API,<br>另外自己记录了一个 <code>dict</code> JSON 对象, 用来存储码表...</p>
<pre><code class="coffee">initialFontValue = 0xe000
String.fromCharCode(initialFontValue)
fonts = fontCarrier.create()
dict = {}
icons.forEach (icon) ->
initialFontValue += 1
char = String.fromCharCode initialFontValue
fonts.setSvg(char, fs.readFileSync("./svg/#{icon}.svg").toString())
dict[icon] = initialFontValue
fonts.output
path: './src/fonts/jimo'</code></pre>
<p>然后主要是生成类型文件的工作.. 基于 <code>dict</code> 数据生成 <code>enum</code>, 基本够用.</p>
<h3>遇到的坑</h3>
<p>使用 font-carrier 过程当中有遇到一些问题, 联系维护者解决掉了,</p>
<ul><li>图标形状奇怪</li></ul>
<p>遇到一个圆弧反向的问题, 本来不知道怎么下手, 用 Glyphs 看了 TTF 文件的线条,,<br>注意到所有的线条跟原先的 SVG 在 Sketch 里面刚好反向了, 就怀疑是转的问题,<br>SVG 的 arc 圆弧有一个 SWEEP 值, 表示圆弧的方向, 就觉得是这个用错了.<br>维护者排查了一下, 是已知的问题, 在 svgpath 模块当中已经解决:<br><a href="https://link.segmentfault.com/?enc=KP%2FVlcvYh8I3PugMBU8cUw%3D%3D.jMoYmVhi%2Bkk4tthkx3BBQRxASSABMj8ntiW%2Ff8QMZ3XTROE2Tgyz7KulYu7E9YAk" rel="nofollow">https://github.com/fontello/s...</a><br>最后靠升级依赖的版本解决掉了, 按说后续不会再遇到.</p>
<ul><li>图标居中出现问题</li></ul>
<p>图标对应的是 UTF8 的字符, 最开始我选择的数值比较小, 主要跳过常用的码位.<br>不过当时出现了问题, 就是图标左右居中不正常, 有很多明显往左边偏移...<br>排查以后原因是选择的码位范围有问题, 刚好命中了一些奇怪的字符...<br>维护者推荐的码位范围是 <code>0xe000</code> 以上, 我估计对应的是空白的 UTF8 码位. 解决了.</p>
<ul><li>曲线闭合问题</li></ul>
<p>从接手项目的时候就提到了 SVG 的图标需要曲线闭合,<br>不是完全懂什么意思, 估计是路径要求闭合, 方便字体填充颜色之类的.<br>设计师没处理好的话, 图标是显示空白的. 用 <a href="https://link.segmentfault.com/?enc=%2B%2BDDlVgEslupp3haOFh58g%3D%3D.qQQ61ZhcOhofV8%2FTHx2uNWCWzFwFDHfZGORAO5pl7I4%3D" rel="nofollow">https://iconfont.cn</a> 可以验证.</p>
<h3>其他</h3>
<p>另外一丝推荐的方案是我们设计师直接用 <a href="https://link.segmentfault.com/?enc=GazjoSuU%2B47HGMGLHHsR8Q%3D%3D.QTW6w5lH3LvRZbJ6xRfCgAxYoHwtbMpaXGmIJvuTDJg%3D" rel="nofollow">https://www.iconfont.cn</a> 维护.<br>工具上可以提供 CSS 还有码位的导出, 比起我们手工处理要省事一点.<br>没想清楚, 后续如果有契机而且设计师可以维护的话, 考虑是否迁移过去.</p>
<hr>
<p>其他关于积梦前端的模块和工具可以查看我们的 GitHub 主页 <a href="https://link.segmentfault.com/?enc=QMACB3ME%2BLZAuVqnyYdZew%3D%3D.2SzyScPiQtcSEEco7hyX81%2B1fegmYJej5%2F4YFmvFOIo%3D" rel="nofollow">https://github.com/jimengio</a> .<br>目前团队正在扩充, 招聘文档见 GitHub 仓库 <a href="https://link.segmentfault.com/?enc=alXFOtnCNTfW66Qb5Jpf8Q%3D%3D.hSIdyKP%2BZw7%2ByyfZK0aRUNGE5a4PR99PJXZVnU4mnFsiCOWhOdxbzLK%2BHoYKQIgC" rel="nofollow">https://github.com/jimengio/h...</a> .</p>
Linaria 替换 Emotion 操作记录
https://segmentfault.com/a/1190000020500927
2019-09-26T12:24:48+08:00
2019-09-26T12:24:48+08:00
题叶
https://segmentfault.com/u/tiye
2
<blockquote>由于 linaria 已经没有活跃维护了, 文章里的方案不推荐使用.</blockquote>
<hr>
<p><a href="https://link.segmentfault.com/?enc=SH%2FuahjlIUZvLKVnp%2BYHnQ%3D%3D.qulZxK29IAeSAAIaTD3stm4YJB4uvfsiL4KJv6daDbaWdruk2UjbJPXewvgj1F%2BU" rel="nofollow">Emotion</a> 和 <a href="https://link.segmentfault.com/?enc=f%2FcycW3X3sxkPwxpPbb6Wg%3D%3D.U9SJZAIbXQt3GRaELrX1H1VYjer59DAXkHqSxugF%2FtKNdl4tJjsU1vgQQ22cZQtC" rel="nofollow">Linaria</a> 是两个 CSS in JS 方案, API 相近.<br>项目里有特殊的场景, 希望能减小体积, 我们一贯基于 Emotion 比较大,</p>
<p><img src="/img/bVbybkG" alt="clipboard.png" title="clipboard.png"></p>
<p>我们的 Emotion 现在都是跟着 JavaScript 走到, 没有做 CSS 分离,<br>之前尝试过生产环境分离 CSS, 但是因为 CSS 规则顺序问题, 效果不够可控,<br>加上不想在 TypeScript 后面套一层 Babel, 这条路也就不想走了.<br>所以目前打包的 JavaScript 里面就有图片上这么大的 Emotion 代码的体积.</p>
<h3>Linaria</h3>
<p><a href="https://link.segmentfault.com/?enc=Fvm6640FpSJNiskWVxJs6w%3D%3D.HizOsJwbHX%2FXijXqDuFW7qCJD8TWvMYS9Evw%2FBZv1madwm4ih1aWiMvJCwIOqnKG" rel="nofollow">justineo</a> 提醒我说 Emotion 可以用 <a href="https://link.segmentfault.com/?enc=hMeLBqICJhFTBBWykRuyCA%3D%3D.oNOM%2B8wtPDsJua57dfDa%2FeZwFl37sKbB2QwAUVV6V2PQSQsTIKRxabNKhg7Nt91s" rel="nofollow">Linaria</a> 替换, 我就去看了一下.<br>这个库的 API 基本上跟 Emotion 一致, 我们的写法大致用到了,</p>
<pre><code class="ts">import {css, cx} from "emotion"
let styleA = css`
color: red;
`;</code></pre>
<p>在 Linaria 当中基本上无缝替换了,</p>
<pre><code class="ts">import {css, cx} from "linaria"
let styleA = css`
color: red;
`;</code></pre>
<p>另外我关注的 prefix vendor, 在 linaria 里边一样也是有支持的.<br>其他的 API 应该也出跟着 style-components 的方案一致的.</p>
<p>跟 Emotion 相比, linaria 有个比较完善的静态的 css 分离的功能.</p>
<p><a href="https://link.segmentfault.com/?enc=7Cvatr8lGnW9J4huXqAkxg%3D%3D.kZ7UGL4l3BiV5ZyBWTIgltwFWVmO%2BgYuWSuaCyrbyoghULBQJ8O1%2BBM53flfadC78WMdodxBqDvDlAf2Oj5Z%2Fd2y9%2F%2BEJJoOxHke8%2BdQXcXA%2FYYCApFHOHmoKM0%2BeFlo" rel="nofollow">https://callstack.com/blog/ho...</a></p>
<blockquote>In short, CSS in JS libraries such as Emotion and Styled Components parse and apply your styles when your page loads in the browser, Linaria extracts the styles to CSS files when you build your project (e.g. with webpack), and the CSS files are loaded normally.<p>Emotion used to have a static extraction mode, which was limited in the sense that it doesn’t support interpolations in the CSS string and can’t use many of Emotion’s features. Linaria will evaluate the interpolations such as JavaScript variables and functions in the CSS string at build time.</p>
</blockquote>
<p>推测是去掉了 Emotion 某些动态的特性的支持把, 方便分离 CSS.<br>分离的过程是通过 Babel 完成的, 所以在特殊的场景当中我还是需要 Babel.</p>
<h3>相关配置</h3>
<p>这个修改增加了几个相关依赖,</p>
<pre><code> "babel-loader": "^8.0.6",
"linaria": "^1.3.1",
"core-js": "^2.6.5",
"string-replace-loader": "^2.2.0",</code></pre>
<p><code>core-js</code> 需要锁定版本, 过高的版本因为不兼容是出现了报错的.<br>string-replace 是为了处理依赖当中引用了 Emotion 的代码.<br>项目当中的代码我可以手动更改, 但是依赖组件因为其他项目复用, 不好直接改掉,<br>通过 Webpack 增加配置, 把 Emotion 的依赖都指向 Linaria(不一定需要):</p>
<pre><code class="coffee"> resolve: {
alias: {
emotion: "linaria"
}
},</code></pre>
<p>然后考虑到依赖代码引用得早, 还是要通过字符串替换把已有的引用提早替换掉:</p>
<pre><code class="coffee"> test: /\.js?$/,
use: [
{
loader: "string-replace-loader",
options: {
search: `"emotion"`,
replace: `"linaria"`
}
}
]</code></pre>
<p>其他的部分主要靠 Linaria 自己的工具配合 Babel 去搞了.</p>
<pre><code class="coffee">loader: require.resolve("linaria/loader")</code></pre>
<p>我这边 CSS 最终没有分离出去, 因为需求方面要打成一个 js 的包, 所以还是用 style 标签运行.<br>打包以后带着 <code>style-loader</code> 跟 <code>css-loader</code> 的代码会显得比较大一点,</p>
<p><img src="/img/bVbybn6" alt="clipboard.png" title="clipboard.png"></p>
<p>另外也要增加配置让 Webpack 去掉 Node 相关的代码, 不需要打包进来:</p>
<pre><code class="coffee">node: false,</code></pre>
<p>而 CSS 部分的代码, 被 Linaria 处理, 单独以 CSS 文件的形式存在了.</p>
<p><img src="/img/bVbybn9" alt="clipboard.png" title="clipboard.png"></p>
<p>道理讲应该是还可以去掉 style-loader 等等, 直接把 CSS 引入标签当中,<br>没想好建构方面怎么处理, 暂时先不管了.</p>
<h3>其他</h3>
<p>整体处理下来, Emotion 换上了 style-loader 等, 体积减小 20k,<br>压缩以后大致上也就是 2k 多一些, 跟预想的还是可以的.<br>但是如果能有办法把 style-loader 部分也简化掉, 还能减小一些, 再找找建构方案...</p>
<hr>
<p>其他关于积梦前端的模块和工具可以查看我们的 GitHub 主页 <a href="https://link.segmentfault.com/?enc=xghvO5XAENEj2UHP5xISMQ%3D%3D.skoyVrXrmRghw1BT8J8LnEFu3hKk%2Fyx8g4d1WuWtdus%3D" rel="nofollow">https://github.com/jimengio</a> .<br>目前团队正在扩充, 招聘文档见 GitHub 仓库 <a href="https://link.segmentfault.com/?enc=xH%2FKJnkK3b1gg9GdWNDOyA%3D%3D.G9VLlCiQmgUfTc47Eik%2Fva9MyjevavXBC2vCy0BYIoUxWzc5Ad0G9WDpbwjycAA6" rel="nofollow">https://github.com/jimengio/h...</a> .</p>
Cirru 后续更新维护: 2016~2019
https://segmentfault.com/a/1190000020389563
2019-09-16T00:30:27+08:00
2019-09-16T00:30:27+08:00
题叶
https://segmentfault.com/u/tiye
0
<blockquote>延续之前的一篇文章<br><a href="https://segmentfault.com/a/1190000004209473">Cirru Project in 2015</a><br><a href="https://segmentfault.com/a/1190000009737250">Cirru 演进历程: 2012 ~ 2016</a>
</blockquote>
<p>大致从 2017 年以后, Cirru 在图形探索上面就比较少了, 还是基于原来的方案.<br>主要在 Stack Editor 基础上设计了新的 Calcit Editor.<br>另外围绕 Calcit Editor 做了一些辅助工具.<br>大多想法还是用 Calcit Editor 维护以前整理出来的这些小的应用.</p>
<p>尝试 Nim 是最近的想法了, 想用静态类型语言再尝试一下以前做的.</p>
<h3>Calcit Editor</h3>
<p>项目地址 <a href="https://link.segmentfault.com/?enc=%2BOA8%2BFsUaoIPJCkr3o6seQ%3D%3D.W9FlsbBnoUyY%2BvZe5oukUhC6zyuPNpxS%2B2%2B6vrWF6%2B87Fb33bGqO92TUq5ROBEou" rel="nofollow">https://github.com/Cirru/calc...</a></p>
<p>大致上就是以前的 Stack Editor 的功能改进. 整体界面功能相似.</p>
<p><img src="/img/remote/1460000020389566" alt="" title=""></p>
<p>项目最开始的时候用的 cumulo-editor 这个名字, 所以记录有写分开的,</p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=vIdpAX0tOId4Fv3KgFSZew%3D%3D.MdszIS5YTn3cC%2BlqeSZGu3XaAyA6vlY3c6e6uexaQm8ONDfrvAaJM5CtN01fW4moz0kKUJPjBPxi9MNy2OSd8JamfbYrhBCKNH4IBzDNVgJQ87crx0A0tXeIlMf8d1HNmcQMQtOT1kmaJJNQoYsAzjPMPXWEKFNJv0h%2BsMot%2B4tXEbhzw4fjrJCvvz2Qhb9DlV%2B%2Fl2tWEPLtQ0bAmpOSkg%3D%3D" rel="nofollow">Cumulo Editor 时间线上一些记录</a></li>
<li><a href="https://link.segmentfault.com/?enc=8QWTbtB53nBRJK6j9MgIcQ%3D%3D.xkCnyL3JLvA5jRvV52wColTDVAkvEqQZivsrPt6OOSPzCrC0I8zR%2BhH%2FrYhN5DJwpo5oTvbY5V28qWONHrSGVtO0rjTCeM4%2BAJILAC7QpxwTI3pP8Vu%2BAt1%2Fs46KC%2FL0RlkdVcz8zOsLG1QM4SkSeX0v3hIsBeeQUiLtT2DtiZ1FGXUXP2UdIBY7J5CMJ8FMtjEsMygayjsrSmoKDEedGA%3D%3D" rel="nofollow">Calcit Editor 时间线上一些记录</a></li>
</ul>
<p>Calcit Editor 当中最核心的算法是 <a href="https://link.segmentfault.com/?enc=ZwupCB6QCPQKxnwdXYOYbQ%3D%3D.3dN59QE8lJY1Y%2BnA3P0mV8oPm90132fwU1fCYjhvfrGbX5H2Oihjv3CkpEJRbg8W" rel="nofollow">bisection-key</a> 的算法, 用来计算插入节点的位置.<br>比如这样一段 Clojure 代码, 包含简单的嵌套的数据:</p>
<pre><code class="clojure">(if (coord-contains? focus child-coord) focus nil)</code></pre>
<p>用 Cirru 的数据形态表示出来, 就是数组的嵌套:</p>
<pre><code class="edn">["if" ["coord-contains?" "focus" "child-coord"] "focus" "nil"]</code></pre>
<p>当有两个客户端在同时编辑中间嵌套的表达式的时候, 数组的位置是 <code>1</code>,<br>那么, 比如客户端 A 的 <code>1</code> 的前面插入了新的节点, 并且修改, 那么也是 <code>1</code>,<br>然后 <code>A</code> 修改 <code>1</code> 的时候, 对应的 <code>B</code> 当中的数据就已经过时了.<br>总体上就是很难控制中间的逻辑不会发生明显的冲突.</p>
<p><code>bisection-key</code> 模块的方案是给每一个节点增加 id, 并且 id 用的字符顺序,<br>再细节就不说了, 至少大幅缩小了发生冲突的可能性, 最终的到这样的一个结构:</p>
<pre><code class="edn">{
:type :expr, :data {
"T" {:type :leaf, :text "if"}
"j" {
:type :expr, :data {
"T" {:type :leaf, :text "coord-contains?"}
"j" {:type :leaf, :text "focus"}
"r" {:type :leaf, :text "child-coord"}
}
}
"r" {:type :leaf, :text "focus"}
"v" {:type :leaf, :text "nil"}
}
}</code></pre>
<p>作为 Calcit Editor 的存储格式(实际使用当中包含更多的节点信息).</p>
<p>基于 Calcit Editor 就可以实现多个客户端实时协作的功能了.<br>不过实际上来说这方面做的探索还是不够, 绝大部分时候还是单机使用的.<br>另外加入的功能有比如实时显示光标位置, 全局通知, 连接 nrepl 之类的功能.<br>以及专门优化了在 Git 切换分支的时候文件重载的问题.<br>为了方便使用, 增加快捷键, 处理复制剪切粘贴, 原始存储格式修改等等功能.</p>
<p>对于 Clojure 开发来说, 在文本的基础上, Calcit Editor 有一定的结构化方案,<br>但相对来说, 由于不是 IDE 那样集成的环境, 很多功能还是缺失的.<br>对于推广来说不是好的事情. 只能说对于 Cirru 本身的探索做了延伸.</p>
<p>另外生成 Clojure 代码的模块也逐渐有调整: <a href="https://link.segmentfault.com/?enc=5tlcr7LKdo15buA9quO49w%3D%3D.TZZgNiQG8yv673g0fTNj8RDcuY1zlDDqKdrM3urhjxd%2BrpypFLwL3kStRBFBPQxA" rel="nofollow">sepal.clj</a>.</p>
<h3>编辑器相关组件</h3>
<p>除了主体的编辑器, 还衍生一些相关的工具:</p>
<ul><li><a href="https://link.segmentfault.com/?enc=PTopQsd4lGX7GtB9%2FMRbxQ%3D%3D.743XmEUIuLougZFP3PkDFCPnXJ5kQW12ZBvmISuE1RTGt3F8n%2BvX40FI29Ta3KKg" rel="nofollow">calcit-snippets</a></li></ul>
<p>由于 Cirru 的文本格式比较特殊, Snippets 需要单独处理.<br>所以做了一个简单的服务, 将用到的几个 Snippets 存放在专门的页面上.</p>
<ul><li><a href="https://link.segmentfault.com/?enc=cEWIItcSckiMEZ5IAqrt1w%3D%3D.UoNCrwFvYAbVQo94FPHFY%2Be9uYnmrksOZAMgvF6vmaOM%2BgGLTW52phGuuGevN8rY" rel="nofollow">calcit-viewer</a></li></ul>
<p>这是一个快速查看 <code>calcit.edn</code> 存储文件的小工具,<br>功能比较简单, 就是讲代码渲染出来, 一次性全部展开, 方便直接查看.<br>实际使用来说应该再加入更多功能的..</p>
<ul><li><a href="https://link.segmentfault.com/?enc=6oKdH8myKdgdE9qvZ4JLbw%3D%3D.g1OIJhcXGRYWMFbPqA6pUpje0%2BwWKid%2BbxlBDyqcIg7DK1BwR3iYLEAauZbsU1Jr" rel="nofollow">calcit-editor</a></li></ul>
<p>一个单独高亮显示代码的模块, 提供类似 calcit-editor 的局部.<br>可以用在 Snippets 预览的场景当中, 以及处理类似的需求.</p>
<ul><li><a href="https://link.segmentfault.com/?enc=H0qlJZ7%2BC%2F2jlfaajgqszA%3D%3D.vRCdKxQfbu0eDaIEY8CLdgoC4f4NOcOtLzRYkZ1isILX3lc0NZ7S00WpGdjCgasJ" rel="nofollow">respo-cirru-editor</a></li></ul>
<p>这个基于之前 Stack Editor 的编辑器组件制作的, 用于网页上直接编辑.<br>存储格式比较简单, 直接就是数组的形式, 操作习惯大体上跟 Calcit Editor 相似.<br>现在 Cirru.org 页面当中编辑预览就是用的这个工具.</p>
<ul><li><a href="https://link.segmentfault.com/?enc=tCCTU0BHB6Z16disX1U%2FYA%3D%3D.9SBbfrTO3xFAheVa24U0NiTOsLiJB5OLYBcnW9gHsbKkdCgiXrsbTRzwaHemT%2FPQ" rel="nofollow">favored-edn</a></li></ul>
<p>Clojure EDN 美化的函数运行有一些性能开销, Calcit Editor 存储信息又可能比较大.<br>慢的主要原因应该是布局的问题, 各种对齐要求比较特殊.<br>我的场景当中需求有限, 所以写了一个简单的生成数据文件的工具.<br>同时我需要代码换行而且大致可读, 能被 Git 自动 merge, 甚至极端时手动调整.</p>
<h3>Parser in Nim</h3>
<p><a href="https://link.segmentfault.com/?enc=37mcZXYpPJe1rBwYHvY2WQ%3D%3D.FS4FS9W8tjny44ODyyQezmOxvft%2BsFyg90LJ5kcjGhpkQYRjiCZLjE6ry0lwAEPv" rel="nofollow">parser.nim</a> 和 <a href="https://link.segmentfault.com/?enc=GgUYr8ypOIOwxn4m2P9%2BiQ%3D%3D.ngLaDwj5z2%2FsVtWHJksOHNG3WoipJGeqxUNLZ4ytMnQp0VtWKPdoRU1ajH3qSwzn" rel="nofollow">interpreter.nim</a> 是近期想到所以开始做的尝试.<br>以前版本的 Parser 都比较慢, 主要还是因为是动态语言.<br>当时最快的版本应该是 Go 的, 但是 Go 的问题, 包管理, 动态特性, 都有影响.<br>所以我考虑试一下 Nim, 一来是简单, 二来性能方面能尝试些不一样的方案.</p>
<p>Nim 版本的 Parser 做了 Lexing, 之前的版本都比较粗暴直接逐字解析的.<br>然后也处理了一下行号和报错方面的问题, 方便在具体的场景当中用.<br>虽然目前解析的代码体积都比较小, 但是明显能感受到跟 JavaScript 版本的提升.</p>
<p>至于 interpreter 也是基于此前 Go 版本的写法做尝试.<br>目前没有想好, 但是希望能做出来一些能用在日常工作中的工具.</p>
<h3>其他</h3>
<p>毕竟要工作, 纯粹的 Cirru 的探索已经很少了, 也过了那样的年纪.<br>对我来说图形布局已经超出当初想要的效果了, 至少在便利性上面.<br>所以我没有多少欲望说要去再做破坏式的改进的, 基本上都是微调.</p>
<p>短期看来, interpreter 部分做一下定制能够派上点用场.<br>日常开发当中还是需要一些脚本的, 用来执行短平快的一些任务.<br>用 Bash 写长了不好处理, 用通用编程语言, 又有一些模块化和 API 的限制.<br>Cirru 最初就是为了方便设计的书写, 非常灵活, 也适合继续定制.<br>我觉得这方面会再尝试一下, 希望能帮到日常开发.</p>
<p>远的先不会去想了, 精力不够的.</p>
Nim 语言写 Cirru Parser 的上手记录
https://segmentfault.com/a/1190000020318783
2019-09-07T14:39:03+08:00
2019-09-07T14:39:03+08:00
题叶
https://segmentfault.com/u/tiye
4
<blockquote>句句换行, 因为我写的是代码啊!</blockquote>
<p>前端码农, 写了多年的动态语言了, TypeScript 算下来也用了两年.<br>之前试过 Go, 但是 <code>interface {}</code> 简直是 any 一般的存在.<br>由于 Clojure 语言本身有开销, 所以尝试考虑学 Nim 来应对一些极端性能的情况.</p>
<h3>性能</h3>
<p>从网上的资料看, Nim 编译到 C 运行, 能跑到媲美 C 的程度,<br><a href="https://link.segmentfault.com/?enc=gfBUzM3aBeaDnY2rFuHmlg%3D%3D.TnO11jrV%2FW%2FtEzP8NnH9%2B55mgCegi8JdiGOWTkzLqAEtmz0XfdbAioHl3Fo%2FDCyi" rel="nofollow">https://github.com/kostya/ben...</a><br>总体上不是最快, 但是在第一梯队, 而且也存在一些优化的空间.<br>不过, 就像论坛上讨论的, 关键性的性能还是跟算法和数据结构等因素相关,<br><a href="https://link.segmentfault.com/?enc=SQKTmIiqtFzBgOcDE%2FLnKg%3D%3D.uavQaQaF4hVyN6cIfpYI66jEFCruhd9WM9X8mp4pRpwN4GnXk%2BHHSkhdc3wRGtd2" rel="nofollow">https://forum.nim-lang.org/t/...</a><br>Nim 提供了相对简洁的写法和概念, 能快速写出性能还不错的代码,<br>如果想继续优化, 可以关掉自动的 GC 做更深层的优化再提高性能.</p>
<p>我在论坛上也看到个例子, 用 Nim 普通的写法处理文件, 还不如 Python 快,<br>后面有人调试代码, 优化了一个依赖库, 干掉了瓶颈以后十几倍的性能提升.. 还是算法.</p>
<p>Clojure 的问题是, 作为动态语言, 本身有巨大的运行时.<br>就运行的来说, 性能不会太慢的, 只是运行时本身的开销基本上无法优化掉.<br>在 ClojureScript 里面, Closure Library 的阴影一直是在的, JavaScript 的开销也在.<br>而 Nim 编译出来的 Binary 显然跟 C 类似, 可以直接跳过这些东西.</p>
<p>比如 Cirru Parser 完成一次文件读取和解析, 代码比较短, 几毫秒就完成了,</p>
<pre><code>=>> cat ../example/echo.cirru
println 1 2
=>> time ./main ../example/echo.cirru
1 2
real 0m0.011s
user 0m0.003s
sys 0m0.006s
=>> time ./main ../example/echo.cirru
1 2
real 0m0.008s
user 0m0.003s
sys 0m0.004s
=>> time ./main ../example/echo.cirru
1 2
real 0m0.008s
user 0m0.003s
sys 0m0.004s</code></pre>
<p>而 nodejs 脚本启动一次就花比这长的时间了. 更不用说 ClojureScript 那一整套,</p>
<pre><code>=>> cat a.js
console.log("100")
=>> time node a.js
100
real 0m0.071s
user 0m0.049s
sys 0m0.016s
=>> time node a.js
100
real 0m0.072s
user 0m0.047s
sys 0m0.019s
=>> time node a.js
100
real 0m0.068s
user 0m0.048s
sys 0m0.016s</code></pre>
<h3>语法</h3>
<p>Nim 语法借鉴 Python 挺多的. 因为 CoffeeScript 当初就跟 Python 相似, 所以比较熟悉.<br>基本上就是借鉴来借鉴去的, 沿着缩进这一派, 简化了很多干扰的符号.<br>比较不一样的地方, 主要是 Nim 是带类型的, 所以多出了一些写法, 可能局部会遇到困惑.<br>普通的逻辑代码, 整体看上去跟 Python 跟 CoffeeScript 都非常相似.</p>
<p>一些特征的地方, Nim 里面没有直接用 <code>def</code>, <code>lambda</code> 或者 <code>() -></code>,<br>Nim 的函数是用 <code>proc</code> 表示的, 这在高级语言里边不多见,<br>但是对于 Clojure 用户我觉得这个就是很明确的信号 procedure 而不是纯函数.<br><code>proc</code> 可以被写成多行的匿名函数用, 大致感觉还可以, 但不如 CoffeeScript 方便.</p>
<p>Nim 当中的模块, 如果暴露公共函数的话要在函数或者变量之后加上 <code>*</code> 作为标记.<br>相比 Go 当中用大写开头来标记, 这个算是友好多了, 不会影响到函数名.</p>
<p>写 JavaScript 或者 Go 的时候, 基本上用的就是 <code>var</code> 声明变量.<br>JavaScript 后面加上了 <code>let</code> 加上了 <code>const</code> 区别也并不大.<br>即便 <code>const</code> 会要求赋值不可修改, 如果定义的是对象, 后面还会被修改掉.<br>在 Nim 当中比较明确还有严格,</p>
<ul>
<li>
<code>let</code> 定义的就是不可变的变量结构, 不能再被赋值, 也不能被修改熟悉,</li>
<li>
<code>var</code> 定义可变的结构, 而且定义在参数当中如果可变, 也需要对应的 <code>ref</code> 标记.</li>
</ul>
<p>编译器会提示, 区分得相当明确了.</p>
<p>跟 CoffeeScript 类似, Nim 有默认的返回值. 算是语法糖.</p>
<p>这样的缩进语法, 逻辑代码写下来, 除了类型以外, 跟 Python CoffeeScript 就及其相似了,</p>
<pre><code class="nim">proc resolveComma*(expr: CirruNode): CirruNode =
case expr.kind
of cirruString:
return expr
of cirruSeq:
var buffer: seq[CirruNode]
for i, child in expr.list:
case child.kind
of cirruString:
buffer.add child
of cirruSeq:
if child.list.len > 0 and child.list[0].kind == cirruString and child.list[0].text == ",":
let resolvedChild = resolveComma(child)
for j, x in resolvedChild.list[1..^1]:
buffer.add resolveComma(x)
else:
buffer.add resolveComma(child)
return CirruNode(kind: cirruSeq, list: buffer, line: expr.line, column: expr.column)</code></pre>
<h3>函数重载</h3>
<p>关于函数的重载, 我长期用 CoffeeScript Clojure TypeScript 接触比较少,<br>比如 Clojure 一般是对于不同参数个数存在函数重载.<br>TypeScript 类似, 主要还是参数个数所以可以重载. 对于不同类型的函数重载都没用过.<br>主要还是动态语言经常就是 uni-type 的习惯, 即便编译, 编译期函数不方便重名, Go 都不行..<br>然后就要等到运行时才能对函数进行重载, 除非说是不同参数个数算是例外..<br>动态类型再极端点要做重载要 Python 的 <code>__add__</code> 或者 JavaScript 的 Proxy 进行骚操作了.</p>
<p>但是 Nim 就是静态类型语言啊, 直接对 proc 做不同类型的函数重载啰.</p>
<pre><code class="nim">proc zero[T: int]() = 0
proc zero[T: float]() = 0.0
proc zero[T: int32]() = 0'i32</code></pre>
<p>在 Cirru Parser 当中我重载了 <code>==</code> 函数用于节点的比较:</p>
<pre><code class="nim">proc `==`*(x, y: CirruNode): bool =
# overload equality function
return cirruNodesEqual(x, y)
proc `!=`*(x, y: CirruNode): bool =
# overload equality function
return not cirruNodesEqual(x, y)</code></pre>
<h3>社区氛围</h3>
<p><a href="https://link.segmentfault.com/?enc=onuEq1eltBmLymWvUgKZaA%3D%3D.SDXV2BvCmHT4qtPSIoXWGqvh%2B2pcQl6a%2B9EOXt6MyDc%3D" rel="nofollow">https://forum.nim-lang.org</a></p>
<p>论坛比较活跃, 跟 Clojure 社区挺像的, 比较友好.<br>我在上面发了几次提问, 都很快有人回答我, 答案也是切中要害, 也不嫌弃我新手.<br>就文档来说, 我感觉这个是比 Clojure 好的, 容易上手.<br>Clojure 那边很多东西在 Slack 上, 搜索不到, 但是 Nim 社区能搜到的东西很多.<br>而且 Nim 有个好处是编译器提示比较清晰, 这比 Clojure 明确多了.</p>
<p>不过跟 Clojure 时不时刷上 Hacker News 不一样, Nim 显得低调很多.<br>Clojure 社区时不时看到有人秀 Macro, 当然, 用得也是比较随意的,<br>在 Nim 论坛上没怎么看到, 估计是用的人不那么多吧, 高级技巧.</p>
<h3>包管理</h3>
<p><a href="https://link.segmentfault.com/?enc=yoOnR7ivJYFFb8IgcnH9Zw%3D%3D.1SjX4C%2FNJBhImY6U2rXxQbms3QC88UBueY7Xk1eUC%2B%2BVXxKxvVgRzsGAUwdMXNEF" rel="nofollow">https://github.com/nim-lang/n...</a></p>
<p>包管理是发布模块依赖的功能, nim 提供了 <code>nimble</code> 命令用于项目管理,<br>npm 的使用还好, Go 跟 Clojure 的包管理都有一些坑的,<br>Clojure 使用的验证机制要搞 GPG 秘钥, 初次使用配置起来挺烦的.<br>Nim 干脆直接用 GitHub 来维护模块列表, 放在一个仓库里, 简单粗暴.<br>所以发布模块的时候需要 fork 仓库提交信息, 等待人工合并, 相对麻烦一点.<br>目前 Nim 的总共的模块数量相对来说不是那么多, 不像 Web 开发圈这么多样.<br>不过上手的门槛确实不高, 相信有 GitHub 使用经验的人很快都能搞定.</p>
<p>另外对于脚手架, 对于测试, 对于本地安装, 对于依赖管理等等, 都提供了简单的方案.<br>刚开始按照教程一步步走下来, 基本没有遇到什么大的坑, 提示也比较明确.</p>
<p>项目最初的开发, 由于比较简单, 也就很快能用命令直接运行, 比较省事,</p>
<pre><code class="bash">nim c -r main.nim</code></pre>
<h3>Object Variants</h3>
<p>具体到 Cirru Parser 的开发, 由于用到递归的数据结构, 刚开始遇到的麻烦,<br>Nim 不像动态语言轻易定义任意结构, 也不像 Haskell 直接有代数类型的递归结构,<br>论坛问了一圈, 意识到 Nim 需要用 Object Variants 的用法专门处理.<br><a href="https://link.segmentfault.com/?enc=IDDOhP8GQS5dJHItrOY0ig%3D%3D.QbhtxTeyRUqf2eZlxOEa0mcmtiRkTI1kKmHs%2BO8qmb9I%2Bp6e01Tn0b%2BbamtE34y%2BOJkgDcPPmGtkVsBS2O8T7A%3D%3D" rel="nofollow">https://nim-lang.org/docs/man...</a></p>
<p>比如 Cirru 表达式的结构就需要这样定义出来,</p>
<pre><code class="nim">type
CirruNodeKind* = enum
cirruString,
cirruSeq
CirruNode* = object
line*: int
column*: int
case kind*: CirruNodeKind
of cirruString:
text*: string
of cirruSeq:
list*: seq[CirruNode]</code></pre>
<p>跟 Clojure 的 dispatch function 有点相似, 需要选定字段专门用于表示类型,<br>后面都在运行时基于这个字段做判断, 然后定义不同的逻辑,<br>个人感觉远远没有代数类型里面的设计优雅, 也没有 TypeScript 直观, 但是使用当中还是够用的.</p>
<h3>其他</h3>
<p>用了 Clojure 以后我比较习惯用 Persistent Data 和尾递归做抽象了,<br>当然, 尾递归相对来说是在编程语言里加限制了, 编码的灵活性反而少一点,<br>这次在 Nim 当中为了性能, 全部用的是可变数据的操作, 还是蛮新鲜的...<br>确实用 mutable 写法, 有点黑科技的感觉, 算法很巧妙, 也很脏, 偏偏性能很快.</p>
<p>Nim 编译器还支持 WebAssembly 和 JavaScript 的后端, 目前没有用到.<br>最初选 Nim 有一个原因也是考虑以后上手 WebAssembly 希望可以方便一点吧.</p>
<p>目前使用比较浅, 数据结构用得也比较单一, 基本参考文档还是能解决.<br>完成的代码在 <a href="https://link.segmentfault.com/?enc=UFqzi3B6HDz5Mr39fXIaQg%3D%3D.m7CPwfIty36MyjWtF8p8cCAPZ1UXORpkBQH70mY6PYBAdGN7QvVFSN%2FztNSiVzAk" rel="nofollow">https://github.com/Cirru/pars...</a><br>等到后续有想法再记录.</p>
积梦前端 Meson Form 的分层抽象设计
https://segmentfault.com/a/1190000019582729
2019-06-26T00:33:16+08:00
2019-06-26T00:33:16+08:00
题叶
https://segmentfault.com/u/tiye
2
<h3>概述</h3>
<p>这篇文章大致梳理积梦采用的表单方案做的一些尝试和回顾.<br>目前从用的方案是 Meson Form, 名字大致来源于 immer json:<br><a href="https://link.segmentfault.com/?enc=%2BAnTGr%2BSdeT5BnlqCU5L0g%3D%3D.EvrIffvuA0YlCVByAAhEg4aVvOV57DbETNnGBGK2xtnJt%2BPLk%2Bx5HqvK9IVB3tP4" rel="nofollow">https://github.com/jimengio/m...</a><br>目前 Meson Form 形态逐渐开始稳定了, 方案上基本还是可靠的.<br>过程当中的考虑有一些曲折, 大致做一些梳理.</p>
<h4>KForm</h4>
<p>早先我们的方案当中其实沿用了一套书写较为简便的方案, 称为 <code>KForm</code>.<br>看过 Meson Form 例子的话, 跟 KForm 的写法已经比较相似了,<br>主要看 <code>items</code>, 每个元素定义了表单当中的一项, 这个表单有 3 项:</p>
<pre><code class="tsx">let { dataSource, onSubmitData, formRef } = this.props;
let items: IFormItem[] = [
{ id: "name", label: lang.lblName, rules: [{ required: true }] },
{ id: "code", label: lang.lblSerialNumber, rules: [{ required: true }] },
{ id: "description", label: lang.lblDescription },
];
return <KForm ref={formRef} items={items} data={dataSource} layout={layout} onSubmitData={onSubmitData} />;</code></pre>
<p>粗看这个例子, 可能觉得已经是比较成熟的表单方案了.<br>不过深入使用的话, KForm 存在两个问题,</p>
<p>第一个问题是没有类型系统的良好支持, 或者说 TypeScript 的良好支持.<br>KForm 的内部是基于 antd 表单的, 而控件一般都有各自的属性,<br>KForm 当中添加属性, 需要用 property 手动写, 这个地方是丢失类型的,<br>这个地方的处理, 对于真实的开发调试来说不够友好, 没有检查也没有提示,</p>
<pre><code class="tsx">let items: IFormItem[] = [
{
id: "userGroup",
label: lang.lblUserGroup,
rules: [{ required: true }],
controlType: UserGroupSelectDropdown,
controlPropsMapper: (controlProps) => {
controlProps.plantId = plantId; // <-- 缺失类型检查
return controlProps;
},
},
];</code></pre>
<p>一定程度上手动添加类型或许可以作为补充的, 但是书写相对繁琐.</p>
<p>另一个问题是可变数据, antd 的方案是基于可变数据实现的.<br>React 当中倾向使用不可变数据来辅助性能优化,<br>同时另一方面, 不可变数据也能避免表单的对象被随意修改,<br>KForm 当中可变对象被传递到多处, 就引发了一些状态改变的 bug.<br>而且随着我们越来越多使用 immer, 两者之间的不协调就越来越明显.</p>
<p>另外还有个遇到的问题是 KForm 封装好以后扩展性不够.<br>这个就跟具体的实现有关系了, 导致不能应对一些特殊的场景.<br>比如自定义组件时要修改额外的字段, 就需要组件能够暴露底层操作.<br>但总体上感觉随着遇到不同的业务, 总觉得不够用.</p>
<h4>Immer Form</h4>
<p>为了能解决前面说的几个问题, 我基于 immer 开始寻找方案:</p>
<ul>
<li>整个方案围绕 immer 设计, 不应该随意出现可变数据,</li>
<li>大部分的逻辑能够被 TypeScript 类型覆盖到, 也能够配合自动补全,</li>
<li>能够比较灵活地定制, 用于处理一些特殊的表单.</li>
</ul>
<p>由于没有想到清晰的方案, 早先我先尝试用简单的函数来抽离复用的代码,<br>比如表单的渲染, 比如错误校验, 我分离出了一些常用的函数,<br>然后整理出大致一套方案, 完成了我当时遇到的几个表单,<br>大致的代码比如:<br><a href="https://gist.github.com/chenyong/9d96e459acd9bd8a185d396d8ee4bbb1">https://gist.github.com/cheny...</a></p>
<p>回头来看, 这套代码其实比较零碎, 表单状态被暴露在外部,<br>也就意味着在父组件当中需要附加上若干状态个方法用于维护校验,<br>渲染部分相当于只有复用布局, 但是没有做封装, 基本没有限制.<br>这个写法好处就是没有什么限制, 各种场景要用基本都是可以用上的,<br>坏处就是.. 代码会比较啰嗦, 错误需要自己绑定到对应位置, 其实挺烦.</p>
<h4>JSON 配置表单</h4>
<p>Immer Form 的写法本来是打算逐步简化的, 但是结果用了挺久的,<br>一方面是没有找到好的入口, 另一方面确实业务也消耗着主要的经历,<br>我跟同事都是有点想念之前老代码当中用的 antd 的, 以及前面这个写法.</p>
<p>我觉得用 JSON 结构配置表单是正确的方向, 因为这样描述比较少冗余.<br>而且之前的 KForm 其实也证明对于简单的业务, JSON 形态完全够用的.<br>所以很自然会想到做一个组件, 将 JSON 渲染到 Form, 以及生成简单的逻辑,<br>以及对于特殊的场景, 提供自定义渲染或者其他配置, 用来特殊处理.</p>
<p>但是中间有个问题, 即便是 JSON 我依然需要保证自动补全能用,<br>不过, 一个巨大的 JSON 整个在 VS Code 当中错误提示, 非常感人.</p>
<p>比如这样一个结构,</p>
<pre><code class="ts">let formItems = [
{
type: EMesonFieldType.Input,
name: "name",
label: "名字",
},
{
type: EMesonFieldType.Input,
name: "name",
label: "名字禁用",
disabled: true,
},
]</code></pre>
<p>我需要在 <code>name</code> 或者 <code>label</code> 位置填写错误时能够被自动提示,<br>同事, 对于 <code>disabled</code>, 我输入 <code>dis</code> 能看到对应的补全.<br>我大致知道 VS Code 有类似的功能的, 在我描述了 <code>type</code> 的前提下, 类似于,<br><a href="https://link.segmentfault.com/?enc=OmjlfXHgXvhrG0H6QvJe%2Bw%3D%3D.oKZcdmwIeYcTVM4jyShCJFVu0O4rdpIc2g8YyPZwOMZqb6GuOMWd7XP%2BQPGOPWrXi%2BvWXq3Y8bEh0pCz7pj32u%2F3%2FdXg9ZWya4KhDUHZ%2BuA%3D" rel="nofollow">https://basarat.gitbooks.io/t...</a></p>
<p>实际使用当中反而预测坑了, 我试了一下, 发现错误提示总是在整个 JSON 数组上.<br>后来在朋友的帮助下, 终于明确了在变量上直接加类型约束, 可以规避问题, 也就是,</p>
<pre><code class="tsx">let formItems: IMesonFieldItem[] = [
{
type: EMesonFieldType.Input,
name: "name",
label: "名字",
},
{
type: EMesonFieldType.Input,
name: "name",
label: "名字禁用",
disabled: true,
},
];</code></pre>
<p>其中 <code>IMesonFieldItem</code> 是借助 Union 关联在一起的多个 interface.<br>这样写之后, 错误提示和自动补全, 都显得相对正常了.</p>
<h3>Meson Form</h3>
<p>基于上面这种 JSON 的格式, 以及一些字段, 我编写了一个简单的组件,<br>这样, 就是一个简单的 Meson Form 的结构了.<br>这是一个例子 <a href="https://link.segmentfault.com/?enc=lbSEztUGfdycKyZsBba8Cg%3D%3D.ZwJIcOy40PeFTtb5xjxc3ABALxXrRI1DjZU%2F%2Be2lwhc%3D" rel="nofollow">http://fe.jimu.io/meson-form/</a></p>
<pre><code class="tsx">let formItems: IMesonFieldItem[] = [
{
type: EMesonFieldType.Input,
name: "name",
label: "名字",
},
{
type: EMesonFieldType.Input,
name: "name",
label: "名字禁用",
disabled: true,
},
];
return (
<div className={cx(row, styleContainer)}>
<MesonForm
initialValue={form}
items={formItems}
onSubmit={(form) => {
setForm(form);
}}
/>
<div>
<SourceLink fileName={"basic.tsx"} />
<DataPreview data={form} />
</div>
</div>
);</code></pre>
<p>类似地, 对于自定义渲染的需求, 直接用上一个 render 函数插入代码,<br>Demo <a href="https://link.segmentfault.com/?enc=GJbhUDhk%2FDkXFTu7448SKA%3D%3D.nNijJm80Bq9xlRcq0Zd4eCeaMSiIFEz5HXzNEnVP%2BkpQSezj4BH2x8UZQ%2BMO6rsQ" rel="nofollow">http://fe.jimu.io/meson-form/...</a></p>
<pre><code class="tsx">let formItems: IMesonFieldItem[] = [
{
type: EMesonFieldType.Custom,
name: "x",
label: "自定义",
render: (value, onChange, form, onCheck) => {
return (
<div className={row}>
<div>
Custome input
<Input
onChange={(event) => {
onChange(event.target.value);
}}
placeholder={"Custom field"}
onBlur={() => {
onCheck(value);
}}
/>
</div>
</div>
);
},
},
];</code></pre>
<p>基于这套写法, 后面又加上了 <code>Select</code> <code>Switch</code> 等组件和样式,<br>目前支持的类型比较少, 经常依赖自定义渲染, 后续还要跟随业务扩展.<br>实际使用当中也提出了需要更多钩子用于状态修改, 慢慢也加上了.<br>只能说大致满足了常用的需求, 加上自定义. 在原来的基础上减少了代码量.</p>
<p>另一方面早期 KForm 大量的场景是跟 Modal 用在一起的,<br>所以 Meson Form 也加上了 Modal 的封装, 尝试覆盖一些常用的需求:<br><a href="https://link.segmentfault.com/?enc=XELy6RdMLCLLoeuhij2O8w%3D%3D.FO7F6cUiFhnAYwdWgjcZGndSssniHI6yagPdrY%2Bg%2BWQb5v3HqkbExwFyuzTOMQoZ" rel="nofollow">http://fe.jimu.io/meson-form/...</a><br><a href="https://link.segmentfault.com/?enc=3UW2YTJCVitU7vyBG9bt7Q%3D%3D.wIEQlKKB4MmU5G%2FivWNHuDsQLW4JvN1qUhHxr0K27BF4SVQCWuYdIIobjJ1DKVVk" rel="nofollow">http://fe.jimu.io/meson-form/...</a></p>
<h3>Meson Core</h3>
<p>不过总体上说业务往往是多变的, 一个 Form 组件的形态总归是不够的.<br>比如说我会遇到场景, 没有文字标签, 标错的样式也有区别,<br>这种场景, 比如就是登录框了, 通常就不是用 Form 的样式去做的.<br>但是又比较明确, 它还是 Form, 有校验, 只是界面和结构有区别.</p>
<p>基于这一点, 我们再进一步想, 前面的 Form 的封装其实是有点仓促的,<br>渲染部分的组件, 实际当中时会有多种可能的, 而不单单是一种渲染,<br>对于 Form 来说, 更加稳定真实的其实是数据和校验的部分,<br>这部分可以超脱 UI 的形态, 但是表单自己基本都会有在表单项还有校验,</p>
<p>那么, 我就想起来用 Hooks 可以分离出表单的状态部分,<br>这部分包含表单的状态, 校验结构, 还有一些操作,<br>这部分代码可以超越表单组件本身, 被用到特殊的表单的场景, 核心的 API 比如:</p>
<pre><code class="tsx">let { formAny, errors, onCheckSubmit, checkItem, updateItem, forcelyResetForm } = useMesonCore({
initialValue: submittedForm,
items: formItems,
onSubmit: onSubmit,
});</code></pre>
<p>这里能获取 <code>form</code> <code>errors</code>, 这是渲染表单必备的数据,<br>然后也暴露出来其他一些用于校验和更新表单数据的函数, 甚至于重置表单的数据,<br>这样就得到一个例子, 可以沿用 Meson Core, 然而自己定义界面如何渲染,<br><a href="https://link.segmentfault.com/?enc=bd9eD2%2Bt7STDKn8SAWEs9A%3D%3D.5sqGWUAOebLpU3xlDqlsggliLeMrxJlsGuGNRrfxL9WYoVXVvHADO5Zx2OX3XGgC" rel="nofollow">http://fe.jimu.io/meson-form/...</a></p>
<pre><code class="tsx"><div className={styleFormArea}>
{formItems.map((item) => {
switch (item.type) {
case EMesonFieldType.Input:
return (
<div className={column} key={item.name}>
<input
value={form[item.name] || ""}
type={item.inputType}
placeholder={item.placeholder}
onChange={(event) => {
let text = event.target.value;
updateItem(text, item);
}}
onBlur={() => {
checkItem(item);
}}
/>
{errors[item.name] != null ? <div className={styleError}>{errors[item.name]}</div> : null}
</div>
);
}
})}
<div>
<button onClick={onCheckSubmit}>Submit</button>
</div>
</div></code></pre>
<p>基于这个思路, 当我们需要一个横向布局的表单的时候, 就可以复用了.<br>核心的规则和校验逻辑是可以复用的, 渲染部分完全用不同的实现,<br><a href="https://link.segmentfault.com/?enc=cFrYdDXgnrUwHOg3qZP0Eg%3D%3D.4X2YfvnlP2X7GBrXJGw%2FEbzFBbBTDP7r3QTPBUOAYNFCLl%2F91kCE7ZO89pd%2FT66J" rel="nofollow">http://fe.jimu.io/meson-form/...</a></p>
<p>所以 Meson Form 提供的 API实际上提供了两个不同的层次,<br>直接用 Meson Form 可以快速生成简单的 Form, 或者 Core 用于定制.</p>
<h3>其他</h3>
<p>当然对于业务来说, 场景可能是无穷无尽的, 前面的方案依然未必足够.</p>
<p>同事在使用 Meson Form 时候, 需要用到自有定义的 Footer,<br>这一定上跟 Meson Form 最初设定的数据流有冲突了,<br>于是他用 <a href="https://link.segmentfault.com/?enc=d9nFDUaKl8V85j%2BmLI4oPA%3D%3D.lIVhg7di8LCXyJSwr0ZpsTVly8VlZ5sszcPfWLqtZ9Z11sgCkq2w%2Fqnl%2BK7m8giwcsYhxHxmsZyGR%2BJ5fNCfH4dThAP4%2BWYCtDs90BByss8%3D" rel="nofollow"><code>useImperativeHandle</code></a> 又加上了一层封装,<br>目的就是为了能把一些事件抛出, 在外部找到地方去触发, 而不收到设计的限制.</p>
<p>另外使用当中发现校验规则不断增多, 逐渐开始有一些明显的重复,<br>这些规则按理说通过高阶函数还是可以进一步进行抽象,<br>或者不用高阶函数, 单纯用 JSON 定义规则的话, 也能够表达.<br>所以这部分的抽象和简化后面依然需要再补充.</p>
<p>按照 Meson Form 最初设想的, JSON 的格式原本极为通用,<br>社区有别人的例子, 用 JSON 定义表单的格式, 然后前端直接渲染,<br>这样如果还能在中台把表单抽象称为服务的话, 还能分担前端的工作量.<br>即便不能替代前端开发表单, 如果说能在一定程度上生成代码, 也是有效果的.<br>由于 toB 的属性本身就具备大量的表单, 这方面会有不小的需求.</p>
<p>总之 Meson Form 还是需要继续扩充很完善, 用来应对更多业务场景.</p>
<hr>
<p>其他关于积梦前端的模块和工具可以查看我们的 GitHub 主页 <a href="https://link.segmentfault.com/?enc=u0N%2BvJ3yj8nL8BRY2cmvrQ%3D%3D.hK8wVRlW%2F7871CxLxkQHP29w4WNhfQtCOR87ssfCZ04%3D" rel="nofollow">https://github.com/jimengio</a> .<br>目前团队正在扩充, 招聘文档见 GitHub 仓库 <a href="https://link.segmentfault.com/?enc=EnDj8K6z10DLbWdOMv2s2Q%3D%3D.TxeezdxFjRMm8GtBoImw2UqGmN%2FkQxLKchWwyU2l8NluRmBqpSaZoEcvvOAfJsfw" rel="nofollow">https://github.com/jimengio/h...</a> .</p>
前端代码兼容 Chrome 44 的部分操作记录
https://segmentfault.com/a/1190000019477441
2019-06-14T10:56:35+08:00
2019-06-14T10:56:35+08:00
题叶
https://segmentfault.com/u/tiye
1
<p>公司项目原有的代码计划支持到 Chrome 49, 特殊项目需要支持到 Chrome 44.</p>
<p>从网上可以找到 Mac 上的 dmg 安装文件, 这个页面能拿到 Chrome 48,<br><a href="https://link.segmentfault.com/?enc=AldIPRsfJtxd4Hnnx43laQ%3D%3D.%2FembHhHIIQ6yBuj7zfMNyLrTd3Vs1FKHBnuoL8WX9LGnaL9e5hI9nFGlQqEHXhMRhvBtqH4nqBu57sFJU7XYgA%3D%3D" rel="nofollow">https://www.slimjet.com/chrom...</a></p>
<p>安装以后需要指定一个目录启动, 否则低版本浏览器读取高版本的配置, 会报错,<br><code>--profile-directory=chrome-old/</code></p>
<p>网上有 Webpack 配置使用 Babel 的详细教程,<br><a href="https://link.segmentfault.com/?enc=mFJj2tYGwVWURR3qPvL6tw%3D%3D.h6zcTvE8UcV4abqDP%2FOyAsN0yBCySR3Uc4irC%2FIv%2FvOtLHSkNvaMz4KApUraKZLved0IGlNKjYhnnFuTgKtqYNnUqLj9L%2BC%2BnoyPUAdVi32GoWKgFBeDzeBsXDDFw3R5" rel="nofollow">https://medium.com/@zural143/...</a></p>
<p>大致安装一些依赖,</p>
<pre><code class="bash">yarn add --dev @babel/core @babel/plugin-proposal-object-rest-spread @babel/preset-env babel-loader @babel/plugin-syntax-dynamic-import</code></pre>
<p>Webpack 部分的配置大致是,</p>
<pre><code class="js">{
test: /\.js$/, //Regular expression
exclude: /(node_modules|bower_components)/,//excluded node_modules
use: {
loader: "babel-loader",
options: {
babelrc: false,
presets: ["@babel/preset-env"], //Preset used for env setup
plugins: ["@babel/plugin-proposal-object-rest-spread", "@babel/plugin-syntax-dynamic-import", "@babel/plugin-transform-arrow-functions"],
}
}
}</code></pre>
<p>加了 <code>babelrc: false</code>, 然后没有配 <code>.babelrc</code>, 免得两个的配置绕在一起.</p>
<p>一些的对应的版本号从 babel 配置可以推测,<br><a href="https://link.segmentfault.com/?enc=7yO5XeC3GrU7eKtMH%2BUJfA%3D%3D.6%2FT4ZKdh5wRG5c1Oc0tTVkZUwjbz2RcR2kfKeQPc6Ed%2BTPei4b449l2vXV%2BKxsaUnfT%2BMAsOXiC9EI38mXUabAhH3AqLBH5kOPWboM5ocXJlN63wElenthf06KEO6pKJ" rel="nofollow">https://github.com/babel/babe...</a></p>
<p>Chrome 48 就已经不支持解构赋值了, 所以需要 Babel 转换,<br>另一个是 Chrome 45(还是 47?) 的箭头函数, 很多第三方模块也在用, 需要转换,<br>我们代码当中用到动态 import, 也需要转换.<br>目前主要是这几个.</p>
<p>编译通过以后遇到报错, <code>regeneratorRuntime is not defined</code>,<br>网上给出的方案是在代码最开头引用 polyfill 代码(不过我写在 Webpack 配置里),</p>
<pre><code class="js">import "@babel/polyfill";</code></pre>
<p>由于需要转换的代码分散在 tsx 跟 js 里, 包括 <code>node_modules/</code> 当中的代码,<br>参考配置文档分开写了 <code>exclude: /node_modules/</code> 跟 <code>include: /node_modules\/query-string/</code> 的代码,<br><a href="https://link.segmentfault.com/?enc=tALcCEHp%2FmHIXvoYUiW1bw%3D%3D.hrCTnzwfKJswnG7q2uceaHmYRcor0%2FZFymuqZaAOLY2cfWeNZEnDGAFJfdJSZBWJ" rel="nofollow">https://webpack.js.org/config...</a></p>
<p>还有期待问题, 待补充</p>
<hr>
<p>其他关于积梦前端的模块和工具可以查看我们的 GitHub 主页 <a href="https://link.segmentfault.com/?enc=Soq3g3GZNoCx2H1PoJEmIg%3D%3D.z7kpCT9iajlOzUfttywGOa7B%2FqrmbfWalHU1owi5g1s%3D" rel="nofollow">https://github.com/jimengio</a> .<br>招聘的计划和条件也在 GitHub 上有给出 <a href="https://link.segmentfault.com/?enc=6rludyfjIjPhJTDHXJaIJQ%3D%3D.8w4ESb04eKlmGx0UEuYhRatAdNWizavtwsYzjMCTyEcJraLickmfc30BreWXC%2BHl" rel="nofollow">https://github.com/jimengio/h...</a> .</p>
新的 Vue Function-based API 当中的看到的 Clojure Atom 的影子
https://segmentfault.com/a/1190000019455310
2019-06-12T13:23:59+08:00
2019-06-12T13:23:59+08:00
题叶
https://segmentfault.com/u/tiye
4
<p>这次 Vue 大会看到了 Vue 新的 API 设计, 中间有一些觉得眼熟的写法,<br>后面也看到了工业聚的一些解读, 大致知道是什么样的用法吧..<br>当然现场演讲过 Vue 具体实现的优化是更复杂的, 比这个 API 要多..</p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=yfvThK1zKhzwgK%2BwnkJqVg%3D%3D.l1AdtzXeu3IK6LutUxUsaghrAFueSAMOpi3V2alCmdPV9q0x6ysmy2A%2F3JkEqDbsO9GIJxg5TZVy8H3ds4EEdg%3D%3D" rel="nofollow">Vue.js作者在VueConf的演讲视频出炉</a></li>
<li><a href="https://link.segmentfault.com/?enc=rQ3SpoanVVP8GNbSeMQv1w%3D%3D.hcEqyZHm8zpiv6AmnQgF9iVrILmFhCpxAOZfS8WPX%2Fn0%2FWEc7gfei3jRI6uiacuf" rel="nofollow">Vue Function-based API RFC 中文版本</a></li>
<li><a href="https://link.segmentfault.com/?enc=shMXmC6lRh1sxDqAcNj%2BOw%3D%3D.kA67plO3wT5s6L0iGRNLzPn9g6Q3Qp%2FGIs259Dxc1OMfVED0NwMPIo3560Dr%2FcJpJi9fevlXj1DlD5OPHo%2F%2Fjg%3D%3D" rel="nofollow">揭秘Vue-3.0最具潜力的API</a></li>
</ul>
<p>其中比较让我觉得眼熟的是 <code>value(0)</code> 还有特别是 <code>state({count: 0})</code> 的用法,</p>
<pre><code class="js">function useMouse() {
const x = value(0)
const y = value(0)
const update = e => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}</code></pre>
<blockquote>
<code>value()</code> 返回的是一个 value wrapper (包装对象)。一个包装对象只有一个属性:<code>.value</code> ,该属性指向内部被包装的值。<p>这是因为当包装对象被暴露给模版渲染上下文,或是被嵌套在另一个响应式对象中的时候,它会被自动展开 (unwrap) 为内部的值。</p>
</blockquote>
<pre><code class="js">const count = value(0)
const obj = state({
count
})
console.log(obj.count) // 0
obj.count++
console.log(obj.count) // 1
console.log(count.value) // 1
count.value++
console.log(obj.count) // 2
console.log(count.value) // 2</code></pre>
<p>作为一个 ClojureScript 用户我就想着大致对应 Clojure Atom 了.</p>
<p>有点特别吧, 在 Clojure 里面数据(value)和状态(state)是不同的表示,<br>一般的都是 value, 比如数字, 字符串, 数组, 哈希表, 都是数据, 而且默认不可以修改.<br>跟 js 很不一样的, 比如说 <code>[1 2 3]</code>, 数组, 这个是不可以修改的,<br>如果修改其中元素了比如说加一个 <code>4</code> 得到 <code>[1 2 3 4]</code> 就一定是新的引用了.</p>
<p>如果在 ClojureScript 当中要表示一个状态, 就需要使用 Atom(来自 Atomic, 原子性),<br>因为是引用而不是数据, Clojure 里的习惯是用 <code>*</code> 作为前缀来标示的,<br>通过 <code>atom</code> 函数可以定义一个 Atom, 是一个引用, 里面包裹了一个数据,<br>包裹在里面的数据可以是简单的值(1, true, "str"), 也可以是复合的数据(HashMap, Vector),</p>
<pre><code>cljs.user=> (def *a (atom [1 2 3]))
#'cljs.user/*a
cljs.user=> *a
#object [cljs.core.Atom {:val [1 2 3]}]</code></pre>
<p>这只是一个引用, 而且没有 js 里面那种赋值语法可以直接去修改当中的数据,<br>需要操作数据的时候, 要通过特定的函数, 比如 <code>reset!</code> 或者 <code>swap!</code></p>
<pre><code>cljs.user=> (swap! *a conj 4)
[1 2 3 4]
cljs.user=> (reset! *a [1 2 3 4])
[1 2 3 4]</code></pre>
<p>你也不能直接读取数据了, 直接去读, 拿到的是一个引用, 而不是实际的值,<br>这时候需要一个 "dereference" 的操作, 就是函数 <code>deref</code>, 或者直接用 <code>@</code> 前缀:</p>
<pre><code>cljs.user=> *a
#object [cljs.core.Atom {:val [1 2 3 4]}]
cljs.user=> @*a
[1 2 3 4]
cljs.user=> (deref *a)
[1 2 3 4]</code></pre>
<p>参考: <a href="https://link.segmentfault.com/?enc=A8A3ZreWtLgMNAJmNnKTXg%3D%3D.pYUsLKStbB1WEs8gDW5Iemtk5UP6zDWJ%2FKt2SMHqLKH24PwPwhlW0byV1tY%2FuMsgTZXM1ctIJyxozyWHpYHasw%3D%3D" rel="nofollow">https://www.braveclojure.com/...</a></p>
<p>再回来看 js 这边, js 对象没有专门的语法来区分引用不引用的概念, 对象上的 key 都是引用,<br>不过, 通过 Proxy 劫持掉赋值操作, 可以在内部插入一系列的逻辑.<br>而这个例子当中的 <code>state</code> 函数, 就跟 Clojure 当中的 <code>atom</code> 比较像了.</p>
<pre><code class="js">import { state } from 'vue'
const object = state({
count: 0
})
object.count++</code></pre>
<p>这个状态会发生修改, 也就需要 <code>watch</code> 的操作来处理数据更新的情况,</p>
<pre><code class="js">watch(
// getter
() => count.value + 1,
// callback
(value, oldValue) => {
console.log('count + 1 is: ', value)
}
)
// -> count + 1 is: 1
count.value++
// -> count + 1 is: 2</code></pre>
<p>以及取消监听:</p>
<pre><code class="js">const stop = watch(...)
// stop watching
stop()</code></pre>
<p>在 Clojure 当中 atom 也有对应的 API 来添加监听和取消监听,<br>取消一般用一个 Keyword 来标记的, 比如这个例子通过 <code>:logger</code> 来取消.</p>
<pre><code class="clojure">(def a (atom nil))
;; The key of the watch is `:logger`
(add-watch a :logger #(println %4))
(reset! a [1 2 3])
;; Deactivate the watch by its assigned key
(remove-watch a :logger)</code></pre>
<p>另外, 虽然 js 数据本身是可变的, 在 state 当中也是允许赋值的,<br>但是我注意到部分场景, 框架作者并不希望用户任意去修改数据, 特别是传递过的数据</p>
<blockquote>Note this props object is reactive - i.e. it is updated when new props are passed in, and can be observed and reacted upon using the watch function introduced later in this RFC. However, for userland code, it is immutable during development (will emit warning if user code attempts to mutate it).<p><a href="https://link.segmentfault.com/?enc=SCKFsKelUKM6UJD7VxZ2rw%3D%3D.EiT8LfZ5yKZ8NWHX5uXI6ut%2FAkbmh621oXXKH4ObOl2cbhC0WaVzmkbpNDDHe6VehovEpc4m99Z8Ro6mU47Kg%2BBlzMkyMNYt5hDqHdJSF%2BSKCTlost%2FDm77g4a0eIaEXGjbG5EX3ewEe06JJc7RHdQ%3D%3D" rel="nofollow">https://github.com/vuejs/rfcs...</a></p>
</blockquote>
<p>这个行为跟 Clojure 最初的设计思路比较相似, Clojure 要求数据是不可的.</p>
<p>ClojureScript 的不可变数据虽然在前端用使用场景刚好适用, 特别是 React 当中.<br>但是最初 Clojure 选择了不可变数据, 设计了 Atom 的概念, 是为了并发编程考虑的.<br>状态可能会被多个线程共享, 所以需要 ref(引用)的改变, 让多个线程能修改这个状态.</p>
<p>我们知道 value(数据)被一个进程拿到, 是不应该被另一个进程偷偷修改掉的.<br>在 js 当中我们也会遇到, 一个对象如果传给另一方, 最好先拷贝一份,<br>如果把原对象传过去, 别人随意修改了, 己方的逻辑可能遇到异常情况而出错.<br>Clojure 选择的方案是, 把数据设计成不可变的, 这样任意传递, 都不会遇到不一致的问题.<br>如果需要能被修改, 那就是 state(状态)了, 就需要用 <code>atom</code> 封装了, 然后再传过去.</p>
<p>这样一比较, 就会觉得 Clojure 对比 JavaScript 数据, 就是故意反过来设计的,<br>js 当中对象和数组默认就是可以任意被修改的, 需要的时候 freeze 掉, 或者加上 watch 监听.<br>Clojure 当中 HashMap 跟 Vector 默认是不可变的, 需要状态的时候放在 Atom 里去,<br>而 Atom 就是可以通过修改引用来修改的, 也能被监听. 只是说里边的数据依然是不可变的.</p>
<p>除了这种共享状态是用 Atom 的, Clojure 也把 Atom 用在性能优化的地方,<br>比如计算 fibonacci 的时候, 需要缓存, 就会用 <code>atom</code> 存一个可以随时修改的数据,</p>
<pre><code class="clojure">(defn memoize [f]
(let [mem (atom {})]
(fn [& args]
(if-let [e (find @mem args)]
(val e)
(let [ret (apply f args)]
(swap! mem assoc args ret)
ret)))))
(defn fib [n]
(if (<= n 1)
n
(+ (fib (dec n)) (fib (- n 2)))))
(time (fib 35))
; user=> "Elapsed time: 941.445 msecs"
(def fib (memoize fib))
(time (fib 35))
; user=> "Elapsed time: 941.445 msecs"</code></pre>
<p><a href="https://link.segmentfault.com/?enc=%2FXenItAqBhEQxua8oS1C%2Fg%3D%3D.%2BxzTg9UyiEsKK3OmhWdSs56RChhZLthK5MCATZvvIy7ZFzBaPhAgjFHEiKkBo5G9" rel="nofollow">https://clojure.org/reference...</a></p>
<p>这种可变状态在 Clojure 当中一般被放在局部使用,<br>这也是函数式编程惯用的套路, 函数式编程认为状态就应该是被隔离的.<br>这个习惯跟 js 那边也是不一样...<br>js 大家用 Vue 或者 Mobx 习惯了, 就会习惯到处用 observable 解决问题.</p>
<p>比如 Svelte 3 当中有一个例子, <code>doubled = count * 2</code>,<br>这是一个 Reactive 的值, <code>count</code> 改变, <code>doubled</code> 跟着改变, 最后界面也改变,<br>Svelte 当中用这样的代码来表示:</p>
<pre><code class="html"><script>
let count = 0;
$: doubled = count * 2;
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<p>{count} doubled is {doubled}</p></code></pre>
<p>参考: <a href="https://link.segmentfault.com/?enc=K4NUDS0lBHU1XuUPodUQmA%3D%3D.7FmU2NFLU7zNfgcEOfF8Ra8v6bXqlX1tnTIdGiXPxrvi1SM1TpXcIudyfvvdALlyo%2BjkTkUlMJ%2BlFo0OZLda3w%3D%3D" rel="nofollow">https://svelte.dev/tutorial/r...</a></p>
<p>函数式编程当中值是不会发生改变的, 所以没法对值进行监听,<br>那么, 函数式编程就会引入一个 wrapper 的概念, 比如说用 Monad 设计一个...<br>在 Clojure 当中, Atom 是一个, 或者用 Channel 来包裹这个随时间改变的数据...</p>
<p>在新的 Vue API 当中有了一个 <code>computed</code> 的概念,<br>这个 <code>computed</code> 封装过的数据, 就有个 <code>.value</code> 的引用, 表示最新的值:</p>
<pre><code class="js">import { value, computed } from 'vue'
const count = value(0)
const countPlusOne = computed(() => count.value + 1)
console.log(countPlusOne.value) // 1
count.value++
console.log(countPlusOne.value) // 2</code></pre>
<p>这个写法看着就跟 Clojure 当中的 ref, 用 Atom 包裹一个值很像了.<br>所以就感觉想法可能越来越像了.. 特别是引入不可变数据又需要数据被监听同时被传递的时候...<br>js 原始的 Object 模拟的就是一块内存, 内存某个位置可以被修改,<br>对于 Reactive System 来说, 这个结构过于简单了... 业务当中又希望不能被随便修改, 又要能替换又能监听...<br>函数式编程在这方面有不少思考... 相互借鉴...</p>
<hr>
<p>留一个我厂(积梦)招聘的链接 <a href="https://link.segmentfault.com/?enc=phD%2Bi37a0gTwPkjKcF6Nwg%3D%3D.NejtadytOvkXpS4j4KCCJwTOE7oX7VvqFKsYx1gB5B54qe73DujZLf7eIhkJJYWo" rel="nofollow">https://github.com/jimengio/h...</a> 我们前端整体都是用 Immer 优化 React 的.</p>
尝试 Clojure Spec 的笔记
https://segmentfault.com/a/1190000019390308
2019-06-05T02:46:38+08:00
2019-06-05T02:46:38+08:00
题叶
https://segmentfault.com/u/tiye
2
<p>工具当中需要检测数据格式, 试着用了一下 Clojure Spec.<br>如果英文好, 直接看文档就行了, 也不用这篇笔记, 太琐碎了, 也缺失例子...<br><a href="https://link.segmentfault.com/?enc=PNDxOC5MUTAJvm87b1JR1w%3D%3D.bwW5zaPMM9GhG%2F50WZf5cftOGt5j%2FtgejCWE7oI2%2FrzFYiCA%2FZ3X52qHVVur1gWA" rel="nofollow">https://clojure.org/guides/sp...</a></p>
<p>例子我整理在了 <a href="https://link.segmentfault.com/?enc=AX0FAJOwMs9geF8lLzOXDQ%3D%3D.uRkTzuhUk9vVpxyTNvsUv6bDw99XyzCaRe1qg1q4skPq%2BuaJmHprm%2B0sczSQX023" rel="nofollow">spec-examples 仓库</a>, 可以用 Lumo 直接跑.</p>
<p>首先添加依赖, 因为我在 ClojureScript 当中用, 所以用了 <code>cljs.spec</code> 这个代码.<br><a href="https://link.segmentfault.com/?enc=8ziODLhIuAXfeR0cf%2FueRg%3D%3D.IT33puzg7dvHe08LFabsTKjN%2FJVTAgP8TE73jtrqh4M%3D" rel="nofollow">expound</a> 是一个用于美化 Spec 输出的类库, 直接引用进来.</p>
<pre><code class="edn">[cljs.spec.alpha :as s]
[expound.alpha :refer [expound]]</code></pre>
<p>首先是一个很简单觉得例子, 有 <code>s/valid?</code> 判断数据是否符合格式.<br>首先用 <code>s/def</code> 定义好一个校验的规则, 其中 <code>::example</code> 会按照命名空间展开.</p>
<pre><code class="clojure">(s/def ::example boolean?)
(println (s/valid? ::example 1)) ; false
(println (s/valid? ::example true)) ; true</code></pre>
<p>基础的校验用的是函数, 也可以是 <code>string?</code>.</p>
<p><code>s/conform</code> 表示返回输出的值.. 当然这个是正确的情况, 返回了匹配到的字符串,</p>
<pre><code class="clojure">(s/def ::example string?)
(println (s/conform ::example "DEMO")) ; DEMO</code></pre>
<p>如果不匹配, 返回值就是 invalid,</p>
<pre><code class="clojure">(println (s/conform number? ""))</code></pre>
<pre><code class="edn">:cljs.spec.alpha/invalid</code></pre>
<p>可以通过 <code>s/explain</code> 来打印失败的原因,</p>
<pre><code class="clojure">(s/def ::example string?)
(println (s/explain ::example 1))
; 1 - failed: string? spec: :app.main/example</code></pre>
<p>可以看到这个原因比较精确, 但是可读性不怎么样, 就可以用 <code>expound</code> 替换了, 可读性会好很多,</p>
<pre><code class="clojure">(s/def ::example string?)
(println (expound ::example 1))</code></pre>
<pre><code class="text">-- Spec failed --------------------
1
should satisfy
string?
-- Relevant specs -------
:app.main/example:
cljs.core/string?
-------------------------
Detected 1 error</code></pre>
<p>既然校验规则是函数, 也可以写成,</p>
<pre><code class="clojure">(s/def ::example #(and (> % 6) (< % 20)))
(println (s/valid? ::example 1)) ; false
(println (s/valid? ::example 10)) ; true
(println (s/valid? ::example 20)) ; false</code></pre>
<p>校验规则也可以组合使用, 最简单就是 <code>s/or</code>, 注意参数中奇数位置都用的 keyword,</p>
<pre><code class="clojure">(s/def ::example (s/or :as-number number? :as-boolean boolean?))
(let [data 0]
(if (s/valid? ::example data)
(println (s/conform ::example data))
(println (expound ::example data)))))</code></pre>
<p>打印的结果是,</p>
<pre><code class="edn">[:as-number 0]</code></pre>
<p><code>s/or</code> 里直接用函数式简写了, 可以专门定义两个规则出来, 然后再使用,</p>
<pre><code class="clojure">(s/def ::boolean boolean?)
(s/def ::number number?)
(s/def ::example (s/or :as-number ::number :as-boolean ::boolean))
(if (s/valid? ::example 20)
(println (s/conform ::example 20))
(println (expound ::example 20)))</code></pre>
<p>返回依然得到数据,</p>
<pre><code class="edn">[:as-number 20]</code></pre>
<p>对于数组的结构的数据, 用 <code>s/coll-of</code> 来判别,</p>
<pre><code class="clojure">(s/def ::number number?)
(s/def ::example (s/coll-of ::number))
(let [data [1]]
(if (s/valid? ::example data)
(println (s/conform ::example data))
(println (expound ::example data))))</code></pre>
<p>得到,</p>
<pre><code class="edn">[1]</code></pre>
<p><code>s/coll-of</code> 还支持比如 <code>:count</code> 这样的校验, 具体可以再看文档,</p>
<pre><code class="clojure">(s/def ::example (s/coll-of number? :count 2))
(defn task! []
(let [data [1]]
(if (s/valid? ::example data)
(println (s/conform ::example data))
(println (expound ::example data)))))</code></pre>
<pre><code>-- Spec failed --------------------
[1]
should satisfy
(= 2 (count %))
-- Relevant specs -------
:app.main/example:
(cljs.spec.alpha/coll-of cljs.core/number? :count 2)
-------------------------
Detected 1 error</code></pre>
<p>对于 Map, 用 <code>s/keys</code> 来判断, <code>:req-un</code> 表示必选项, <code>opt-un</code> 是可选项,</p>
<pre><code class="clojure">(s/def ::age number?)
(s/def ::name string?)
(s/def ::example (s/keys :req-un [::age] :opt-un [::name]))
(let [data {:age 1, :name "a"}]
(if (s/valid? ::example data)
(println (s/conform ::example data))
(println (expound ::example data))))</code></pre>
<p>得到,</p>
<pre><code class="edn">{:age 1, :name a}</code></pre>
<p>如果不满足校验规则, 会准确提示出来, 比如可选项的规则不满足,</p>
<pre><code class="clojure">(s/def ::age number?)
(s/def ::name string?)
(s/def ::example (s/keys :req-un [::age] :opt-un [::name]))
(let [data {:age 1, :name 1}]
(if (s/valid? ::example data)
(println (s/conform ::example data))
(println (expound ::example data))))</code></pre>
<pre><code class="text">-- Spec failed --------------------
{:age ..., :name 1}
^
should satisfy
string?
-- Relevant specs -------
:app.main/name:
cljs.core/string?
:app.main/example:
(cljs.spec.alpha/keys :req-un [:app.main/age] :opt-un [:app.main/name])
-------------------------
Detected 1 error</code></pre>
<p>上面用到的 <code>-un</code> 的后缀表示 "unqualified", 如果没有后缀, 意味着 keyword 要根据命名空间展开,</p>
<pre><code class="clojure">
(s/def ::age number?)
(s/def ::name string?)
(s/def ::example (s/keys :req [::age] :opt [::name]))
(let [data {:age 1, :name 1}]
(if (s/valid? ::example data)
(println (s/conform ::example data))
(println (expound ::example data))))</code></pre>
<p>于是就不满足了,</p>
<pre><code class="text">-- Spec failed --------------------
{:age 1, :name 1}
should contain key: :app.main/age
| key | spec |
|---------------+---------|
| :app.main/age | number? |
-- Relevant specs -------
:app.main/example:
(cljs.spec.alpha/keys :req [:app.main/age] :opt [:app.main/name])
-------------------------
Detected 1 error</code></pre>
<p>就需要改写一下 key, 也用 <code>::x</code> 的语法带上命名空间,</p>
<pre><code class="clojure">(s/def ::age number?)
(s/def ::name string?)
(s/def ::example (s/keys :req [::age] :opt [::name]))
(let [data {::age 1, ::name "a"}]
(if (s/valid? ::example data)
(println (s/conform ::example data))
(println (expound ::example data))))</code></pre>
<p>得到,</p>
<pre><code class="edn">{:app.main/age 1, :app.main/name a}</code></pre>
<p>Spec 也可以对字符串进行校验, 同时也可以解析得到数据,<br>其中需要用到 <code>(s/conformer seq)</code> 来对字符串进行转化...<br>这个写法目前我也不够清晰, 参考了一下例子,<br><a href="https://gist.github.com/thegeez/77aee6f0ebcf6a42aa7d893388502e40">https://gist.github.com/thege...</a></p>
<pre><code class="clojure">(s/def ::left-paren #{"("})
(s/def ::right-paren #{")"})
(s/def ::space (s/and string? (s/conformer seq) (s/+ #{" "})))
(s/def ::token (s/and string? (s/conformer seq) (s/+ #{"a" "b" "c"})))
(s/def
::example
(s/cat
:left-paren
::left-paren
:expr
(s/+ (s/or :token ::token :space ::space))
:right-paren
::right-paren))
(let [data (seq "(a b)")]
(if (s/valid? ::example data)
(println (pr-str (s/conform ::example data)))
(println (s/explain ::example data))))</code></pre>
<p>最终得到,</p>
<pre><code class="edn">{:left-paren "(", :expr [[:token ["a"]] [:space [" "]] [:token ["b"]]], :right-paren ")"}</code></pre>
<h3>更多</h3>
<p>另外关于 multi-spec 的例子, 还有生成代码的例子, 我在 GitHub 上整理了,<br><a href="https://link.segmentfault.com/?enc=Yj2jh4nVtjUmWzxXn3MTVw%3D%3D.ZqHupWu6PkaYe1oiNPyEtmlDRwqFaNpm9x%2FOSl7FVvsMZ8Cb8e3QA79SBdbcJ5QP" rel="nofollow">https://github.com/jiyinyiyon...</a><br>代码比较成就不复制了.</p>
<p>另外细节的功能没有记录, 具体要看官方文档. <a href="https://link.segmentfault.com/?enc=3SBtsUzLm%2FYTf11yVPnJ0A%3D%3D.cCtA7Y%2Fg4bT5dgQi4mLzOPmE%2BdTS3hciRkdO2VGOQJrmqjz97x7thLh4y9D%2B78BU" rel="nofollow">https://clojure.org/guides/sp...</a></p>
积梦前端的路由方案 ruled-router
https://segmentfault.com/a/1190000019143914
2019-05-11T00:03:00+08:00
2019-05-11T00:03:00+08:00
题叶
https://segmentfault.com/u/tiye
7
<p>积梦(<a href="https://link.segmentfault.com/?enc=INTa7pEdhJSU3bWfiZz%2Fiw%3D%3D.OrzHm9SMnib0Gu7NGyU%2FNy7TAUdO4X8iNfCevwTVas4%3D" rel="nofollow">https://jimeng.io</a> ) 是一个为制造业制作的一个平台.<br>积梦的前端基于 React 做开发的. 早期使用 React Router.<br>后来出现了一些 TypeScript 集成还有定制化的需求, 自己探索了一套方案.</p>
<h3>使用 React Router 遇到的问题</h3>
<p>React Router 本身是一个较为稳妥而且全面的方案, 早期我们使用了它.<br>后面随着积梦数据平台的页面的重构, 遇到了一些问题.<br>积梦的管理界面从顶层往内存在多个层级, 复杂的情况会出现五六层嵌套,</p>
<pre><code>导航栏 -> 子导航 -> 标签页 -> 功能 -> 子页面</code></pre>
<p>虽然一般的情况只是三四个层级, 但是页面的嵌套大量存在,<br>早期的我们办法是定义一个 <code>basePath</code> 变量用来表示外层路由,</p>
<pre><code class="tsx"><Route
path={`${this.props.basePath}/:page`}
render={(props) => {
return this.renderSubPage(props.match.params.page);
}}
/></code></pre>
<p>然后在内部跳转时, 也会使用 <code>basePath</code> 变量快速生成路径,</p>
<pre><code class="tsx"><Redirect to={`${this.props.basePath}/${EWorkflowPage.Step}`} /></code></pre>
<p>这样手动传递偶尔会出错, 特别是当页面结构发生一些修改的时候.<br>经过一两次导航的重构, 我们在局部出现了一些代码, 无法正确跳转.<br>虽然靠着测试逐步修复了问题, 但是随着页面增多, 这个问题不能轻视.</p>
<p>我觉得这个问题是两部分,</p>
<p>一方面是 TypeScript 的类型检查没有帮助到的路由部分,<br>React Router 当中基本上通过字符串定义的路径, 这些不容易被类型检查.<br>特别是拼接的路由, 发生改变以后就难以准确追踪了.</p>
<p>另一方面, 我认为 React Router 的规则也限制了 JavaScript 代码的使用.<br>相对于 React Router 通过 Context 传递路由状态的方案, 更倾向于代码.<br>基于 <code>switch/case</code> 还有函数组成的控制流, 有更为灵活的应对的办法.</p>
<h3>路由的解析 <a href="https://link.segmentfault.com/?enc=4U%2F8jL%2B27s5GTI1g48kpnA%3D%3D.fdc%2F6VjpAIq9kSMllsCl1Ow5MeqxMX4sN9hnLiUalzqOad1%2FtOqKsESB0u7TX%2BUM" rel="nofollow">ruled-router</a>
</h3>
<p>我同事和我都有一些使用基于路由配置生成路由的经验, 商量后我打算尝试.<br>我的想法是定义路由规则, 然后将路由解析称为对象, 然后通过代码进行控制.</p>
<p>比如这样一个路径:</p>
<pre><code class="bash">/plants/152883204915/qualityManagement/measurementData/components/21712526851768321/processes/39125230470234114</code></pre>
<p>进行拆解以后我认为就是几个层级:</p>
<pre><code class="text">/plants/152883204915
/qualityManagement
/measurementData/components/21712526851768321/processes/39125230470234114</code></pre>
<p>跟 React Router 直接用标签做匹配的写法不同, 我认为路由应该先被解析,<br>该路由包含了页面的信息, 也包含了响应的参数, 实际上对应一个链表, 用对象表示是:</p>
<pre><code class="js">{
"name": "plants", // <--- 第一层路由
"matches": true,
"restPath": null,
"data": {
"plantId": "152883204915"
},
"query": {},
"next": {
"name": "qualityManagement", // <--- 第二层路由
"matches": true,
"restPath": null,
"data": {},
"query": {},
"next": {
"name": "measurementData", // <--- 第三层路由
"matches": true,
"restPath": null,
"data": {
"componentId": "21712526851768321",
"processId": "39125230470234114"
},
"query": {},
"next": null
}
}
}</code></pre>
<p>这是一个比较清晰的层级的结构, 很容易用 <code>switch/case</code> 判断渲染对应的子页面.</p>
<p>而解析这个路由所需要的规则, 也可以通过大致这样的代码定义出来.</p>
<pre><code class="ts">let pageRules = [
{
path: "plants/:plantId",
next: [
{
path: "qualityManagement",
next: [
{
path: "measurementData/components/:componentId/processes/:processId"
}
]
}
]
},
];</code></pre>
<p>这样基于路由规则和解析函数, 路由定位的方案就变成了:</p>
<ul>
<li>从 URL 改变的事件获取到 <code>location.hash</code> 的字符串,</li>
<li>用函数解析得到路由信息的 JSON 树,</li>
<li>根据 JSON 逐级传递, 用 <code>switch/case</code> 跳转到对应的页面.</li>
</ul>
<p>示例代码比如:</p>
<pre><code class="tsx">render() {
const nextRoute = this.props.route.next;
switch (nextRoute && nextRoute.name) {
case RouteOutgoing.Records:
return <Records route={nextRoute.next} plantId={plantId} />;
case RouteOutgoing.Settings:
return <Settings route={nextRoute} />;
}
return (
<Redirect
to={router.getPath(RouteOutgoing.Records, {
plantId,
})}
/>
);
}</code></pre>
<p>解析的代码在 <a href="https://link.segmentfault.com/?enc=yc3cWvY7sqMYNwZTmxchiQ%3D%3D.PgwxTgMHap%2Fpnhv%2FRL4EOCPirV87YxBZe1F%2BlDmjX0UT1C4uEdjRb10qx%2BeYL25O" rel="nofollow">ruled-router</a> 可以找到, 使用 TypeScript 开发, 有基础的类型约束.</p>
<p>从代码看, 由于路由层级的显式处理, 会存在不少的 <code>.next</code> 需要手工维护, 对于维护有些啰嗦.<br>当然这个写法好的一面是路由信息随时可以打印和调试, 方便定位问题.</p>
<h3>路由的跳转(code generator)</h3>
<p>在 React Router 当中路由的跳转相对简单, 提供路径的字符串表示即可完成:</p>
<pre><code class="ts">history.push('/a/b/${c}/d')</code></pre>
<p>但是前面说了, 这样无法进行类型检测, 无法定位出现问题的路由位置.<br>我们尝试了几个方案, 用比较多的一个方案是给路由定义唯一的 ID 的枚举值, 然后查找枚举值跳转.<br>后来我从另一个思路开始尝试, 试着用不同的方案来搭配 TypeScript.</p>
<p>比如说这样的一套规则, 定义 3 个页面:</p>
<pre><code class="ts">let routeRules = [
{ path: "home" },
{ path: "content" },
{ path: "else" },
{ path: "", name: "home" }
]</code></pre>
<p>那么对应这个路由我就生成响应的代码, 这段代码, 就是 TypeScript 可以做类型检查的了,</p>
<pre><code class="ts">export let genRouter = {
home: {
name: "home",
raw: "home",
path: () => `/home`,
go: () => switchPath(`/home`),
},
content: {
name: "content",
raw: "content",
path: () => `/content`,
go: () => switchPath(`/content`),
},
else: {
name: "else",
raw: "else",
path: () => `/else`,
go: () => switchPath(`/else`),
},
_: {
name: "home",
raw: "",
path: () => `/`,
go: () => switchPath(`/`),
},
};</code></pre>
<p>其中 <code>.go()</code> 方法用于跳转, <code>.path()</code> 方法用于生成其他组件需要的字符串形态.<br>当然, 维护这样的一段代码, 成本并不低, 但是好在这样高度重复的代码是可以用代码生成的,<br>于是我们增加了 <a href="https://link.segmentfault.com/?enc=wkR2OtrfYjRIUiQeKYAvHA%3D%3D.L0rLBvwS4agZ9qa8JEP1f%2BCOdyyffisa7TjmCgjuKbO9%2FEAGf1QlicYfc5%2FflfXOjA28TMAS9u0R1ZFU3apDCw%3D%3D" rel="nofollow">router-code-generator</a> 这个脚本, 用于生成路由代码.</p>
<p>这样, 添加新路由的时候就需要,</p>
<ul>
<li>在 rules 当中添加路由规则,</li>
<li>运行脚本生成路由的代码,</li>
<li>在需要跳转的位置引用 <code>genRouter</code> 对象, 调用对应方法进行跳转.</li>
</ul>
<p>实际业务当中的代码当然会复杂很多, 项目最终生成出来是两千多行的路由文件,</p>
<pre><code class="ts">export let genRouter = {
plants_: {
name: "plants",
raw: "plants/:plantId",
path: (plantId: Id) => `/plants/${plantId}`,
go: (plantId: Id) => switchPath(`/plants/${plantId}`),
information: {
name: "information",
raw: "information",
path: (plantId: Id) => `/plants/${plantId}/information`,
go: (plantId: Id) => switchPath(`/plants/${plantId}/information`),
products: {
name: "material.finished",
raw: "products",
path: (plantId: Id) => `/plants/${plantId}/information/products`,
go: (plantId: Id) => switchPath(`/plants/${plantId}/information/products`),</code></pre>
<p>实际项目当中的脚本生成也是个需要处理的地方, 我们用 Webpack 将这部分代码打包运行,<br>性能上还好, 关掉类型检查的话几秒钟内可以完成, 具体看示例的代码:<br><a href="https://link.segmentfault.com/?enc=e6CBcEnLo2BHtwmnvOw2HQ%3D%3D.b1MrKAdEIrQlShrvSvuPAXPz05Gn0TPgwV6atHBiIIA759%2FvbG86mbI3woVjFF9aP5Hyfd2pZDcBvpUxP1oj6I4HCKCSJRHQreA5WQLm9tQ%3D" rel="nofollow">https://github.com/jimengio/t...</a></p>
<h3>类型检查的覆盖</h3>
<p>前面的两部分, 覆盖了路由的解析, 还有路由的跳转, 完成了基本的路由的功能.</p>
<p>路由解析部分, 路由规则可以通过 JSON 结构定义, 基本能得到 TypeScript 的提示.<br>路由的解析结果, 是一棵大的 JSON 的树, 这中间有不少动态的部分, 需要开发时自己留意.<br>路由跳转的代码, 整个 Object 定义的结构可以被 TypeScript 解析, 基本上有完整的补全.</p>
<p>虽然并不完美, 但是很大程度利用了 TypeScript 的自动补全以及类型检查简化了书写.<br>当路由有增改时, 通过运行脚本还有执行类型检查, 比较容易定位到发生改变的部分.</p>
<h3>该方案的不足</h3>
<ul><li>路由劫持等功能</li></ul>
<p>React Router 提供的功能显然远不止解析和跳转, 还有一些页面跳转相关的钩子, 甚至渐变等效果.<br>ruled-router 的方案没有去实现相关的功能.</p>
<ul><li>脚手架比较麻烦</li></ul>
<p>从前面的描述也能看出来, 这一整套写法, 特别是后面跳转的写法, 引入了大量脚手架.<br>需要专门写一个 Webpack 配置来生成路由, 一般项目来说觉得非常繁琐了.</p>
<p>实际在项目当中, 由于我们有着较深的路由层级, 实际代码看上去又长又啰嗦:</p>
<pre><code class="ts">genRouter.plants_.product.batch_.ooc.go(plantId, value);
genRouter.plants_.model.projects._status.go(this.props.plantId, record.id);</code></pre>
<p>这代码是靠着 VS Code 提供的代码补全才能很快写出来... 也就是和 TypeScript 以及 VS Code 等工具绑定死了.</p>
<h3>结尾</h3>
<p>除了上面介绍的, 其他一些功能也在 ruled-router 方案里做了一些支持:</p>
<ul>
<li>Query 参数. 可以被解析, 也可以在跳转代码当中被生成出来. 基本可用. 只是类型有缺失.</li>
<li>性能优化的问题, 需要配合 <code>shouldComponentUpdate</code> 或者 <code>useMemo</code> 来优化, 就用到易于匹配的字符串形态.</li>
</ul>
<p>特别是随着项目规模增加, 几百个大小页面的木有, 更多会需要类型检查工具来帮助我们做校验.<br>当然目前的方案在开发当中依然有着细节上的各种需要优化的地方, 要后续再想办法进一步优化.</p>
<p>其他关于积梦前端的模块和工具可以查看我们的 GitHub 主页 <a href="https://link.segmentfault.com/?enc=TO4mODqlAFm7yZyrBlTmzw%3D%3D.FfEjVp8XQYeLm%2B4paVyYPj0mG7mCAPdVM16zSm528xw%3D" rel="nofollow">https://github.com/jimengio</a> .<br>招聘的计划和条件也在 GitHub 上有给出 <a href="https://link.segmentfault.com/?enc=Da7o8mUej7DbbfDOXhWm0A%3D%3D.EyHM8Q8xRnzgtWGHOsT8VGR1GKf16Kei4XbV8jDkWzM5xlsXHPvS0ZaKHR7ZSryW" rel="nofollow">https://github.com/jimengio/h...</a> .</p>
积梦前端采用的 React 状态管理方案: Rex
https://segmentfault.com/a/1190000018940757
2019-04-21T23:52:14+08:00
2019-04-21T23:52:14+08:00
题叶
https://segmentfault.com/u/tiye
10
<p>积梦(<a href="https://link.segmentfault.com/?enc=kA75ZE5s%2BpR0V4JTBmxPMg%3D%3D.mF68DM084ez5SQxDNdkgI1KBD96d7OWF6A1N2Pn01Tg%3D" rel="nofollow">https://jimeng.io)</a> 是一个为制造业制作的一个平台.<br>积梦的前端基于 React 做开发的. Rex 是我们在前端使用的状态管理方案, 类似 Redux.<br>从名字也可以看, Rex 是一个基于 Redux 做了大幅简化的方案.<br>另一方面, Rex 跟 Immer 有比较好的整合, 能够很轻松得使用不可变数据.</p>
<h3>先前的技术方案</h3>
<p>在开发 Rex 之前, 我们主要采用了 mobx-state-tree 的方案, 以及试验过 Redux.<br>最早的代码使用了 mobx 搭配 mobx-state-tree, 比较迎合 observe 的用法.<br>但是使用 mobx 全家桶遇到了一些比较困扰的问题,</p>
<ul>
<li>mobx 对数据封装的话,数据量比较大的时候初始化非常慢, 对应图表.</li>
<li>observable 数据调试很不方便, 打印在 Console 是一个难以读取的对象.</li>
<li>mobx-state-tree 内置了 types, 加上偶尔有改版, 经常出现不可控, 比如报错, 字段修改.</li>
</ul>
<p>由于我一直就是 immutable 数据的支持者, 就一直在试验能否用不可变数据解决这些问题.<br>但是早先主要是 immutablejs 方案, 按照以前的使用经验, 成本比较高.<br>后来出现了 immer, 在工业聚当有 Micheal 的介绍下我们开始局部尝试, 取得了不错的效果.<br>而且因为 immer 也是 mobx 全家桶作者 Micheal 发布的模块, 使用也比较顺畅.</p>
<p>最初我尝试过用 immer 搭配 Redux 来局部替换一些全局状态,<br>试验之后我觉得效果上没有达到预期,</p>
<ul>
<li>Redux 的 action/dispatch 在 JavaScript 当中没有足够灵活,<br>在函数式语言比如 Clojure 当中, 一切解释表达式, 默认不可变数据, 处理 action 非常顺畅,<br>但是用 JavaScript, 加上 immer 之后好一些, 但还是需要显式地到处引入 immer.</li>
<li>TypeScript 类型跟 Redux 配合比较麻烦, 需要非常明确定义好各个 Action.<br>在 ReasonML 当中用代数类型很容易定义不同的 Action, 而 TypeScript 相对繁琐.<br>而且早先因为代码处理不干净, 类型推断并不是生效, 影响了实际体验.</li>
</ul>
<p>所以 Redux 方案没有按预期地推进下去.</p>
<h3>Rex 的特点</h3>
<p>其实不管 Redux 还是 mobx-state-tree. 我想要的还是状态透传的功能.<br>通过 <code>@connect(() => {})</code> 来封装组件, 让局部能获得访问全局状态的能力.<br>至于具体的数据操作, immer 已经做到我们可以接受的程度了.</p>
<p>后来在知乎看到过别人模仿 Redux 开发的类库, 我萌生了自己裁剪 Redux 代码的想法.<br>在同事的帮助下优化了 decorator 部分的代码, 我大致梳理出这样一个类库,</p>
<ul>
<li>基于 Context 实现 <code>@connect()</code> 的语法, 进行数据透传,</li>
<li>用 immer 维护全局数据, 并且暴露出方便使用的方法.</li>
<li>基于以前的代码, 大致处理好监听状态改变的的逻辑.</li>
<li>生成 TypeScript 使用的类型文件.</li>
</ul>
<h3>Rex 的使用</h3>
<p>目前 Rex 经过半年多的使用验证, 大致已经趋向稳定, 代码在 GitHub 上可以查看,<br><a href="https://link.segmentfault.com/?enc=qSbshL0Y1Y8PIrXcPVYwaA%3D%3D.j%2FNE1QGkuB8mWk0Qq0PORTUvxAFCW9Nhr1tkAAkhzA4%3D" rel="nofollow">https://github.com/jimengio/rex</a><br>或者通过 npm 安装到本地,</p>
<pre><code class="bash">npm install @jimengio/rex</code></pre>
<p>使用 Rex 首先就是要定义全局状态的结构, 比如:</p>
<pre><code class="ts">export interface IGlobalStore {
obj: {
a: number;
};
b: string;
}
export let initialStore: IGlobalStore = {
obj: { a: 2 },
b: "b"
};</code></pre>
<p>然后初始化一个 <code>globalStore</code>, 包含该状态:</p>
<pre><code class="ts">import { createStore } from "@jimengio/rex";
export let globalStore = createStore<IGlobalStore>(initialStore);</code></pre>
<p>这里如果你想获取 store 的状态, 通过一个方法来读取,</p>
<pre><code class="ts">globalStore.getState()</code></pre>
<p>以及监听 store 的改变, 处理重绘:</p>
<pre><code class="ts">globalStore.subscribe(() => {
// rerender
});</code></pre>
<p>当你要对数据进行操作时, 有两个方法可以使用, <code>update</code> 和 <code>updateAt</code>.<br>这两个方法直接将 immer 封装在内, 虽然是赋值操作, 但实际上是不可变数据,<br>如果你对 immer 有疑问, 请仔细阅读它的文档并自行试验 <a href="https://link.segmentfault.com/?enc=q%2F9HWPyfL2JlQRDU3MHZyw%3D%3D.sl0C8Xsc7c%2Fyq0Rss6Zb4zO7%2BLblvagAt%2FTY6uhUQixBiO8vV7SxYnPPnvxL1nCO" rel="nofollow">https://github.com/mweststrat...</a><br>其实 <code>updateAt</code> 是 <code>update</code> 的语法糖, 对于特定分支的数据的修改相对方便:</p>
<pre><code class="ts">export function doIncData() {
globalStore.update((store) => {
store.b = "modified data";
});
globalStore.updateAt("obj", (obj) => {
obj.a += 1;
});
}</code></pre>
<p>这个抽象大致从 Clojure 的 Atom 借鉴, 数据并不能直接修改.<br>如果你要修改状态, 就需要发送一个函数给 Rex, 然后 Rex 会在内部进行修改.</p>
<p>此外也提供了 <code>RexProvider</code> <code>connectRex</code> <code>useRexContext</code> 等函数, 跟 Redux 习惯尽量一致.<br>基于这些函数, 就能实现一个简单的状态管理, 以及数据更新的透传.</p>
<h3>一些需要注意</h3>
<p>Rex 实现较为简单, 目前没有做更深层的优化, 也在避免引入新概念.<br>实际使用除了一些模板代码, 主要是 immer 的不可变数据需要注意.<br>immer 使用的是可变数据的写法, 但是内部通过 proxy 机制进行了转化, 达到不可变数据的效果.<br>平时写代码的时候需要注意区分可变和不可变的数据, 比较写法上是类似的.</p>
<p>另外要注意上面比如 <code>doIncData</code> 函数, 如果内部包含异步操作, 需要注意,<br>Rex 并不能支持异步, 所以在异步事件前后, 都需要直接调用 <code>.update()</code>,<br>也就是说对应会有两个(甚至多个)更新事件, 界面会更新多次.</p>
<p>Rex 封装时, 考虑到性能问题, 做了一些基本的 <code>shouldComponentUpdate</code> 检测.<br>不过对于 <code>useRexContext</code> 这个 Hooks 的写法. 目前没有想明白怎么样处理, 需要手动处理.</p>
<h3>其他</h3>
<p>其他关于积梦前端的模块和工具可以查看我们的 GitHub 主页 <a href="https://link.segmentfault.com/?enc=12ahVqtCitGNAj%2FRnbcsag%3D%3D.As%2BdnGXdYYWzpJWC1qYPKWTH1Ie%2BxDBs8brgmY7n370%3D" rel="nofollow">https://github.com/jimengio</a> .<br>招聘的计划和条件也在 GitHub 上有给出 <a href="https://link.segmentfault.com/?enc=%2Br6I70wrFg3TNo2R4zx4FQ%3D%3D.DPU6dbJ%2BP9kT%2BxwZrdrui889xZBRcopZfUSNI4GdNr2Hk4JUWh790vPat32uBvod" rel="nofollow">https://github.com/jimengio/h...</a> .</p>
笔记, 创建 VS Code 的 snippets 扩展
https://segmentfault.com/a/1190000018819751
2019-04-10T15:30:45+08:00
2019-04-10T15:30:45+08:00
题叶
https://segmentfault.com/u/tiye
1
<p>最近考虑把公司的常用代码用扩展处理成复用的, 照着文档处理了一遍.</p>
<h3>Snippets 扩展</h3>
<p>关于扩展的结构, 可以直接参考官方给出的示例,<br>一个 <code>package.json</code> 文件, 加上一个 Snippets 的 JSON 文件, 就算写完了:<br><a href="https://link.segmentfault.com/?enc=aIrnwq%2F3y%2BJupydPp7ay8Q%3D%3D.r3eJ90Mv6dQDp%2Fwaet7Xj%2Ffq9L8sR0CU1EAzaVBpEX8ZVIBtk3O1nn4dPRYgq44mAG0u4B0Icvts1f4PZchoX4HUlBng1Q0bbQJ4NqKkN0iESAOUNbYlBJsE03sw9pjP" rel="nofollow">https://github.com/Microsoft/...</a></p>
<p>其中 Snippets 的定义大概是这样,</p>
<ul>
<li>key 跟 <code>description</code> 只是介绍性文字</li>
<li>
<code>prefix</code> 是 snippet 在自动补全当中的触发字段</li>
<li>
<code>body</code> 当中的代码片段, 分开是换行(<a href="https://link.segmentfault.com/?enc=YxWpROJZpiOTgOx9e6Gtag%3D%3D.FG33hjaa5j4BpTU9XryzRvwGrlOjw%2BYaWBfRwimbJhSXGiwBvkgoKAoiVX%2F0japW" rel="nofollow">做了个小工具来把多行拆成数组</a>)</li>
<li>
<code>$1</code> <code>${1:placeholder}</code> 这样的写法, 表示 Tab 控制的光标位置, 其中 <code>$0</code> 表示结束位置</li>
</ul>
<pre><code class="json">{
"For_Loop": {
"prefix": "for",
"body": [
"for (const ${2:element} of ${1:array}) {",
"\t$0",
"}"
],
"description": "For Loop"
}
}</code></pre>
<p>复杂的例子就看别人写的扩展了:<br><a href="https://link.segmentfault.com/?enc=BcXSKFDPK2BwXiHpKtDpLg%3D%3D.oO1JQlXAIkApIG2svRfmCBCffxR50tblVEP48THWxgNSJF%2FUg8jzMgNQ6yRcLWPG" rel="nofollow">https://github.com/xabikos/vs...</a></p>
<p>VS 还支持一些复杂的功能, 直接看文档上:<br><a href="https://link.segmentfault.com/?enc=wWpyD7US72XN9zhCV8Wg0w%3D%3D.RTagO4N4MWrDsOI33VJ1IOOH5Yjpf815p%2BxTunUkycvdc5RWBG%2BjRyXVDP3d7UdU%2Bov5qUSFMlAg5SdZF73hS9OsAlfIvEVk003tdLCPkjxAj5R5ausvPxsOkRYFf8KJ" rel="nofollow">https://code.visualstudio.com...</a></p>
<h3>安装扩展</h3>
<p>安装扩展需要一个命令行工具, 在 <code>package.json</code> 所在目录运行:</p>
<pre><code class="bash">npm install -g vsce
vsce package</code></pre>
<p>这时会生成一个扩展名为 <code>.vsix</code> 的文件, 就可以通过 VS Code 的命令安装了:</p>
<pre><code class="bash">code --install-extension my-extension-0.0.1.vsix</code></pre>
<p>这一步也可以通过 <a href="https://link.segmentfault.com/?enc=4XoD%2FR%2B6SjtKZRkTU7IBwQ%3D%3D.Sv4Zr23vi78jfziIGUNon7j84gBDI%2FQefhDC3bIprm79rGnsXiUHMMeD4IUUGBLqig7mF87V0ITLHi05dNcR9AgPu4UURP6G74jW0kbanfSwsY%2B%2FRsTQvqTDN9TYfX2F" rel="nofollow">VS Code 图形界面</a>来完成:</p>
<p><img src="/img/remote/1460000018819754" alt="" title=""></p>
<h3>发布扩展到市场</h3>
<p>说的话大致步骤不麻烦, 大致上:</p>
<ul>
<li>先到 <a href="https://link.segmentfault.com/?enc=WPeNs5SI470OGKTsJWaNJA%3D%3D.K%2FrbhIVgPUVDn05pgJy173%2Fn8AFe9eXxttAgxqV5hsQ%3D" rel="nofollow">https://dev.azure.com/</a> 上注册账号, 创建组织,</li>
<li>在 dev.azure 访问自己的 security 页面创建一个 token, 选中所有 Marketplace 权限,</li>
<li>在组织当中创建一个 creator, 命令行或者界面都能操作, 把 creator id 写进 <code>package.json</code>,</li>
<li>然后通过 <code>publish</code> 子命令发布...</li>
</ul>
<p><a href="https://link.segmentfault.com/?enc=W2ndvGg2kdt%2Bt%2BRH%2BU%2FO7w%3D%3D.MZHCBwi7n2fCVGBzmDchqkUPrWZNuc3%2F5CTJTgSpmkrsa0oEE%2FPxH8RWRojpJ95OgpIyXyudKooldTHEJCw4PyZgdRpc51xxm9Nf7vCNjlE%3D" rel="nofollow">https://code.visualstudio.com...</a></p>
<p>不过...实际操作挺多坑的, 网络有时候不好, 提示也不明确, 我浪费了不少的时间查原因.</p>
<h3>结尾</h3>
<p>VS Code 用支持 <code>.vscodeignore</code>, 也支持 <code>icon</code>, 结构跟 npm 模块也类似.</p>
<p>最后是我厂的 Snippet 代码, 目前功能很少, 后续慢慢扩展:<br><a href="https://link.segmentfault.com/?enc=ZszAt5b%2B3nNBn0iAYqvxXQ%3D%3D.wQukGgw1NHk284lcw%2F60cXUmqBkfeCiyiwnBpgKRWZMR0O3RJgoZqhGYyPtiyrU2" rel="nofollow">https://github.com/jimengio/j...</a></p>
跟帖"我对技术会议的一些看法"
https://segmentfault.com/a/1190000017328660
2018-12-11T00:57:24+08:00
2018-12-11T00:57:24+08:00
题叶
https://segmentfault.com/u/tiye
8
<blockquote>今天看到勾三股四的文章<a href="https://link.segmentfault.com/?enc=RgizMChwa835piSA%2FsT0Zg%3D%3D.wOILxdYIc3Q1v52pTFrUJMutko1nHGGVzod4gXfKLmchsq5XwkaWxD%2FKaIYXb8L65%2FzbtIt78Ww6GynS15ugsQ%3D%3D" rel="nofollow">我对技术会议的一些看法</a>,<br>有些感想, 也想把自己感受到的写出来, 大致顺着勾股的文章做感想吧.</blockquote>
<p>秋天的时候整理的吊牌... 这些年压下来已经挺多的了.</p>
<p><img src="https://wx1.sinaimg.cn/mw690/62752320gy1fwmthicxmpj22c02c04qs.jpg" alt="" title=""></p>
<p>这些年参加的会议都是国内的, 之前是同城的聚会, 然后有一些其他城市的活动,<br>最远的一次跟着勾股去了台湾那边参加 ModernWeb, 体验也更特殊一些.</p>
<h3>会议的意义</h3>
<p>我觉得网络发达了真的对技术会议有不小的改变, 特别是视频的传播,<br>国内的开发者应该也感觉得到, Google Apple 那些大会我们都没机会去,<br>但是会议结束之后, 那些视频往往会被整理出来, 全世界的开发者都有机会看,<br>视频的好处, 可以暂停, 可以快进, 可以选择, 纯粹出于学技术的目的, 完全足够.<br>所以我会觉得更多是交流的意义, 面对面那种投入感, 信任感, 能进行更深入的交流.</p>
<p>当然, 分享也是挺重要的. 不做分享, 陌生人交流会尴尬, 不好进入话题.<br>熟人之间当然是很容易进入到相互熟悉的话题的, 因为铺垫比较充足,<br>但是陌生人之间要沟通, 首先要有一定的了解, 然后有一些共通的知识基础.<br>我当然也相信有些人口才好, 陌生也能聊起来, 但是我想, 更多人还是需要先相互有所了解.</p>
<p>我幸运的地方是, 我因为 React 认识了不少人, 所以在大会上能有熟人交流了,<br>但我最初去 D2 那样的会, 跟着五花肉问, 放眼望去总共就认不到几个人,<br>那种情况下交流的障碍带来的隔阂我也是能体会到.<br>但是她当时说的蛮有道理, 先要有些认识的人, 然后才能认识更多的人,<br>至少做些准备吧, 匆匆忙忙的机会本来也就不是那么多, 总不能不说话就结束了.</p>
<h3>审稿</h3>
<p>我觉得审稿有一定的必要, 之前有机会旁听过贺老给人审 review Qcon 试讲,<br>对于演讲质量和演讲技巧来说, 有不小帮助的, 主要是对演讲经验少的帮助更多一些,<br>程序员很多都在埋头做技术, 锻炼演讲技能的机会不是那么多,<br>我的经历, 如果不是 Teambition 当年有内部的分享, 我最开始的演讲底子就很欠缺.<br>这两年工作节奏忙这方面做的不够, 所以我能感觉到审稿和试讲会有不少帮助.</p>
<p>而且稿子的内容, 长短, 深度, 是否跟其他嘉宾有重叠, 我觉得也是主办方需要考虑的,<br>倒不是说要主办方控制讲师讲的是什么, 而是说根据观众群体水平调整一下侧重,<br>有时候大会上嘉宾内容之间深度会有明显的反差, 这时候事先通气总是能调整一些的.</p>
<h3>会前预热</h3>
<p>参会是交流的好机会, 但是很多次, 会发现等到认识了, 会议已经散场了,<br>如果说接下来晚饭或者第二天有活动, 大家还能一起继续交流交流,<br>不然的话, 各自就回到自己城市去了, 下一次碰面不知道什么时候.<br>所以我觉得, 会议开始之前进行预热, 很多时候能带来好处.<br>深圳 JSConf 那年, 就是提前有建群, 然后大家出车站机场就有在指路或者约饭的,<br>小圈子里当然建群还更早一些, 熟人可能更早约见面了.</p>
<p>我个人比较期待的话, 如果可以带着自己代码的 Demo 去交流, 效果会更好,<br>其实很多说的事情, 你在网上说, 或者看新闻说, 是相似的,<br>或者见面的时间那么短, 能说的事情反而可能不如在网上能展开的细节来得多,<br>但是 Demo 的话, 现场演示和交流还是比录视频去演示要好的,<br>随时能跟着对方的想法去调整, 去解释, 会更有效果. 也明白哪里更重要.</p>
<p>而且可能提早一段时间去讲师的微博 Twitter 上熟悉一下相关领域还更好一些,<br>会场要上去聊天的话, 对方近期做的一些事情当然了解一下是更好,<br>特别是相互之间做的事情有没有交集, 有没有项目方面搭上的机会...<br>我站到讲师的角度, 我也希望有人到我面前说事情之前, 微博上是打过招呼的.</p>
<h3>交流工具</h3>
<p>目前为止我看到的, 大陆的会议, 如果交流, 都是用的微信,<br>台湾那次比较邪乎, 他们用的 Gitter, 结果人真的少, 听不到什么八卦,<br>有微信其实蛮不错的, 主办方能很快收到反馈, 有什么问题, 都很快.</p>
<p>微信麻烦的就是刷屏了, 话题也很容易带偏, 几个人聊着很容易被大部队冲散了,<br>所以我一直有点期望能有个两个维度的聊天工具, 论坛帖子那样的,<br>聊着聊着能很容易走近小圈子, 这样有些技术问题能在时间线当中延续.</p>
<p>还有一个就是陌生人的话, 你很难把他微信跟他在技术社区的身份很快关联起来,<br>网上的话, 看看这人 GitHub 或者中文版 StackOverflow, 或者博客, 能看到水平,<br>技术会议上看着别人主页晒美食晒行程总觉得哪里不大对....</p>
<h3>本地聚会和全国会议</h3>
<p>参加大会, 有一个问题是, 一些新人去了, 其实很难找到机会跟有名气点的人聊天,<br>如果害羞, 话都没机会说, 如果没有知识储备, 聊两句就聊不下去了,<br>也不能指望对方一定是耐心的人, 毕竟一天时间另一个人还想找别人聊天呢,<br>对于新手来说, 更需要的是本地小规模的聚会, 甚至多接触经验相近的人.</p>
<p>相比各种大会, 我觉得地区性的小型聚会需要更多的人去组织,<br>我在上海, 参与过很多次朋友办的 Linux 用户组的分析, Elixir 社区的分享,<br>也有参与过比较少的 Ruby 社区的活动, 或者 FreeCodecamp 的活动,<br>上海这些活动应该说比较好, 相对其他城市加班少一些, 人口数量大.<br>但是小型的活动挺难延续的, 找场地, 找话题, 都要免费, 比较难凑起来.</p>
<p>之前同城的 Ruby 聚会, Elixir 聚会, 给我的感觉更好一些,<br>人少, 相互都能认识, 交流的时间也稳定一些, 也不像大会那么拘谨,<br>这种氛围对于经验少一些的人更合适一些了, 而且也不用去愁很贵的门票.</p>
<p>而大会的话, 听说是门票钱很多耗在了外国讲师的行程上面(没去确认过),<br>我也当过讲师, 能感觉到机票食宿方面如果折腾起来, 都挺耗钱的...<br>这对很多新人来说是吃亏的地方, 因为大老远请来了嘉宾, 你一句话都搭不上,<br>而且在会场认识的人多了, 能做的事情也就更多, 这是有点两极分化的.</p>
<h3>英语</h3>
<p>广州这次终于壮着胆子跟国外讲师套近乎了, 我口语也不够流利,<br>听 Sean 聊天给我的感觉, 他在国外各种分享认识了好多人,<br>所以他接触过很多的想法, 不同的技术氛围, 我们在国内很难碰到,<br>我是在讲师聚餐时候听到讲的, 所以听了总比平时碰到能听到的多,<br>当然相应的, 需要的知识储备, 不同的方面的东西, 也要求会多一些...</p>
<p>然后问题就是, 国内的话, 越是年纪大的人当中, 英语好的人越少, <br>这么一趟下来我更觉得英语重要了, 或者说对英语好的人是个机会, 万一突然红了呢!<br>说不定哪天开会的时候就要征志愿者给他们配翻译呢, 谁知道呢...<br>我觉得外国讲师应该也是会觉得国内生态有什么有意思的, 也想听听八卦,<br>就算不听八卦, 满街的中文饭馆还有路标总是需要人帮衬的.</p>
<h3>圈子外的人</h3>
<p>我有在参会的时候遇到过圈子外的人来听技术分享的情况, 当然这种少一些,<br>我是说, 换个角度可以想想, 除了新手, 还是有一些人需要我们给与一些帮助的,<br>那些人可能是对某项内容感兴趣的创业者, 可能冲着朋友或者讲师过来的,<br>技术会议内容晦涩, 如果能提供一些帮助, 我们可能包容多一些人参与进来.</p>
<p>而且某些情况, 等这一茬的程序员长大结婚了, 带着配偶带着小孩参会也是可能发生的事情,<br>这种, 说不定同城聚会不那么正式的场合会多一些,<br>这样其实还是要有心理准备要跟圈外的人甚至还没入行的人作交流.<br>我印象当中新人畏惧社交会多一些, 还没有进入圈子的人, 他们也需要多一些机会.</p>
<h3>结尾</h3>
<p>勾股说得对!</p>
<blockquote>最后我想鼓励大家多学习英文,多出去走走。</blockquote>
关于 ClojureScript 裸写 stateful React Component
https://segmentfault.com/a/1190000016828953
2018-10-28T18:22:05+08:00
2018-10-28T18:22:05+08:00
题叶
https://segmentfault.com/u/tiye
4
<p>目前的 ClojureScript React 绑定都是比较复杂的, 比如 Reagent, 做了不少的修改,<br>我打算看看直接用 cljs 裸写, 按照 React 本身的语义, 会是什么样子,<br>网上搜到几个版本的代码, 总之核心代码就是这样了</p>
<pre><code class="clojure">(defn my-component [props context updater]
(cljs.core/this-as this
(js/React.Component.call this props context updater)
;; anything else you want to set-up. use goog.object/set on this
this))
(gobj/extend
(.. my-component -prototype)
js/React.Component.prototype)</code></pre>
<p><a href="https://gist.github.com/pesterhazy/39c84224972890665b6bec3addafdf5a">https://gist.github.com/peste...</a><br><a href="https://gist.github.com/pesterhazy/2a25c82db0519a28e415b40481f84554">https://gist.github.com/peste...</a><br><a href="https://gist.github.com/thheller/7f530b34de1c44589f4e0671e1ef7533">https://gist.github.com/thhel...</a></p>
<p>最关键的部分就是定义一个子类继承 React.Component , 然后增加 render 方法, 参考:<br><a href="https://link.segmentfault.com/?enc=tv7dPulgErVDm0VpwHSE9w%3D%3D.YQNU0MjmKgMnItmpVhFVHypmaHfnYHcMIQtE%2F%2BtKlIGSFr1L3BujUOUg%2BslPkY%2FjZvDqDVtu7RuTraGV%2FLnEdoojnmFCfyAJeMZ2acHZnfnqNzGgKlHgyyQdkvlMHiKG" rel="nofollow">https://developer.mozilla.org...</a></p>
<pre><code class="js">// Rectangle - subclass
function Rectangle() {
Shape.call(this); // call super constructor.
}
// subclass extends superclass
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;</code></pre>
<p>最终得到的一个版本是这样,</p>
<pre><code class="cljs">(def comp-input-area
(let [Child (fn [props context updater]
(this-as this
(.call React/Component this props context updater)
(set! (.-state this) (clj->js {:draft "initial thing"}))
this))]
(set! (.-prototype Child) (.create js/Object (.-prototype React/Component)))
(set! (.. Child -prototype -constructor) React/Component)
(set!
(.. Child -prototype -render)
(fn []
(this-as this
(div
{}
(input
{:value (^js .-draft (^js .-state this)),
:onChange (fn [event]
(.setState this (clj->js {:draft (.. event -target -value)})))})
(^js .-draft (^js .-state this))))))
Child))</code></pre>
<p>注意用 <code>this-as</code> 这个 macro 来声明 this, 这个在 cljs 是不能随便用的,<br><a href="https://link.segmentfault.com/?enc=lpWaaeGI4aiGIj7ClL0gMQ%3D%3D.%2FM4L2Agl%2ByjPJU0pH2mqvK0TTuoB6%2FXtg78zolVXXGn4f78iCN3c1eWZwzQv5Z%2Bd" rel="nofollow">https://stackoverflow.com/a/2...</a><br>不过这个 macro 有坑, 我用 <code>let</code> 的时候, <code>this</code> 被绑定到 <code>window</code> 上去了,<br>cljs 编译生成的代码存在一些问题, 感觉 <code>this</code> 怎么说其实还是很成问题的</p>
<p>完整代码涉及到更多的 InterOp 用法, 不专门写了.<br>大概的意思就是需要转很多类型, 在上面的例子当中也看到了.<br>这样一来, 通过 macro 之类的手段在语法上做改进, 很难了.</p>
<p>另外看到 JavaScript 有个 reify <a href="https://link.segmentfault.com/?enc=8lKrPRXlUoZJmoiWRi%2Bepg%3D%3D.xBAX4vc8SjvDfZ0Zl4uxmEuHtOubyk5Lz9g0yVgCE6WnzR%2Fo1%2Fhh3sE2mrtR4hZSf73HH0liENsCI9qq%2BkWbdBxAoQ5HZ3TyhtHL7xhHnIo%3D" rel="nofollow">https://github.com/clojure/cl...</a><br>按说可以简化语法, 而且在 Om 代码有到看类似的用法, 不管比较复杂.<br>直接跟上面的例子对比, 初始化 state 的地方不好写.</p>
<p>总之不好完全按照 React 的语义直接封装了.<br>当日内 Hooks 出来有带来一些改变, 不是很确定能搞成什么样, 社区也在探索中.<br><a href="https://link.segmentfault.com/?enc=eYREdvwKZ5kKfuvAbY8Kdg%3D%3D.FA4V10%2BYfE7vBOIwjuhgT1rC0DrYiP94wXSg5GZ5B2mQE%2BA752Q06n8uoTi0oD3X8XdlmvMrC9lWRcoG%2BnCrEm6RMBljO4RWvf5ss8uC4pM%3D" rel="nofollow">https://github.com/Lokeh/hook...</a></p>
不用 Lein/Boot 部署代码到 Clojars
https://segmentfault.com/a/1190000016361609
2018-09-12T00:00:50+08:00
2018-09-12T00:00:50+08:00
题叶
https://segmentfault.com/u/tiye
1
<p>Clojure 部分代码是按照 Java 生态的习惯打包发布到 Maven 仓库上面的.<br>作为 npm 程序员, 我对 Maven 相当了解...<br><a href="https://link.segmentfault.com/?enc=9QhfyV3Po218%2BNIDYZnb5A%3D%3D.EqUF0zknWpQbcET%2F7v5lzgFs45MaQOruQUoEdQglvcQ%3D" rel="nofollow">Clojars</a> 是一个类似 Maven 的仓库, 相对于 Maven, 更多包会发布在这里.<br>一般要发布 Clojure 或者 ClojureScript 模块, 就是发布到 Clojars.</p>
<p>一般教程都是基于 Lein 或者基于 Boot 用 deploy 命令发布,<br>但是对于不用 Lein 或者 Boot 的 cljs 开发者来说, 我就觉得挺不爽的.<br>那两个东西启动和部署太慢了, 所以一直在寻求替代方案吗最后基于 cljs 快速冷启动.</p>
<h3>原理</h3>
<p>我在论坛上发了一个帖子求助: <a href="https://link.segmentfault.com/?enc=IUpUI4s5hdJtTLxMrRO90w%3D%3D.EmEQHHvdFyxPL2VD%2Boi3vibXH3Ut6ScXAcTcLCrN%2FnOYj0OX7FZ2i9tz0zguvTpr6mW%2BueaJmBq2rEE4h%2Bw0BDxi1oSswan6O8wr15qRnDf4c0N2Yhl8ywpKy7YrK7i1" rel="nofollow">Feature request: release to Clojars with ClojureScript</a></p>
<p>有人给了比较精辟的回复</p>
<blockquote>I’ve been doing this with the clj cli. You can generate dependencies and paths into your <code>pom.xml</code> from <code>deps.edn</code> by running <code>clj -Spom</code>. Then run <code>mvn deploy</code>. Here’s an example pom.xml file for a project I published: <a href="https://link.segmentfault.com/?enc=JD9fW6VS6b5O2hrOTkemPA%3D%3D.O66G9h%2Fdq6iXMbkkIdwSw1KkXHUOp4IlqNokvhnmk2nMEcabu0JHYfmzS8ZrO4B2ybnmYpyBOs%2FLCPM%2B%2B%2F2uxA%3D%3D" rel="nofollow">https://github.com/protocol55...</a>
</blockquote>
<p>我一开始没有看, 后来反应过来, 就是要先有一个 <code>pom.xml</code> 文件, 然后 Maven 就能上传了.<br>后面的回复当中还有给了一个更详细的教程, 具体到命令怎么用: <a href="https://link.segmentfault.com/?enc=2eXKe7UeZvrUj2LrtY6SbQ%3D%3D.l6yVBIIkVBwpg5mFlUxGC33bxXjqCN1wKCjMQ%2BgiPhS3YiN8%2F%2Fm7xRMohEYL7L8rMJtOpgPfNULSJr0g%2BWr8Dw%3D%3D" rel="nofollow">Clojure projects from scratch</a></p>
<p>首先因为是 Clojars, 需要填写这边的账号密码, 本地先加一个配置文件 <code>~/.m2/settings.xml</code>:</p>
<pre><code class="xml"><settings>
<servers>
<server>
<id>clojars</id>
<username>username</username>
<password>password</password>
</server>
</servers>
</settings></code></pre>
<p>后面的步骤主要是 <code>clj -Spom</code> 可以生成 <code>pom.xml</code> 文件. 最后就是 <code>mvn deploy</code> 了.<br>生成的 <code>pom.xml</code> 有几个地方是需要手动修改的, 默认没有 Clojars 的配置.<br>我不重复了, 戳上面的原文链接看细节.<br>我按着跑了一遍, 最后跑成功了. 突然感觉清晰了.</p>
<h3>meyvn</h3>
<p>然后我在想 <code>pom.xml</code> 是 XML 维护起来太麻烦了, 能不能用脚本处理一下,<br>想起来后面的回复当中有人写了 <a href="https://link.segmentfault.com/?enc=WFQaLS0zIDPxzHTfhlDpHQ%3D%3D.nkPdb2hQhBuvDtNXN13Y43fqEdbzrtrlMKetb2KpGw8R8WWKvkvEOn8heykOPYdY" rel="nofollow">meyvn</a> 这个工具.<br>是一个 Maven 的封装的样子. 突然觉得可能就是做这个事情的.<br>Meyvn 依赖 <code>clj</code> 命令还有 <code>mvn</code> 命令, 所以需要 brew 提前安装好.</p>
<p>安装步骤的话, 首先在 <code>$HOME/.clojure/deps.edn</code></p>
<pre><code class="clojure">:aliases {:meyvn {:extra-deps {org.danielsz/meyvn {:mvn/version "1.0.5"}}}}</code></pre>
<p><img src="/img/remote/1460000016361612" alt="Clojars Project" title="Clojars Project"> 注意版本号用最新的吧, 可能以后版本安装方式会简化一下.</p>
<p>然后要找一下系统当中 Maven 的安装路径, 需要执行一些命令:</p>
<pre><code class="bash">brew install coreutils
greadlink -f `which mvn` | awk '{gsub("bin/mvn", ""); print}'
# 得到结果 /usr/local/Cellar/maven/3.5.4/</code></pre>
<p>拿到安装路径之后, 在 <code>/user/local/bin/</code> 创建一个命令 <code>myvn</code>:</p>
<pre><code class="bash">touch /usr/local/bin/myvn
chmod +x /usr/local/bin/myvn</code></pre>
<p>其中执行的脚本用到前面的路径, 我这边看起来简单是这样的:</p>
<pre><code class="bash">#!/bin/sh
M2_HOME=/usr/local/Cellar/maven/3.5.4/ clj -A:meyvn -m meyvn.core "$@"</code></pre>
<p>保存之后就有一个可执行的命令了. 然后就是需要一个 meyvn 的配置文件 <code>meyvn.edn</code>.<br>我看了下文档没写清楚, 但是项目里又一个神似的, 直接可以抄:<br><a href="https://link.segmentfault.com/?enc=c7PWgM1gNGRwWmGx%2ByvrAg%3D%3D.qCBCDucBqxZGbK22YXkSlX5hpGOzF02dInSJ26dhEJjEAPgwdqLAWts6GWRRn1acVAQjxmx4GTcRIawJU3IOFg%3D%3D" rel="nofollow">https://github.com/danielsz/m...</a></p>
<pre><code class="edn">{:pom {:group-id "org.danielsz",
:artifact-id "meyvn",
:version "1.0.5",
:name "Better builds for Clojure"}
:packaging {:jar {:enabled true
:remote-repository {:id "clojars"
:url "https://clojars.org/repo"}}}
:scm {:enabled true}}</code></pre>
<p>这些完成之后, 运行 <code>myvn deploy</code> 就会开始代码的打包和部署了.<br>除此运行会自动下载依赖, 很多的依赖, 我的文章都快码完了, 依赖还没有下载完...<br>后面运行好一些, 但是冷启动还是挺慢, 最终完成发布还是需要十几秒.<br>此外账号密码似乎读取的时候前面 Maven 的配置, 所以直接通过了.</p>
<h3>其他</h3>
<p>不管怎样, 整个方案是可以用来替代 Boot 来进行发布的.<br>实际上用 <code>clj</code> 加上 <code>mvn</code> 命令应该说可以很快的, 但出维护性方面不知道怎么办.<br>我暂时先用这个方案, 后面想办法改进.<br>Meyvn 的文档其实应该说只有 README, 用到其他配置我还得想想.</p>
台北 ModernWeb.tw 参会流水账以及感想
https://segmentfault.com/a/1190000015726798
2018-07-23T00:58:44+08:00
2018-07-23T00:58:44+08:00
题叶
https://segmentfault.com/u/tiye
9
<h3>概要</h3>
<p>大会的主页可以看 <a href="https://link.segmentfault.com/?enc=JrQrgmbaZVZyD%2Fx5PMDybw%3D%3D.M2aR2qGvbjTEMjGi%2FdgI7UuejbDVD7jRqF3O3fNaIIs%3D" rel="nofollow">http://modernweb.tw/agenda.html</a><br>大部分的 slides 已经放出, 有兴趣可以点开看看.<br>这次我是跟随勾股去提供分享, 另外还有几个同行做分享的朋友,<br>我没出去过, 一路是跟着赵洋走的, 算是有惊无险. 手续上有点麻烦.</p>
<p>大会主要是两天, 每天上午是 Keynote, 下午是分开会场做分享,<br>总共有 4 个分会场, 我在第二天下午 D 会场, 应该说就是最小的一个会场了.<br>我跟赵洋周二到台北, 会议是周三周四, 下午结束时候太阳是没下山的.<br>第一天上午 Keynote 是 Google 和 Slack 的讲师分享, 还有勾股,<br>第二天上午记不清了, 记得有个是新加坡的, 但是我只听了他场外的回答.</p>
<p>因为早上开始是 9:30, 我实际上两天都是迟到的, 有讲师保留的席位还好.<br>晚上准备 Slides 其实觉得挺紧张的, 加上陌生的地方没睡好, 作息乱了.<br>会议结束后没有 after party 之类的, 所以都是自由活动了.<br>周三晚上跟着韦字还有勾股活动了一下然后吃晚饭,<br>周四一起去 Hacking Thursday 看了, 跟台北的一些朋友聊天了一会.<br>两天会议就结束了. 跟主办方跟其他讲师交流的机会少.</p>
<p>另外最后一天快结束的时候拍了一个 AMP 的 Keynote, 以及几个 Lighting talk.<br>Lighting talk 上讲的内容也是挺有料的, 长度短, 但精彩不输平时的分享.</p>
<h3>会议的印象</h3>
<ul><li>React</li></ul>
<p>主要是我个人印象, 以及一些新奇的地方, 不是成熟的意见, 只是分享.<br>我听到台湾那边关于 React 的一个分享, 对讲师的印象挺好的:</p>
<blockquote>鍾曜年 (Jay Chung) / 趨勢科技 Sr. Front End Developer</blockquote>
<p><a href="https://link.segmentfault.com/?enc=okGeqJ0T1YafOHnBSCg3fA%3D%3D.fSoAnlXNNeycw84sQWZfl8lAh2nOl2R0Naw21bB%2BVrCiLhmKxOg2MHF815MC1SfL" rel="nofollow">https://weibo.com/1651843872/...</a><br><a href="https://link.segmentfault.com/?enc=06goMs1KXbe2LCxgkRWaXg%3D%3D.oT8kvHa4iFgovubIc4E6KfAOv39UoKUC2jsHv8LiBx4rQPNAk4g61ff%2FbVImCb8T" rel="nofollow">https://weibo.com/1651843872/...</a></p>
<p>算是唯一一个聊得比较多的其他讲师吧, 在会后在门外聊了一下.<br>他对 React 社区的任务非常熟悉, 我当时很惊艳, 就去问了,<br>他说都是 Twitter 上看的, 而且给我看了他的 Twitter 上的消息.<br>我当时想, 台湾接触到 Twitter 应该更多吧, 而且对英文社区更了解.<br>后面跟他交换了一些 React 跟函数式编程之类的看法, 其实差不多,<br>毕竟都是跟着社区在学习, 知识面覆盖相似度挺高的.<br>但是他对每个用法的掌握程度还是让我满惊讶的, 大陆都很少看到.</p>
<ul><li>网络</li></ul>
<p>会后的 Lighting Talk 有个是会场的网络提供商做的,<br>他们大致介绍了一下, 给很多大型的活动做过网络支持, WiFi 网络.<br>他们调侃了一下大会当天流量太大, 很多人可能在备份 iOS 上的数据,<br>总之就是说会场网络很快了. 我也有印象, 讲师专用的 WiFi 非常快.<br>回想一下大陆技术大会的渣网络, 这方面真的非常值得提高.</p>
<ul><li>QA 环节</li></ul>
<p>一般分享结束是会有提问题的机会的, 大陆还有英文社区都会拿话筒给提问者,<br>这次会上挺特别的, 这个环节叫做"讲师面对面", 并不是放在演讲结尾进行的,<br>而是在门外边有一排桌椅分别对应四个会场, 摆着牌子, 方便人上去提问,<br>分享完的时候工作人员就把讲师请到外面去了, 然后想提问的就自己去找.<br>这样提问多也不影响其他的分享继续进行了, 而且很多人去提问也没关系.<br>当然坏处也有, 在提问的人就听不到接下来马上进行的分享了.</p>
<ul><li>聊天室</li></ul>
<p>胸牌背面有个"共笔", 我当时看不懂事什么, 后来提到了, 是个 Wiki,<br>后面 Wiki 上也加上 Gitter 的聊天室, 整个挺有意思的.<br>记得勾股当时还说他们习惯真好, 还有人去共笔上记会议的概要.<br>我看了看 Gitter, 人数 70+ 的, 但是聊天聊不起来.<br>我也问了下认识的台北的朋友, 说是很少有群聊的习惯, Line 上面就很少,<br>这个跟我们我们这边每个活动有微信群有知乎问题还是差挺远的.</p>
<ul><li>午饭和零食</li></ul>
<p>大会的午饭是便当, 两天不一样. 大致就是肉配一些蔬菜跟饭吧.<br>大致是领了便当然后回到会场的座位上吃的, 也是比较直接.<br>吃便当的时候回听到前排后排的年轻人自己相互之间在聊天, 挺好玩.<br>场外零食挺多的, 还有放在楼下展厅的冰的饮料. 可以说是很多了.</p>
<p><img src="/img/remote/1460000015726801?w=4032&h=2016" alt="" title=""></p>
<ul><li>女性开发者</li></ul>
<p>我承认我不少时间是在看妹子. 年轻的妹子挺多, 至少比我在大陆大会遇到的多.<br>除了女性开发者大会逆天的女男比例, 其他大会一眼能看到连着几个女生不多吧.<br>整体上当然是男生多的. 还是不好确定到场的女生技术实力如何.<br>至于具体的人数多少, 着装怎样, 等等看大会是否放出照片吧.<br>会后的提问也遇到过一两次女性开发者提问的, 感觉深度也有.</p>
<ul><li>演讲技巧</li></ul>
<p>从 Lighting Talk 看, 上场的人演讲技巧, 至少在幽默感方面挺赞的,<br>个别讲师讲着讲着就欢声笑语了, 极个别人还是段子手.<br>不过那边的笑点我并不是都能 Get 到, 有几次还是看着他们笑的.<br>我听的有一场讲前端图像识别的, 全程 Demo, 反正 Demo 是很有意思的,<br>而且 Demo 中间还很自然插进去笑点, 这个还是有点厉害.<br>总体感觉他们在驾驭演讲上面很好, 至少看过当中演讲技巧能耐的很多.<br>拿我自己上台紧张的跟他们比, 这个是有很大差距的. Lisp 也没笑点.</p>
<ul><li>举办活动</li></ul>
<p>Lighting Talk 有几个是上去做活动预告的, AngularConf 之类的,<br>听下来觉得他们年度的会议可能还挺规律的, 预告也很明确,<br>不知道是专业团队在举办还是怎么样, 总之至少预告了两个活动,<br>天珑书局的人也提到会把一些店面用来做共享空间之类的, 没记清楚.<br>大陆这边活动相对来说没有那么明确. 但也不好对比, 其实小型活动挺多.</p>
<ul><li>敏捷开发</li></ul>
<p>提到了很多次敏捷, 我以前反而没怎么听到. 但是大家对敏捷的定义似乎也不同.<br>搞不清楚, 总之 agile 听到了很多次, 也可能只是说小步迭代.</p>
<ul><li>AMP</li></ul>
<p>有两个 Keynote 是关于 AMP 的, 说的好像整个是很大的趋势似的.<br>前几年在大陆也听过, 但是 PWA 不是没有推动好嘛. 那边雅虎奇摩却做了.<br>好像他们挺感兴趣的, 这个让我觉得挺费解的. AMP 很强大的?</p>
<ul><li>Vue React</li></ul>
<p>遇到过讲师询问用过 React 举手, 用过 Vue 举手, 看下来真的挺多了.<br>大概的感觉一半一半? 可能 Vue 更多? 记不清了. Angular 反正是少的.<br>大会赞助方站台有看到 Laravel 社区的字样, 看上去也不少 PHP.<br>大会议题似乎是 Vue 多一些, 而且勾股毕竟是在 Keynote 上讲的.</p>
<ul><li>Lisp</li></ul>
<p>我分享的时候问 Lisp 多少人了解, 两个人举手都是我认识的, 尴尬呀.<br>应该是 Lisp 社区跟前端社区很疏远吧, 到场都没这方面的人.</p>
<ul><li>工作人员</li></ul>
<p>会场工作人挺热情的感觉, 好几次引导我到前排讲师席就坐, 也蛮踏实了.<br>总是会一些感觉吧, 那边的人笑得时候多一点, 不像我整天老脸.</p>
<h3>台北的印象</h3>
<p>主要是为了去参会, 所以对于游玩没有做攻略, 台币也大部分靠赵洋帮忙了.</p>
<ul><li>街道</li></ul>
<p>非常干净, 真的, 很多地方都是, 跟勾股还专门聊到了, 干净得不习惯.<br>而且我出捷运的时候, 地面, 天空, 墙体, 都是很干净的样子.<br>我们首先在的地方是 101 大楼附近, 算是繁华地段, 后面看很多都是.<br>勾股说很少看到灰尘, 我后来还注意找了找, 除了工地附近, 真的很少.<br>台北城区的绿化也是蛮不错的, 植物比上海杭州肯定高大繁茂.</p>
<ul><li>垃圾分类</li></ul>
<p>夜市吃完有时候要分类, 会场的便当吃完要分类, 也还好.<br>夜市看下来地面也蛮干净的, 要找垃圾还是能找得到, 但想想差别已经很大了.</p>
<ul><li>支付</li></ul>
<p>全家和 7-eleven 好像支持支付宝的, 其他就很少了.<br>至少夜市还有捷运站都要用到台币, 而我算钱已经很慢了.<br>夜市总共也就看到一两家是支持支付宝的, 少得可怜.<br>台币经常是即使的硬币, 还有上千的纸币, 汇率关系, 真是触目惊心.</p>
<ul><li>食物</li></ul>
<p>两天被拉着吃海鲜我当时还是很害怕的, 万一吃了闹肚子, 我有前科的,<br>但结果料理很好吃, 我也一直没事. 夜市的东西也没怎么样.<br>那么我只能认为大陆的海鲜处理的不够干净所以我才闹肚子了.</p>
<ul><li>河道</li></ul>
<p>几天去过的地方基本是韦字攻略上的, 自己找着去的只有河滨了,<br>不知道叫什么河, 我当时为了拍日落, 直接从西门町暴走过去,<br>但是隔着一道墙, 似乎是高速公路之类的, 有看到立交桥,<br>找着楼梯才走过去的, 感觉挺不容易了. 似乎只有一两个月出入口.<br>这跟上海杭州这边随随便便走到江边看江景差别还是有点的.</p>
<p>因为空气好, 整个日落其实很漂亮吧, 跟在陆家嘴江滨看到很像了.<br>不过江滨没有好好规划的感觉, 有些东西看着老旧, 虽然也挺干净.<br>我是在东岸靠近西门町位置看的, 不知道西岸怎么样.<br>我个人觉得江景比较能展现一个城市规划的眼界吧, 对于空间感的控制.<br>这一点上我觉得上海杭州还是很厉害的, 只是空气跟植被真的不如台北.</p>
<ul><li>Hacking Thursday</li></ul>
<p>会场遇到了一个 Clojure 台湾 telegram 群的朋友, 搭上话了,<br>然后去了趟 H4, 听了一些介绍, 没什么区块链, 倒是物联网有几个,<br>其他就是一样会提 Clojure, 会提 Nix, 会提代数, 会提算法,<br>也是在咖啡馆, 场面大小是差不多的, 其他没有仔细看了.</p>
<ul><li>天珑书店</li></ul>
<p>关于天珑书店, 我在微博上发了, 那么多技术书真的觉得很震撼,<br>希望有一天国内也有类似的书店能出现吧, 对于程序员来说很赞了:<br><a href="https://link.segmentfault.com/?enc=nni3qULEWLd0JJcp8DOXPQ%3D%3D.129QK2h%2F3bxqoxI3pWAnng2T5NRknV7Ve9xCSW2jZSn%2BdH34BPFGMBlnha1bpQYg" rel="nofollow">https://weibo.com/1651843872/...</a><br><a href="https://link.segmentfault.com/?enc=lh2uwkuTkGTzicbHSnPO8g%3D%3D.F20oqq6ILMQhZcaz7ljPc%2FUJNq%2FotCwSGgXKZlNh9GGogxHRfdY7Pjf7PQYWFajH" rel="nofollow">https://photos.app.goo.gl/AHa...</a></p>
<h3>其他</h3>
<p>第一次出去看到比较不同的文化氛围, 还是很开眼界的.<br>不过技术方面, 因为都是跟随着英文社区, 似乎没有太多隔阂.<br>除了某些地方闻到几次汽车尾气, 整体的感觉空气是很好, 视野开阔.<br>没有机会去趟海边有点可惜了, 毕竟看海的机会真的是挺少的.</p>
<p>街上拍的照片 <a href="https://link.segmentfault.com/?enc=WK5BzUfnABblL4CTxvjobg%3D%3D.m0DimxAlpQEGPPX4ZQ7sKgDt3FMUp5qnk3e08Lsp%2BMs3GQAtHJpCuyuAMD7aupAy" rel="nofollow">https://photos.app.goo.gl/mjs...</a><br>夜市扫的照片 <a href="https://link.segmentfault.com/?enc=W7%2FrsOKQ8Vv5yQbUpAaP1w%3D%3D.fWsHWJx3OJqNAWVJ28Z%2Bx6UJimJxKh4RbTcwR4RRrv5%2Fi276%2BK544v1OzhgAdiVT" rel="nofollow">https://photos.app.goo.gl/j7e...</a></p>