duangsuse::Echo
711 subscribers
4.24K photos
127 videos
583 files
6.44K links
import this:
美而不丑、明而不暗、短而不凡、长而不乱,扁平不宽,读而后码,行之天下,勿托地上天国。
异常勿吞,难过勿过,叹一真理。效率是很重要,盲目最是低效。
简明是可靠的先验,不是可靠的祭品。
知其变,守其恒,为天下式;穷其变,知不穷,得地上势。知变守恒却穷变知新,我认真理,我不认真。

技术相干订阅~
另外有 throws 闲杂频道 @dsuset
转载频道 @dsusep
极小可能会有批评zf的消息 如有不适可退出
suse小站(面向运气编程): https://WOJS.org/#/
Download Telegram
唉真是头疼呢。要是这些大佬能在写的时候注意点,就不会出现这么多难以复用的代码了
不过这也不能,如果只是修改代码风格、压行的话,是起不到简化作用的,不同的调用参数太多了。 我吐嘈的是这个人没做到一些最基础的复用,看起来就像是 python wtf.py 的面向查错编程一样(当然对 Tk 的 GUI 有些问题也迷所以必须测试,但不意味着你可以把冗余逻辑复制粘贴而不是化作复用函数)

如果要进一步提升代码质量,不使用高阶函数是很难实现的(那意味着你没法把模板化函数命名作动词,然后用另一些动词调用它从而实现更高层次的复用)
用高阶函数(当然那是概念上的名字,工程中我把函数值命名为和函数/方法一样的动词)才能实现更好的效果,如图把表述式代码和定义式代码比较,它简直像汇编一样眼花……

其实相关知识许多人也知道,但是他们的代码复用意识淡薄,而且现在框架丛生,人们越来越懒得写自己的抽象了;开始习惯把自己禁锢在别人提供的安乐窝里,照着惯用法一点一点堆积木。
即便不是低级错误也应该是可以预防的错误啊…… 怎么看都是原有 888 端口加个 /pma 都直接越权访问。 无非是外部代码插入server不当或是验证时序/接口失当级别的问题,在程序封包层错误都可以导致此问题,还不是很渣..
Oh come on ... 这可爱的 truthy value... 我还以为是 0.0>0.5 或者自动数值类型提升出问题了呢。 我开始怀念 Ruby 里只有 nil 和 false 的 if / || 表达式了。
This media is not supported in your browser
VIEW IN TELEGRAM
Python 里感觉用个 (a if p else b) 都不敢乱写,乱写会出岔子/影响预期性能(开个玩笑)/etc.
HowFarIllGo
1.9 KB
对了,我这里有份迪斯尼动画电影 Moana 主题曲的(半完成)音高线,用过我 Hachiko(当然我知道没人用 哪怕是 producer. 嘿嘿)的人可以拿去现场演奏(
啊... 花了好大力气它终于 works 了,本来还想一笔带过的,花了一天…… 不得不佩服写 GUI 控件的人的勇气,我被迫从它的烂代码里了解了它的具体操作,不过重构真是无聊,相信我,别做 refactor man, 一点也不好玩。
This media is not supported in your browser
VIEW IN TELEGRAM
不过老实说我挺喜欢定义式代码的 #Python
比如
x = self._valPixels(float(value))
后,
w=self._wid.winfo_width()
if x + half_width > w: x = w - half_width
elif half_width > x: x = half_width
咱给简化成一句话了,所以可以直接写参数里
x=max(TtkScale._bounds(x, self._wid.winfo_width(), half_width), half_width)
其中,
def _bounds(n, max, sub): return max-sub if n+sub > max else n

看来我果然还是讨厌写 if while ( 如果抽象程度更高 何必重赋值呢
以此类推,五六处这样的代码我都用我的方法简化了。 既然弄完了不如就顺便讲点啥……
面向对象基础设计模式小讲堂 🙉 #OOP #learn
我们要介绍 override(覆写), overload(重载, 重新的重) 两个基础多态、以及 Mix-in(混入), Delegate(代理) 这两个设计模式
注:咱不懂 JS 的 prototype chain 也不太懂 Python ,说得概念化一点。
当然我一贯的习性是先讲原理,看官如果讨厌的话就先看后面简单的。 😙

面向对象的基础,除了易语言和其他伪面向对象所谓的 this 参数特殊化处理(最基础的"主语this"封装),关键有三点:
抽象,意味着应该有只指定调用型参(formal parameter)的 interface ,从而允许其他类型覆写。实例如所有东西(门/盒子/...)都可破坏,但破坏的实现是按它的具体属类决定的。
继承,意味着有 Duck: Animal 这种子类型关系,即前者具有后者的所有成员。有了继承和 public(公开)/protected(族内)/private(私下) 的可访问性区分,可以实现对模块外隐藏的代码 抽象-提出为方法 复用,
如把 for (i=0;i<n;i++) op(); 建模为一个 class , i 即成它的私有属性、 op() 即成它的私有方法。如果 op 是构造器参数等可变动项,不就是一种 Iterable 了么🤪
有些语言如 C++ 有多继承,可混入两个超类,但那样对常见模型来说过于复杂
有些语言如 Ruby,Python 有主要支持 mixin 的多继承,(Ruby 的 module Mix; class A < T; extend Mix; Py 的 class A(T, Mix): ) 可以把插入方法解析链上(A.ancestors / A.mro()) 但对构造器调用
多态,本身是「同一名字引用不同含义」的意思,但一般意味着通过子类型的兼容性,你可以在子类里覆盖其超类的方法行为,比如 java.util.function.Function 就严重依赖这个东西(Java 8 的 lambda 当然也基于此)

多态意味着可以创建函数值,它是 Listener 等惯用法所必须的特性(T=Runnable; T oper = new T() { void run(){print(1);} } 这种 "anonymous subclass" 就利用了多态,不然你以为 oper.run() 的 receiver(this=oper) 是怎么知道调用其 T.run 实际做 print(1)
也有人说,对象的本质不是分配出个 struct 存储空间的指针或者 GC object,而是一个强类型的函数式闭包(可依赖外部变量的函数值,这个函数就是 send 方法解析+执行函数、依赖的变量就是 this)

多态的本质是 type checker 的子类型(subtype)兼容+运行时方法解析,方法解析是重点、继承树优化处理点,因为子类型意味着如参数/局部变量可以兼容其要求「表象类型」的所有「实际类型」,如 Animal + Duck

在 Java 里一般用 @Override 标注覆写方法,但这不是必须的。 override(子类型多态) / Generic(parameterized type, 参数化多态) 和 overload 同属多态,但 overload 属于可以静态完成的特殊多态(ad-hoc polymorhism),想想编译器可以通过 type signature 查找的方法简单找到要调用的方法,不需要在运行期基于对象实际类型完成方法派发。

对于 JVM/Ruby/Python 的对象都存有其 type: a.getClass() / a.class / type(a) 所以可以有其VM内建的派发方式,不过在相对底层(无 GC 而用指针)的语言里一般有两种多态方法解析方式: virtual table (虚方法表) / fat pointer (双指针) ;前者意味每个对象都存储其实际类型虚表指针、后者则是每个引用都存储对象的实际类型(优点是局部分配不需要vtable,省内存了)
顺便提一句: Python 目前是 gradual type & consistence type 的语言,分别意味可选标注类型/Any 类型与任意类型自动强制转换
VM:语言用虚拟机(线性指令解释器)/GC:垃圾回收器,分配对象

类(class)有三种基础成员:构造器(constructor)、属性(property, 或者说名词/类层次的 val/var)、方法(method, 或者说动词/类层次的 local fun) ,当然 inner class 和命名空间意义上的 inner class 就是语言特定的了。(基本都是语法糖/特殊命名解析

Java 的属性是用方法实现的,它的 "field" 概念不叫属性,其暴露了过于底层的东西,使得程序员被迫选择是否定义冗余的 private this.x;T getX() ,或者完全放弃可扩展性;这是不重视设计模式优雅性的表现。
一般的 OOP 语言都会支持把 property initializer 和它的类型定义放一起,然后可以集合并生成默认构造器,但记住这只是简写法,构造器应当存在。

方法的继承是基于方法解析,属性继承则是通过构造器调用。无论程序员是否显式写出,超类链(直到Object)上的所有构造器都会被调用,一般而言是超类优先调用,那样子类构造器就能访问超类属性。
所以不允许构造器访问 non-final (open for override) 的属性,因为如果它在子类里被覆盖 超类就会访问到 uninitialized property 。 这种情况属性换成 protected 方法即可。
**更新:如果 initializer 在子类被覆盖就不进行初始赋值,对超类来说也太难判断了。经过测试我才发现那样只是令依赖 override val 的属性得到旧的值:
abstract class A(px:Int) { open val p=px; val p1=p+1 }
class B:A(0) { override val p=2 }
B().p1 //1

open class A { open val p=0; val p1=p+1 }
class B:A() { override val p=1 }
B().p1 //1
当然如果有在构造器里对子类成员的环形引用,是可能访问 uninitialized 的。继承在构造器里必须是单向的。(不必注意 这个不常见)

注意即便对象不是 hash 表(动态对象)实现(Ruby/Python/JavaScript),它和它所有超类的未初始化属性所占用的内存空间也是可以被静态计算出来的,new 将分配和构造器调用同时进行。

https://github.com/duangsuse-valid-projects/Share/blob/master/Others/LuaOOP/oop.lua 这里有一个为 Lua 实现面向对象 new/inherience 的

最基础的封装好多语言都有,我们这次不讲那个可用于支持链式调用传递this上下文的。(因为它的功能局限于此,尽管this成员和global变量的区分隐含一个链结构,那是可编译期静态解决的)
对于不使用 interface 进行"抽象"的返回类型检查,在 Kotlin 里可以用 Nothing (实际上不会返回的操作如 System.exit() / throw Error() 为此类型) 来略过。虽然抽象方法的概念是无需实现体即可声明,它的「实现」与否也是相对其子类而言的。

Mixin 呢,表示隐式(无需指定 receiver)访问不在超类链里方法的设计。假设小明要创建一个 class Duck extends Animal, 但他发现 Animal 里有一群冗余方法可以复用: propertyKeys(), getProperty(), setProperty() 但生成它们返回值的方法需要 super (即 Animal) 对象,而且,因为他在扩展别人已继承 Animal 的类,无法定义出 ProperAnimal 而必须直接继承指定 ? extends Animal
他可以选择复制粘贴,但除了 Duck 他还有一大堆一样的 Animal 子类要定义。不能继承自消除模板化的超类,也不能覆盖超类的抽象方法来提供数据从而复用其逻辑,怎么办?
Java 没有多继承,也没有 Mixin 。 假设它有的话,或许我们可以选择用 Mixin (注意概念上它们全都是 private 方法,不可参与方法解析的)。

interface AnimalBoilerplates extends AnimalKind {
default Set<String> mpropertyKeys() {/**/}
default Object mgetProperty(String key) {/**/}
default void msetProperty(String key, Object value) {}
}
class MoreDuck extends HerDuck implements AnimalBoilerplates {
Set<String> propertyKeys() { return mpropertyKeys(); }
}

当然这个做法也是比较鸡肋的,毕竟是在扩展别人的类库,只是个例子哈哈。
#Kotlin #Java #Lua #design #cs
Delegate 呢,和 java.lang.reflect.Proxy (方法调用拦截) 当然是有区别的啊,不过提到 reflect.Proxy 可能有助于对 Delegate 的形象理解。
class MyDict(private val m: MutableMap<K, V> = mutableMapOf()): Map<K, V> by m
这就是咱漂亮的 Kotlin 对 Delegate 的支持(T by m) 里 T 是任意接口,它会代理其中的成员(从 this)到 m 这个 receiver 上。

负责的老师会告诉面向对象的程序是一群对象相互「发消息(message passing)」的过程,确实是这样;而且 OOP 的 this(即receiver引用, 或者说"主语") 使得我们以更自然的方法编程和划分代码块,不过发消息的确切说法是「驱使」或「属性访问」 (就是引用某个「主语」上的动词或名词)。

Delegate 在你了解这个事实的时候很直白:它就是把某些方法(调用的"消息")从某个 receiver 上转移给另一个 receiver ,所以我们可以更灵活的利用面向对象的继承,甚至实现"多继承"…… 在上例里,MutableMap 是个无构造器的类,但我们利用 Delegate 使它上的方法(get,set,remove,...) 通通可以访问了 (当然这个"子类型"并不兼容 mutableMapOf() 的实际类型,它是另一个继承自 Any 的新类型,但如果有 m 实现了的 interface 则其方法皆可被代理,这才是"代理"存在的意义)

如果你利用 Delegate, 则可以完美解决上面用 Mixin 很麻烦的复用问题,定义一个需实现的接口(子集)即可 (而且 AnimalBoilerplate 也可以使用 this 外的变量, 同时不局限于访问某 interface 了)。

附赠另两个特色设计模式: Visitor 和 Reducer(非标准) #PLT #ce #OOP

经常写基于 Iterable<T> (map/fliter/sorted/zip/first/...) 的程序吗?那你应该听说过 java.util.stream.Collector :
import java.util.stream.*
Stream.of(1, 2).map { it + 1 }.collect(Collectors.asList())

我最讨厌 Java 的一点就是它废话太多了,聒噪。仔细看 Collector 里有什么? supplier; accumulator:BiConsumer; combiner:BinOperator; finisher ,这就是所谓的 "architect" 到处弄 XXX er/or 的对象,不懂得沉默是金、混淆组合(combine)流与普通映射/过滤流、没有复用 Collection 而是用了所谓的 finisher,咱才懒得看它呢。

Reducer 和 Fold 是另一个版本的 forEach: 假设你是要拿到序列的最大值/最小值(min/max),或者把它化为映射表(associate/groupBy),或者仅仅只是复制一份 List ,但不想写重复代码,你会怎么做?
val xs = listOf(1, 2, 3)
xs.reduce { a, b -> kotlin.math.max(a, b) } // 3
等价这个"折叠"循环: var acc=xs[0]; xs.indices.drop(1).forEach { acc = kotlin.math.max(acc, it) }; acc
当然如果是求和的话,也可以不要求列表至少存1项: xs.fold(0) { a, b -> a+b }
fold
操作往往比 reduce 更实用(如 xs.fold(StringBuilder(), op) ),如果我们把他们结合起来变成对象呢?就成了 Fold/Reducer 。这名词是我自造的 😗

以上操作或依赖一个可变量, 或是 MutableList 什么的。 他们的交集是,都依赖一个(变)量,且在 <T> 流结束的时候,返回结果类型 <R> ,所以必须建模为对象,因为它要构造并持有一个状态并且在 accept(T) 时更新此状态。
我写了泛用的 ConvertReducer<T,R>ModifyReducer<T,A> 两个基类,因为这不是标准设计模式我不赘述,有兴趣找我的代码看,Fold 是 () -> Reducer (R.的构造器类型)。
ParserKt 利用了这个设计模式,从而实现 Repeat(asList()/*asInt(radix=10), asMap,joinAsString...*/, digit) 这种方便的定义式单项构造。

流式编程是来自于函数式 #FP 的智慧,这可以视作它们广为人知的特色(自Lisp就有,地位类似OOP链式调用,不过有了 Kotlin 的 scope function (let/run/apply/also) 后我觉得 Builder 等设计模式要无地自容了😂,流式编程和 MapReduce 却越来越火,超过 LINQ 和 SQL ,嘿嘿),看看你听没听说过这些处理方法: flatMap(trans)/zipWithNext()/first(n)/takeWhile(predicate)/foldRight 。大体上分为 映射(map)/过滤(filterNotNull,first,takeWhile)/排序(sorted,reversed,shuffle)/组合(zip,zipWithNext,interleave)/归纳(fold/associate/flatten/maxBy) 五大类。
注: interleave/shuffle/flatten 应该是不太常见的用法, that's correct ,简单来说就是 交替流/乱序洗牌/铺平嵌套流

顺便一提:猜猜这个是什么: def zipPairs(xs): return zip(xs[0::2], xs[1::2])。当然这么简单定义不了 zipWithNext, 它和 fold (的累积变量)系出同门... 😂
但是 zipWithNext = lambda xs: zip(xs, xs[1:]) 我们仍没有用到重赋值,这证明了函数式的组合表现力,的确应该更新观念了。
" and ".join([f"{a} < {b}" for (a,b) in zipWithNext("1 2 3".split(" "))]) #'1 < 2 and 2 < 3'

很有趣不是吗? 定义式的代码,根本无需数学化的简写语法(点名批评 Py 的 slice)来保持他它们的易读性,这也是 Kotlin 的一大特色。
用 Visitor 你可以实现计算器(add a b = a + b) 和编译器(see (it:Name) = print(it.str) )
interface Eval<out T> { fun eval(): T }
sealed class Calc: Eval<Int> {
/* literal number */
data class L(val n: Int): Calc() { override fun eval() = n }
data class Op(val a: Calc, val b: Calc, val op: Int.(Int) -> Int): Calc() {
override fun toString() = "($a ? $b)"
override fun eval() = op(a.eval(), b.eval())
}
}
val c = Calc.Op(Calc.L(1), Calc.L(2), Int::plus)
c.eval() //3

我们实际在进行后序树遍历(postorder tree walk),先访问二叉树的两个子节点再访问其本身(如 Calc.Op 里的 this.op),因为求值操作依赖所有子节点的值。
注:Calc 的 sealed 删掉也行,表示其子类皆限于内部;其子类的 data 也可删, Op 的 val op 亦可为 (Int,Int) -> Int ,因为 Kotlin 里 receiver 和 param1 是可以自由互换的

试着把 Calc.Op.toString 里的 "?" 换掉。提示:作为超类构造器参数

看起来这已经足够了,但也还没够。我们所做的是用多态—虚方法 eval 实现了树遍历求值,但这是不可扩展的。
我们知道任何数乘0等于零,那么,想想你能不能在不改变 Calc 的前提下添加 optimize() 操作以实现?进一步解释,如果新建的操作不返回 Int 有可能吗?
如果有新节点(如 Negate 取负操作; 更 Kotlin 的创建语法树:operator fun unaryMinus 😂) 你可以非侵入式地添加,但新操作你做不到。

Visitor 就是解决这种问题的(当然,鱼和熊掌而已,选它会抛弃新节点的可扩展性)
interface CalcVisitor<out R> {
fun see(t: Calc.L): R
fun see(t: Calc.Op): R
}
fun <R> Calc.visitBy(vis: CalcVisitor): R = when (this) {
is Calc.L -> vis.see(this)
is Calc.Op -> vis.see(this)
//如果 Calc 不 sealed, 这里得加 else -> throw Error("impossible")
}
试着实现 Visitor 版本的 object Eval: CalcVisitor<Int> 。 补充: Calc 其实可以叫 Expr, 实现这一步再去看看咱的手写 Parser 入门王某人的解释器入门,你就可以设计一门简单的 数值/逻辑/动态对象属性访问+全局作用域/动态(链式Map.get)作用域 脚本解释器了!
visitBy 写在 Calc 里用 override 也可以,但它和新定义 extension fun 都是不能实现新节点扩展的
总而言之就是把 eval(): T 放到另一个对象(而不是this)上,多态仍然有效,而 Visitor 还多用了个重载(参数1类型),所以它是 override+overload 的典型结合设计模式。

选择 interface Eval ,新建操作要修改它但新增子类不用;选择 Visitor ,新建子类要为它实现 visitBy 且所有 Visitor 都要加对应的 see 方法,但新增操作只要加个 Visitor

两种方法里类型和其上操作的定义都是解耦合的,所以无法在保证 interface/subclass 或 Visitor/subclass 不变的情况下改变另一个。(看来有时候解耦合反而不是我们想要的?) 😂
也有完美的双向扩展做法 ("Object Algebra"),它把子类型构造器都建模为高阶函数,所以能做到同时拓展 节点/操作;不过其用处就更虚无缥缈了,请看我这篇文章

记住,对所有设计都只有「适合」而没有「最好」。 作为一个优秀的复用库设计者,你最应该考虑的不是怎样最快、怎么特性最多、怎么最"面向对象"、最"Pythonic",而是如何平衡这些参数之间的关系。有得必有失。

或许有人觉得代码写成什么样、符合某种教条一样的(命名)习惯会"很酷",但我觉得对库设计来说,没有什么新技巧、新花样,我们在这世界上很普通,就像我们不温不火的代码一样,只是它本来就应该是那样而已。 尽量自然地还原程序行为的真实面目、抛弃傲慢和偏见,才是设计者该有的追求。
duangsuse::Echo
用 Visitor 你可以实现计算器(add a b = a + b) 和编译器(see (it:Name) = print(it.str) ) interface Eval<out T> { fun eval(): T } sealed class Calc: Eval<Int> { /* literal number */ data class L(val n: Int): Calc() { override fun eval() = n } data class Op(val a: Calc…
呃... 既然都说到这一步了就顺带讲一下我之前关于 动态作用域/词法作用域的见闻,虽然我知道没人感兴趣的。

动态作用域(dynamic scoping)就是你们能想到的,末端是全局作用域的嵌套表。 理解为栈,当前层的key被赋值、最顶层的表里的key被取值,所以栈帧销毁后解析会有不同的结果。
动态作用域没什么问题,也被用于诸如(嵌套命名空间)类型名解析的地方,除非函数可以作为值——比如 (set! a 1) (def get-kst() (lambda () a) ) (def main(a) (get-kst) )
全局的a被取值是预期行为,但 def main (a) 里的"a"最新,处在栈顶,它会覆盖 (get-kst) 的那个"a",所以动态作用域亦称时序作用域,它只看某个命名的定义顺序。
(其实我一直想找到不同的示例,但看起来就这一个,必须让函数内的命名被新嵌套层遮盖)
(如果你是在写静态编译器,或者说命名的引用早在实际调用前已经确定,那用嵌套表也不会错,只需处理闭包支持的问题了)

如果你的脚本语言不支持词法域,千万别叫函数值为 "lambda",那太离谱了。 因为它只是有形式参数(formals) 的代码块而已,和 C 的函数指针甚至预处理宏没有区别。

之前看以前的冰封哥(@ice1000)的博文,在那个他竟然写过 Lice 脚本解释器、看龙书(编译原理丛书)的时候,关于动态作用域实现有三个 trick :
0. Map<String, Object> 然后每层 eval 都收环境表,即Py 的 f.eval(env=env.update({"a":2}))
这样实现闭包容易且符合理论但巨耗内存,是经常被我遗忘的做法
1. Vector<Map<String, Object>> 常规嵌套表。
2. Map<String, Vector<Object> > 少创建哈希表的等价。
(Java 里 Vector 和 Stack 的关系就像 Hashtable 和 HashMap, 是线程安全的)
3. Map<String, Object> + 每层 eval 的 Map<String, Object> 旧值备份
因为有不少用 (let (a 1) (+ a 1) ) 这种仅绑定名字 a 而无闭包的表达方式,是很实用的。

当然只有作用域变更才需分配新解析表,以上 eval 都只有 FunctionCall.eval 的情况(
0号不算 trick 因为它往往用于实现 static(lexical) scoping 😂,尽管用嵌套表级联查询+复制也可以实现闭包创建,显然默认就复制更直观。
它最常被不在意内存的新手想到或作为教程 (但对于习惯编程的人不用局部对象才是直觉的)

词法作用域(lexical scoping)的名字来源于它的一般实现方法。因为手写解析器一般都用递归下降法,解析过程本身已经反映出命名覆盖的栈结构且可以分配独立固定的变量存储。词法作用域是函数式的标准选择,就像函数式语言必须支持尾递归优化(tail call opt.)一样,它是正确闭包(closure)创建的依赖根本。
在词法域里, get-kst 的 "a" 是被闭包 (lambda () a) 包住的量(称为 Upvalue),所以它的值不会随调用的嵌套序而变动,这是我们预期的结果。

老实说,如果你不常规一点,为所有命名自动生成不冲突的名字如 a, a1, a2, ... 那也不会出现覆盖的问题,但那不优雅也不支持多线程(却可以加递归支持,类似C里的 static local 局部名全局静态分配)。

记得我们刚才说的 "动态作用域的"lambda"相当于 C 里的函数指针"吗? 函数指针是怎么调用的?它里面的形式参数引用是怎么解析到实参的? 🤔
答案是使用类似 bash 的 function add() { echo "$1+$2" }. 没有形式化参数,只有参数编号。
把动态作用域做到编译期
、给引用静态编号,再加上环境闭包的创建和调用支持,就成了词法作用域。

我和冰封大佬在幼稚的时候竟然都没有意识到,其实本来就不存在“名字冲突、被覆盖”和“解析不对”的问题,都是纯人类思维而不为计算机考虑的后果而已。

对了,如果分配全局名字(不是指在有可能遮盖下层同名时自动重命名,那要变动语法树)虽然不会冲突,却无法实现多线程复用代码对象,那如果把参数和局部变量在局部分配呢?
参数和局部变量本来就应该分配在栈上,然后以编号而非名字的形式引用,因为只有一个栈和一个全局表,不可用名字胡乱解析,为支持lambda的局部量引用自然就有了闭包,这是词法作用域出现的理由。
所以词法作用域才对计算机自然,动态作用域则是为人类准备的 trick (毕竟它是动态的)。

Lua 的解析器会把命名引用解析为 Local(这包含形参), UpValue 或 Global, 如果它不是任何一个外层函数的参数/局部量 那么就是动态到全局表里取值,否则就用闭包包起来。 当然 Lua 从来没有 function,只有 op = function() {} 的简写 :)

只是入门讲下而已…… 实现的话自己应该也明白必须分配什么类型、多大的存储空间。 🙉
提一句: Ruby 是直接把闭包的栈给整个 capture 下来了,这样加上程序计数器存储就能实现 Fiber(coroutine 协程) 了,啧啧啧...
(顺便科普术语: lambda calculus 里三种必要项: \formals -> body abstraction/function:abstract; (fn args) application/call:apply; arg substitution/variable ,
所谓的 alpha-conversation 就是重命名, beta-reduction 就是 evaluate, point-free 就是在 curry(多参转单参函数嵌套化) 里消除 ((\a -> f a) a)Abstract(id, Apply(op, id)) = op "两边约掉一个a" 注意两个a不是一个哦, alpha conversation.

substitution 包含 bound/unbound(即free) variable 两种情况(就是是否存于 local binding 啦),绑定变量是 local/upvalue, 自由变量一般作为 global 解释
在 Scheme 里除了函数调用还可以用 (let [a 1] (+ a 0)) 的方式创建 binding, 等价 ((lambda(a) (+ a 1) 1)

反正我不建议你们花太多精力在记忆这些莫名其妙的术语上,它们本不该是问题(不过是 Function/Call/NameRef 的意思写那么长); 另外函数的嵌套引入了代词 inner/outer function 你们应该知道是啥意思,
还有一些语言如 Python 2 支持局部函数但必须作为定义语句,支持 lambda 表达式的AST:语法树 变形被称为 lambda lifting, 从表达式提升(lift)到其函数的语句

子程序调用的 call-by-name 传名就是传表达式,其在惰性求值语言里等于 call-by-need 、 call-by-value 就是传值
另外在数据值或存储引用上 call-by-reference 传存储引用 所以可以重赋值 不过现在都结合到*指针/&引用类型里了)

顺便: 如果你真的要在 Kotlin/Java 实现树遍历解释器(interpreter),只有四个要点: 语句(statement)&表达式(expression)的对象模型、前(prefix)中(infix)后缀(postfix)表达式链与其优先级(precedence) (即 加减乘除余/逻辑/(下标与成员)访问/类型is&as操作)、自动类型转换和数值类型提升(如Int+Long=Long)、函数抽象/调用和作用域策略 #ce #cs #FP #PLT
语句其中可能包括量声明、重赋值、顺序/分支、循环或者选择支持尾递归,以及 return/break/continue 等跳转。

你要真能做到,不管是用了什么解析器框架/工具,抑或是没用什么状态机/算法,也绝对可以算工程界的大佬了。 (谁知道呢,反正我没真正做到过) 👍

不过有个打OI的朋友做到了: https://github.com/Mivik/kamet
而且还支持泛型推导 (这个)... 真的具有 Kotlin 的所有根本特性,我正愁想不出怎么写 :/
https://codon.com/hello-declarative-world 这篇文章我也看不懂 ……
https://github.com/ParserKt/ArgParser 也没发布……
我不该废几个小时说这么多的,反正各位看官不感兴趣。 #statement #life #dev
不管费多大力气,以后被自己遗忘意义又有多大呢…… 虽然我对同一类问题的描述也在越来越好。

如果我有个个人博客…… 不对,认识的大佬博客都无人问津,不管他们前端技能多好或者性格多热情。 如果他们的性格弱一点,反而会被最普通的工程师,甚至其队友轻视。

当个「大佬」,不管多热忱都只是自我陶醉的话,好像还是默默做项目、作高冷不食烟火的姿态比较好呢,不过我是不会变成那样的大佬的。

我不知道大佬的性格高冷和他们的「开发成功」有没有关系,是因为高冷所以成功的多、还是因为成功的多所以高冷? 但我始终乐意与别人分享我的经验和知识、努力提升自己的表达能力 让更多人看懂,抽点时间出来,不管有没有人能学到。

不知道专门研究这些东西的学院派大佬看到后会怎么想,不专业?不尊重学术?异想天开? 但我觉得一个问题真正的价值在于被人知晓、被人理解,仿佛它是有生命的一样;而不是与生俱来就有价值要让人为它头疼和浪费时间。如果有问题没人解释,那么较次的解释也可以接受。存在是第一意义。
duangsuse::Echo
呃... 既然都说到这一步了就顺带讲一下我之前关于 动态作用域/词法作用域的见闻,虽然我知道没人感兴趣的。 动态作用域(dynamic scoping)就是你们能想到的,末端是全局作用域的嵌套表。 理解为栈,当前层的key被赋值、最顶层的表里的key被取值,所以栈帧销毁后解析会有不同的结果。 动态作用域没什么问题,也被用于诸如(嵌套命名空间)类型名解析的地方,除非函数可以作为值——比如 (set! a 1) (def get-kst() (lambda () a) ) (def main(a) (get…
不过做这种事大概也不是没有意义的,这一次我发现了实现 dynamic scoping 的做法靠环境表复制也可以实现 lexical scoping 和闭包,所以说动态静态的 scoping 之间并不是完全隔离的,它们的本质都是命名覆盖,动态作用域的问题仅在于运行时在调用栈上的解析使得变量名唯一性问题显现,只有环境闭包一个区别,竟然是这样…… #PLT #cs #ce
This media is not supported in your browser
VIEW IN TELEGRAM
有些东西,其实本来就只是那个/那几种意思而已,可是说得太细又不好看、太粗略怕各位去看别人文章和「业界标准」的时候get不到同义词。

同一件事情、一件直白到可以隐含的事情,不同的人讲会有不同的说法,我也不知道该怎么说。