为啥 js 语言里面 那么喜欢嵌套,匿名

2023-05-01 16:34:42 +08:00
 yagamil

作为一个后端,算法的开发者,平时会写点简单的页面。

不过看着 js,vue 的一些语法,感觉可读性非常差,函数喜欢用匿名的,在一个函数传参里面,把函数作为参数传入,导致的是,这个函数的参数 入参 非常冗长,不用 IDE ,根本不清楚这个匿名函数在哪里结束,因为里面可能还嵌套里其他参数。

好好定义一个函数名,设置一个好的名称,好好写,不行么?

还有链式调用有点泛滥。

这样做的原因是做什么?

5044 次点击
所在节点    程序员
50 条回复
FrankHB
2023-05-02 15:51:23 +08:00
@Al0rid4l 其实和为什么没多大关系,虽然允许遵循链式命名解析的变量(名)算是 lambda 演算的主要贡献,lambda 演算的命名就只有约束变量(函数参数)这个地方能用。而现代语言更常见的命名依赖的环境这种抽象是在 Lisp 用动态作用域实现 lambda 演算变体时提出的副产品(相比纯 lambda 演算依赖 capture 和 capture avoidance substitution ),并且环境的实现方式( alist )因为保存了状态恰好还同时更加兼容词法作用域和非纯函数式的用法( impure λv calculus ),能够直接平替没作用域的符号表而已。
所以也不是学 C 学傻,基本上不清楚一些历史的,默认只能当作都是傻的。
FrankHB
2023-05-02 16:21:42 +08:00
@yagamil 明明命名困难是两大计算机科学难题之一,你不在乎就无所谓了?
你这首先用复用不复用来判断命名的方式是病,得治。

使用符号命名是有明确的目的性的,就是塞入语义上无关紧要(操作语义上就是某种 alpha renaming ;虽然遇到反射会失效)但有确切含义的元数据来辅助人类阅读者改善推理,同时语言实现不影响语义等价翻译(比如编译优化)的可行性。后者可以在语义上约定自己的假设而同时能被机器改善推理(比如 devirtualization )。
其实这就是所谓的封装性(encapsulating)的确切内涵。因为教学质量的普遍垃圾,很少有人理解,不管是 access control 这样在 name resolution 上加 hook 还是 inclusion polymorphism 这样在 call 上加 hook ,都只是 encapsulation 的实现细节而已。

你若只是因为要让看得清楚,而不管这里的原始目的,这是胡搅蛮缠。因为只是为了看清楚,而不是区分“这个不是那个”这种依赖元数据内容的场合,完全可以用编辑器辅助的纯语法方式(例如高亮边界)解决。

一般地,在一个合理的设计中要求实体具名,充分必要条件是这个命名属于用户关心的 API 的一部分——只有这种场合人理解命名的冗余含义才能确保可能帮助准确地正确使用(当然前提是命名对了)而不是干扰人类读者的冗余噪声。否则,都是耍流氓。

有一种常见的误解是关于人为在源代码(而非实现内部变换)中引入“中间变量”的必要性。
实际上如果不考虑一等状态(比如 C++的那种 RAII ),任意的显式声明引入的中间变量在逻辑上是冗余的,只不过因为考虑读者的脑容量有限,避免发生 register spill 之类的问题,所以有的作者才试图这样做。
但是,这种中间变量完全可以 canonicalize 成 let form 。(学 ALGOL-like 语言学傻的不容易理解这一点,因为不知道 block 实际上是 lambda 的 derivation——用 JS 的话来说就是 IIFE 。)
这种情况下,到底什么变量才是真正有资格当中间变量的问题才会显著——只有能被复用时,才配被提升到 let 的变量名的位置上。
那么不被复用的复杂表达式可读性差怎么办?很简单,拆成具名函数的调用。引入的函数名就不是这种中间变量了,它可以是内部 API ,即便只被用到了一次。
之所以函数名和普通的变量名区别对待,是因为具名函数的实现默认是封装的(函数体不可见),因此它有资格单独占据名称。
当然,如果考虑一等状态,比如 lifetime 这种依赖对象自身存在的情况,那就有复杂多了的例外——因为存在隐含的资源释放,实际上有类似函数调用的作用。不过,遵循约定的代码反而会更清晰可读:如果严格遵循“不复用的对象不在 let form 中出现为变量名”,人很容易推理出为什么需要有孤零零(甚至一次都没用到)的对象;反之就麻烦多了,读者要自行验证这里到底复用了几次才放心。结果,在这个上下文中尽量排除没有复用的具名对象,反倒更合理了。

题外话,显式类型(explicit typing)和命名的必要性同等,也就是在 API 上才配用。所以 bb 什么 auto 啊 var 啊看不清楚类型的,那也是耍流氓。
FrankHB
2023-05-02 16:50:15 +08:00
@FrankHB 严格来说,现在大多语言支持的 lambda 都是 applicative 的,也就是实际参数必须在调用前被严格求值,那么所有对应的形参和函数体默认都是封装的(编译器能够自由重写其内容而不管源代码里到底用了什么命名),除非有 MIT Scheme 那种 procedural-environment 这样的反射功能,或者另外严格约定基于源代码等价性的等于操作之类的奇葩设计。但如果函数不仅仅是 applicative 而可能是 operative 的(包括一般的过程宏或者 vau 这样的 FEXPR ctor 引入的匿名函数),实际参数可以非严格求值而直接把 AST 这样暴露源代码信息的东西扔进去计算(换种说法,默认就对源代码总是有完全反射),那么函数体封装和不封装之间区别就大了去了:不封装的函数会要求实现单独证明语义等价之后才允许变换,这一般是不可能的(除非预知实际参数是什么内容,做 partial evaluation )。

具名函数和匿名函数在这里的习惯性差别是,前者是否使用单独的语法形式(而非把匿名函数当初值进行变量定义的方式引入)的选择是未指定的。因此前者能够具有更严格的封装性要求,例如完全可以在语言规则中指定假定函数体就是封装的。ALGOL-like 的语言中函数声明和定义分离其实就依赖这种假设。
FrankHB
2023-05-02 17:47:50 +08:00
@FrankHB 还要做个语言限制的修正。
上文所述的 API ,不仅仅是环境中提供访问的函数名,明确包括形参名称这种 lambda calculus 中唯一提供的命名( bound variable )的方式。
实际情况是,具名函数的函数名被当作 API 的一部分,因为它指称的对象(函数)的内部结构是默认封装的,所以也就函数名以及类型这样的外部特征允许标识(identify) 一个 API 了。更何况还有基于具名函数的 ad-hoc polymorphism:重载。
然而函数以外的普通的对象的表示其实也是封装的,甚至某种意义上是更加封装的,内部结构同样不保证总是清楚唯一( ABI 的意义上例外)。那么为什么这些对象名不是 API 的一部分?因为两者的实现难度存在差别:实现的语义等价变换会重写函数体(或者 basic block 之类的变体),但是重写更一般的对象内部结构会依赖更困难和特设(ad-hoc)的方式来证明语义等价而不属于标准的优化技术(比如把整数和浮点数类型相互替换需要证明计算结果没有误差没有 fenv 副作用),所以一般不考虑。因此,把一个非函数的对象的结构暴露到 API 层次,容易混淆接口和实现的边界。所以就算全局对象作为 API 的一部分,一般也不会提供字段这样的内部表示,而是具体函数功能的一部分配置(典型例子是语言提供的标准流对象)。
但是,参数名在实现内部实际上还遵循一种更特殊普遍的 API:naming convention 。大多数情况下,只有一个函数形参命名成 x ,只要没其它嵌套作用域冲突,就已经够可读了。循环不变量的 i 、j 、k 同理。要强行提供更具体的命名反倒不一定更清晰,因为原始目的就没有这个意思。这种约定在大多数语言中不被视为 API ,因为大多数语言压根就没提供足够的反射机制来对变量名直接进行操作。但是,如果语言允许局部的完全反射并提供充分的操作(如 Lisp-like 的 symbol->string/string->symbol ),对代码对应的 AST 直接进行元编程,应该怎么命名变量名的 pattern 就是 API 的一部分。只不过这类 API 基本只会在自动化代码审查的元程序中用到,而且通常不像函数名和类型一样会被提前静态检查。
其它语言中这种情况相当少见,不过不是没有:例如 C 的宏允许做字符串拼接生成参数;例如 #pragma GCC poison 。无论如何,这些只能生成而无法反射的用法都相当恶心,所以缺少关注。
icenine
2023-05-02 20:53:13 +08:00
由于历史原因 JS 工程化程度不高,处理的问题也相对单一
就像小公司小团队一般有什么问题当场就办了,很少专门立项拉会批文
yagamil
2023-05-03 02:18:13 +08:00
@FrankHB 谢谢回复。
命名是计算机学科的两大难题之一。
这个很早就听过,只是某些媒体的吸引眼球的论点而已。 这都算难度,要么英语四级没过,要么就是个菜鸡。这都算是难题,还是原来计算机了
yagamil
2023-05-03 02:18:53 +08:00
@yagamil 原来->远离
FrankHB
2023-05-03 03:06:17 +08:00
@yagamil 根本不是你理解的这样。就因为这种冗余的元数据的选取跟语义独立,所以才困难。因为这种东西就是给人看的,机器原则上不可能检查(除非是具有比你更强 NLP 能力的真·强 AI 到直接能把你淘汰的那种),你不可能预知别人看了会跟你想得一样,更不可能保证你的命名绝对是最优解,光是确保能满足需求的工作量就触及了“软件工程的本质困难”。一旦要添加能影响语义的特性(比如反射),又是一地鸡毛。
当然机器原则上不可能检查的东西多了,比如怎么让 GC 代替显式资源释放的位置。但是没有一个其它问题的普遍性能和命名困难相提并论。(如果有,那么最接近的是如何定义相等操作。)
chnwillliu
2023-05-03 08:54:42 +08:00
@yagamil 哈? native speakers 一样烦恼命名问题啊,这跟英语好不好没关系。
yagamil
2023-05-21 23:25:48 +08:00
反正就是不懂为啥那么多箭头函数,还有作为回调函数的参数传入。
比如 setTimeout(res=>{
xxxxxx
xxxxxx
},5000 )

快速阅读代码时,我要知道定时器里面做什么东
东,还得看那个箭头函数里面的大体直接。

比如如
直接给一个函数名叫 collect_user_feedback, 这样是不是会更好? 起码大概意图阅读者已经清楚了。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/936734

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX