交替使用 TypeScript 和 Nim 的一些感想
我之前的背景主要是 js 和 ClojureScript, 对类型了解很有限,
到 Nim 算是才开始长时间使用静态类型语言吧. TypeScript 那只当 type checker.
Nim 的明显问题
JavaScript 到底是 Google 砸钱了的, 调试的体验真的是好.
至于 Nim, 大部分的报错靠着类型信息倒是也能定位出来,
不过没有趁手的断点调试工具, 经常要靠大量的 log, 我也挖得不够深.
Profiler 我也用过, 导出的是文本的调用栈和开销占比, 当然没 Chrome DevTools 清晰.
VS Code 使用体验自然也远远不能跟 TypeScript 比, 我直接 Sublime 了.
Nim 的 echo 首先就很让我头疼, 没有自动加空格, 挺烦的.
Nim 没有内置的 string interpolation, fmt 不确定是函数还是 macro.
通常 fmt"balbla {b}"
这样的语法可以插值, 但是效果完全不如语言级别的插值方便,
这个写法中如果有 {
或者 }
还需要手动处理, 我在使用的时候就感觉比较糟心.
要在语言里边实现 interpolation, JavaScript 或者 CoffeeScript 都不会这么麻烦.
Nim 类型和运行时的一致性
TypeScript 虽然有很风骚的类型系统, 但我也没用着 strict, 也不是所有依赖都 ts.
然后偶尔会遇到写了类型但是底下完全不是那么回事, 就很莫名.
Nim 的类型跟数据是直接对应的, 使用当中除了一些 edge case, 都能对应上,
意味着类型检查报错的地方修复, 代码对应的报错也就解决了,
这让我感觉到类型才是可靠的. 当然, 很多静态语言应该就是这样子.
Method call syntax
Nim 不是面向对象语言, 里边的 object 大致对应 C 的 struct, 而不是对象.
object 里边就是数据, 这个还是比较习惯的.
不过代码观感上, Nim 还是有点贴近 js 这样支持 OOP 的写法的,
我是说大量的 a.b(c)
这样方法调用的语法, Nim 里边叫做 Method call syntax.
也刚知道这在 D 里边已经有了, Wiki 上都明确说了:
https://en.wikipedia.org/wiki...
这个特性对应的 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)
最初我使用的时候没有在意, 但是随着迁移一些代码到 ts, 才感受到灵活.
在 JavaScript 当中, 继承, 多态, 依赖 class 结构才能实现,
这也意味着我要定义 class, 然后 new instance, 然后才能用上.
但定义了 class 也就意味着这份数据不是 JSON 那样直白的数据了.
我对 OOP 使用不多, 但是思来想去大致也理解, 动态类型能做到 JavaScript 这样已经很强了.
Nim 的多态是通过编译器判断类型来实现的, 比如前面的 add
可以有很多个 add
,
proc add(x: Y, y: Y): Z =
discard
proc add(x: P, y: R): Q =
discard
后边遇到 o.add(j, k)
根据类型在编译时候就能实现多态了.
当然这在 JavaScript 靠 class 是能够实现的, 但那就一定要把数据操作绑在一起了.
长期使用受 Scheme 影响的语言, 对 class 这个臃肿的做法就很难适应.
有类型的情况下, 在这套方案当中 overloading 很自然的,
比如 Nim 当中对类型 A
定义 equality 判断的写法这这样的,
type A = object
x: number
proc `==`(x, y: A): bool =
discard
没有耦合在一起, 意味着我引用 A
类型在另一个项目也能自行扩展,
而且这基于类型的, 不会修改到原始的模块当中, 不影响到其他引用 A 的代码.
这一点, 我的代码从 Nim 转译到 TypeScript 就比较头疼,
因为我定义数据结构的访问和判断需要 overload 这些个 array accessing 和 equality,
我一时半会也想不出来 TypeScript 当中能怎么做, 只能用 mutable data 在运行时强行模拟.
Nim 里边就很简单, 我对 []
进行重载, 后边就能 ys[index]
直接用了:
proc `[]`[T](xs: MyList[T], idx: number): T =
discard
这个对于 iterator 的场景也是类似, 定义了 iterator 就能直接写 for..in 了.
iterator 这在 JavaScript 当中也行, 只是说 Nim 当中很多运算符都能自己重载.
然后好处也是比较明显的, 比如我重构了操作内部实现, 但使用的地方基本不需要调整.
而在 JavaScript 里边, 长久我就习惯性直接面对 Array 跟 Object 了.
没有碰过 Java 跟 C#, 碰过语言当中这套玩法跟 Haskell 倒是挺像的,
Haskell 从 class 产生 instance 的时候可以定义一些函数, 就很灵活.
(具体 Haskell type class 高阶玩法真是还玩不起来.)
不过 Nim 相比来说, 简化是简化了, 但这个语法糖在编码当中就是很方便.
也因为缺失这个功能, 导致我对 Clojure 跟 TypeScript 这都有点不适应了.
动态数据的类型
转译代码还发现的问题是由于 JSON 跟 EDN 极为便利,
引起我在大量代码当中直接使用 Array 和 Map 直接表示数据,
这个不能算错, 但数据在 Nim 当中这些都是明确用类型表示的,
也意味着在 Nim 当中有明确的结构进行判断, explicitly...
反观我的 TypeScript 代码, 大量的 Array.isArray
,
然后还有那个不知道怎么说的 typeof x === 'object'
的尴尬,
在类型系统当中使用习惯之后, 回过头感觉特别不踏实,
然后我自动跑去折腾 instanceof
的玩法来做对应功能了.
当然, JSON 或者 EDN 通用地表示各种数据, 确实在通用型来说非常好,
我跨项目调用数据, 这些动态类型就是直接通用的结构,
在 Nim 当中, 一般传递数据是会涉及到一些类型转换, 写写是有点麻烦的.
我不是很能衡量那种方案是更好, 但是对于底层类库, 我是希望有明确类型的.
内存相关问题
因为要编译到 C 运行, Nim 当中的数据结构多少还是要涉及到一点内存的部分.
不过好在 Nim 当中指针绝大部分已经封装成 ref 了, 也很少要去操心.
主要是感觉就是不同数据结构之间性能的区别比较容易体现出来了.
这个在 JavaScript 这边, JIT 老是偷偷帮忙优化, 自己写出问题没那么容易察觉.
我感觉到如果我早使用 Nim 的话, 对算法和性能的朦胧感就会轻很多.
而能触碰到内存, 也就意味着内存管理会遇到一些问题,
我之前遇到, 似乎是用 macro 的时候, 遇到语言内部的代码出错了,
然后 illegal memory access, 这就变得很无助了,
论坛上给我的帮助让我编译 Nim 编译器本身然后打 log 来获取细节,
这个体验还是蛮新奇的, 反正 V8 我是没有自己带参数编译过...
至少我目前用到的还不需要很清晰了解内存布局细节, 以后再看吧...
一些语法细节
Nim 当中的语法糖还是挺多的, 有很多使用 CoffeeScript 时候那种轻快的感觉,
比如说 JavaScript 现在不好加语言级别的 range,
这在 Nim 当中直接用 ..
或者 ..<
就能生成 range 了:
for i in 0...<n:
echo i
然后 if 在 Nim 当中虽然比起 CoffeeScript 有那么点不顺手, 但也还是表达式:
let a = if b:
c
elif d:
e
这样的代码转到 TypeScript 马上就变得挺长了, 我更不能用三元表达式去牺牲可读性.
当然还是跟 CoffeeScript 去比的话, Nim 毕竟还要考虑类型, 没的那么灵活.
对于代码格式化, Nim 有个内置的 nimpretty 命令.
我没怎么用, 只是试了一下, 快当然是很快的.
不过我用缩进写代码本来就已经精确管理空格了, 再弄一个好像没必要, 也没手写灵活.
其他
TypeScript 的强大是我不得你承认的. 为此我对 AssemblyScript 还挺期待的.
但是随着 Nim 带来的这些感受, 我也起了一些疑惑.
比如说基于 WebAssembly 我们将来有个更好的浏览器语言了, 怎样才更好?
一方面要兼顾 Web 应用大量的界面处理的场景, 一方面高性能和灵活,
单纯 AssemblyScript 这样, 总感觉还是不够的吧
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。