/tmp/duangsuse.sock
23 subscribers
303 photos
3 videos
92 files
337 links
从 duangsuse::Echo (@dsuse) 跟进出来的分支,将在作者恢复原帐号访问的时候合并删除。
Download Telegram
This media is not supported in your browser
VIEW IN TELEGRAM
/tmp/duangsuse.sock
😜 Sticker
好可爱的小海豹
大眼睛 那个像眉毛的部位和哈士奇、Doge 一样
本苏打算去看看盗版《天气之子》之类的动漫电影什么的,
在此之前,本苏不得不先谈谈解释器实现时的作用域实现 #CS #PLT

下文,我们涉及到主要术语:子程序(subroutine)、参数(argument)、局部变量(local variable)、上值(up-value)、闭包(closure)、词法作用域(lexical scoping)、动态作用域(dynamic scoping)

在编程中,我们经常……

算喽,简略点。
我们要处理的,就是实现局部变量,此外作为子程序参数的局部变量也包含在内。
此外,还有闭包——对闭包的创建有点像面向对象的架构器调用,我们需要包住 UpValue——也就是外部函数(outter function) 的局部变量,之后闭包被展开(调用,或者说求值)的时候可以用到包好的值。

function makeCounter() {
var count = 0;
return () => { count++; };
}

var counter = makeCounter()
counter() //: 0
counter() //: 1

假如你是李华,你的朋友 Ada 想要创建一门编程语言,但她不知道如何提供作用域,以使得程序表达更模块化。
你和 Ada 通过 SICP 和龙书了解了有这么两种作用域方案(划掉,怎么可能这么简单)
(-1). (大脑降级方案)使用全局作用域
0. (不全实现方案)使用 Map<String, Perfixed<String>>,给变量加函数名前缀。
1. 使用时序作用域,也即动态作用域。 你们用 Stack<Map<String, Object>> 存储变量,每一层都是 Map<String,Object>,然后每次取值时用名字往外找,第一个匹配的就是值,注意,如果你们用 String 作为名字(标识符),不同函数如果名字一样会导致产生可跨层引用,对返回函数指针的处理也可能遭致问题,因为子程序和变量值是完全分开的。
2. 使用 Map<String, StackOrSingle<Object>> 优化方案
3. 使用 pre-bind 预先指定使用到的变量,然后在进入可能修改变量的子程序前把老变量备份在系统栈的 Map 上,速度更快。

但是 Ada 同学很快否定了这些做法,理由是,那样容易造成混淆。

one = 1 # "one" in add1
def add1(n):
return one + 1

def each_people(people, fn):
for one in people: # most recent definition of "one"
return fn(one)

def print_grow1(person):
name, age = person
print(name + " aged " + add1(age)) # here

each_people([('Jack' 22), ('Rose', 19)], print_grow1)

那么咱来看 C++ 的 caputre: [some_upvalue](int arg1) { return arg1+some_upvalue; }
咱知道,子程序可以是递归的,也就是说,属从它的参数可能在同一时间有很多个,上面也说了。

但关于子程序 (x) => x + 1 最关键的其实是,我们知道 (x+1) 里的 x 是这个东西的第一个参数。
但除了参数,局部变量也是要有的,怎么办?
如果程序自己调用自己也是可能的,怎么办?

def just(n):
if n == 0: return 0
else return just(n-1)+1

just(2) //= just(1) +1 //= just(0) + 1 + 1


调用栈最深的时候,just 的参数是这样的:
[(2), (1), (0)] Top
所以,为了保证递归可能性,作用域的实现必须和栈有关。

那么局部变量怎么办呢?也在栈上分配嘛。
变量是什么?最关键却不太容易感知到的一点是,变量就是针对某个存储空间的,名字,或者标记符而已,它后面藏着『一个』——有且只有一个 存储空间。

就好像 f= (x, y) => x+y 里的 x, y 在生存周期上是『局部变量』,实际上是『参数』,再细化一点,x 代表第一个形式化参数、y 则是第二个。
f(1, 2) 最终就代表着 (1+2),这才是正常一点的世界观。

也就是 lambda 演算的世界观,不过这里才不会提太多术语呢!上面的 (x, y) 被称为 formals,形式参数、 (x+y) 被称为 body,体。

我们从 (x, y) => x+y 的『抽象 Abstraction』,给定两个实际参数 (1,2),填充到 (1+2) 的过程被称为『替换 Substitute』,这 x, y 就被叫做『代词 Substitution』(我自己翻译的……)
而从 (1+2) 算出最终的结果 3 的过程,被称为『收束 Reduce』(也是我自己翻译的…… 好像也叫『归约』)

这整个动作,则被称为『施用 Apply』,整个程序表达呢?就叫『应用Application』。

所以参数、局部变量、闭包该如何实现呢?(当然也有全局变量)

Lua呢,因为是手动递归下降解析器,所以直接在解析的时候,做好这个问题就完了。不就是一个名字对应一个存储空间,冲突按嵌套次序深者优先么。
这个问题怎么解决呢?每一层解析器,都得知道当前有什么参数,方便统一这个存储位置。
最根本的需要就是一个 Map<Symbol, Local> 符号表,代表本层的词法作用域信息(之前所有外层嵌套的参数和局部变量)

局部变量呢?当然,参数就是局部变量,不过是可以暴露出来给调用者提供的那种而已。
局部变量,定义的时候直接在栈帧上分配好了存储空间。至于都放在栈上如何不冲突呢?
这就涉及到调用约定的问题,调用者知道如何操纵咱共同的存储——系统栈,不会破坏要执行的子程序的取参行为。

function solve(a, b, c)
d = 233
return a, b, c, d -- (arg0, arg1, arg2, var0)
end

(注:Lua 包含常量折叠,也就是说 d 这个数值常量可能不会作为局部变量,这里忽略这种可能)
名字冲突呢?不存在的事情,字符串的名字可能冲突,存储空间是不可能冲突的。
名字如何在内部的闭包中共享呢?

让解析器知道,闭包里的 someVar 实际上代表着外部 someVar 的值,就对了。
不过这有点区别——其实也可以作为引用实现,假设某函数把一个包着自己变量的闭包递了出去,就有可能在变量还『活着』的时候,被自己调用的函数修改,我们这里不考虑这种情况。
Lua 的实现是,UpValue(也就是上面我们说的 someVar,某个闭包包住的外层函数变量)
有两个状态:open/close,我忘记它们代表什么了,好像和 GC 有关。

于是一个闭包长这样: [captures](formals) {body}
我们怎么创建它呢?考虑一下实现的细节——每个参数都是 Subst(val idx: FrameIdx, val name: String)、每个局部变量都是 Variable(同上),区别主要是参数不可被赋值,而这个存储(编号)分配的过程在解析阶段完成。

调用者知道的事情,就是按顺序把它们压到栈上,一般是后参先压(我是指传统 CDEF 的,Lua、JVM 里可能不是这样)。
然后子程序知道的事情,就是现在 (a,b) => a+b 里的 a,我们到栈的第一个位置取,这就保证了 a 指代同一个位置,并且符合参数传递的预期。

局部变量也是一样,都商议好分配位置,如知道 x 是在当前栈顶 -1,取/置都按照这个位置来即可。

于是,我们的参数可以传递,并且有名字、局部变量可以读写,并且也有名字。现在闭包对 UpValue 的引用怎么办?
我们觉得,既然闭包不可避免地是一种数据对象,那就给它安排一个数组,来存放其 capture 住的部分,其他的和普通函数无二,也有自己的代码、参数和局部变量。

UpValue(val idx: Idx, val name: String)

解析闭包时,记住外层函数的局部变量表,如果知道闭包引用了外层的东西,就把这个被引用的东西往 captures 列表一放,再对应地把对它的引用视为 UpValue(idx, name)
创建闭包时,检查闭包的 captures 列表,分配一个数组把 capture 住变量的实际值都存到里面去,合并入闭包对象的状态里,这样运行时就能拿到 UpValue 的信息了。


实现注记:
1. Subst 和 Variable 都是 Local,但 Subst 没有 setValueIn。
2. Upvalue 和 Local 都是 Resolvable,能够关于一个栈取到值
3. Function 和 Closure 都是 Subroutine,可以被提供参数调用也可以有局部变量
4. 在 Lua 里,函数是有 maxlocals 的,换句话说,Subroutine 就有使用的局部存储空间大小这一说,可以在运行前确定。
5. 解析器可以选择低内存优化和低计算优化
如果要为低内存优化,可以使用 ArrayMap,再加上 base map, shadowed map 先往局部遮盖的符号(可能没有)里找,再往上一层的遮盖找,这个数据和算法可以封装为一个 class,一般情况都不用。
6. 查找顺序:局部变量、参数、外部函数的 UpValue,否则就是全局变量。
Forwarded from Deleted Account
Wave.kt
728 B
#Kotlin 你说的好有道理啊,虽然花了很长时间我才知道 index/44100.0 再 %1.0 是什么意思,(time/ratio)*2*Pi*Short.MAX_VALUE 嘛,我之前也听说过 PWM,不过只知道能用来做 LED 的闪烁走马灯
Audio
然后像是这样
Forwarded from Deleted Account
有意思,我又可以把这个发上 GitHub 了(当然是到我那个文件分享里面)。
Forwarded from Deleted Account
上面一个是 1/700 的,下面是 1/9000 的
什么叫做 不 可维护性。
真是太过分了!这个程序员懂不懂什么叫做生命周期啊!? …… 算了,其实是我重构的时候没注意 🤪 ,早不抽提出来的好。 看来 ScopedPointer 或者干脆 SharedPointer 还是有作用的,调试了我十分钟啊!
audiooutput.zip
8.2 KB
#Qt 哈哈这个逗,没想到还有这种玩法。
gtk3-widget-factory 真舒服
audiooutput
67 KB
这个蛮好玩,就是 qCos(2*M_PI * tpos * sampleRate / format.sampleRate() * pwmRatio)
🦠 是什么东西 🧬💉
Forwarded from 永久封存 | Yuuta 台 | 😷 #Pray4Wuhan (Yuuta | Nya⠀)
Forwarded from 永久封存 | Yuuta 台 | 😷 #Pray4Wuhan (玉米狐狸)
/tmp/duangsuse.sock
#China #Low
#PLT 咳咳,咱来谈谈这个东西的实践的问题。
既然咱说是实践,那干脆就引入一个编程语言吧,我们叫它 MiniScheme。

Scheme 呢…… 不错!我们使用满是括号的 s-expression,这种基础表达方式。
(+ 1 2) ; 3

那么这个语言呢,我们打算用 Kotlin Common 开发(因为也没必要特别地放 JVM 上)
Literate Programming…… 算了,LKT 还没完成,在那之前,还是保持老编程风格好了。

怎么计算?慢慢看,但我们认为,输入代码是 CharIterator,这样支持 REPL 会比较方便。
怎么实现那种按 Enter 就求值的 REPL 呢?我们仿冒一下 Firefox Developer Console 和 IRB 的设计:一个表达式可以写在多行,就是说换行时如果输入不完整可以继续读下去。
实现简单,将会使用到 Observer<Expr> 呢,这样我们可以同时支持按单个 Expr 求值的 REPL,以及按整个文件求值的直接执行。

我们的任务是什么呢?
+ 支持 (fun () (println 1))(fun (a0 a1) (+ a0 a1)) 这样的 Abstraction 和 (f x y) 这样的 Application
+ 支持 Lexical Scoping,当然也包括闭包这个东西
+ 支持 null Name 以及 Boolean Int Long 的字面表达,注意 Int Long 也包含二进制和十六进制表示法
稍后也会支持 Float Double Char String 的字面表达,以及 `() 这种 Scheme 式 symbol list。
+ 支持 () 括号,<> 括号稍后会被我们用于语言扩展,以下我们会把它作为分词过程的语言构词,保留词。
此外,为了语言的一致性,我们要禁止对 [] 和 {} 的使用。

大家觉得,(+ 1 (* 2 3)) 是怎么计算的?我们会把它折叠到 (+ 1 6) 然后继续算下去。
每一个 (+)、(*) 都代表一颗『语法树』的子树,它们都有值,比如 1(* 2 3) 这些都是子树啊。
(+ expr expr) 这是它的类型,而 (+ 1 (* 2 3)) 的第一个 expr,是一个 literal "1",是子树。第二个是表达式,另一个子树。

就是这样,那我们的语言怎么计算呢?或者说它怎么知道 (* 2 3)6 呢?
参考一下上面的全局作用域吧,我们在全局作用域提供 (* int int) 这个操作。
我们可以把全局作用域做成一个 class,包含 get, set 方法,为了语言的灵活性,还得有一个 getFunction 方法。 算喽,既然都是动态类型,动态在全局作用域取那些函数,就检查强转吧。
interface GlobalScope: BasicMap<Symbol, Value> {
// val functions: BasicMap<Symbol, Value>
}
class Prelude: GlobalScope by mutableMapOf(
"*" to { a, b -> (a as Int) * (b as Int) }
)
这个语言认为它的 function 是 interface Invocable { operator fun invoke(vararg args: Value): Value }
这样,我们就能够提供对 (*) 计算的定义了——定义在 Kotlin 里面,一个基本操作。

过会我们会通过简单的面向对象子类型多态 override 入门,认识 Visitor Pattern,写关于 evaluate, dump, check 这类操作的 AST Walker。

— 稍后的语言扩展 —
+ 支持 call-by-need,对参数进行惰性求值
现在我们是 call-by-value,在调用前对参数进行求值。
+ 可利用 (f x _) 进行的 Partial Application
+ <1 + 2> 这样的中缀链,和 (infix l 4 +) (infix r 2 ->) 这种东西,不必支持特殊的词法规则

待会我也会讲一下 Trie 树的实现,这样对可扩展关键字的实现,也可以自己思考下。