课程内容复习
Lec5 函数式语言
-
急求值:急求值是严格求值(Strict),完全求值;急求值对应值调用,最安全
-
正规求值:函数中的形参每次出现均用实参替换(相当于传名调用)。要做多次实参计算。可能有副作用。
-
懒求值:是正规求值的特例。
-
它只到用到被结合的形参变量 的值时才对实参变元表达式求值(第一次出现),求值后将结果 值束定于形参变量,以后形参变量再次出现就不用求值了。
-
对于复杂的表达式如果子表达式求值对整个表达式求值没有影 响就不再求它。
-
短路(short circuit)求值,一般用作条件表达式,也叫短路条件。
E = Exp1 and Exp2 and …Expn 若Exp1求值为false,则E值已定为false。
再如: E = Exp1 or Exp2 or …Expn 若Exp1为true,则E值为true不论其它表达式取何值。
C、Ada及近代函数式语言均采用懒求值。
-
λ演算参考
并发——go语言
并发编程练习题:
假设存在一个生产者,依次产生数字0到9。存在一个奇数消费者,一个偶数消费者分别使用奇数和偶数,使用Go语言通道实现这个生产者消费者模型。使得数字按从小到大的顺序输出。
思路:新添加一个Channel,用以两个Consumer之间通信,传递一个flag。拿到flag的Consumer进行输出,没有拿到flag的Consumer阻塞。因为从0开始输出,所以flag最初在偶数消费者手中。
package main
import (
"fmt"
"sync"
)
// wg 用来等待程序结束
var wg sync.WaitGroup
func main() {
oddChannel := make(chan int)
evenChannel := make(chan int)
//排序标志,必须获取到这个量才能输出,false表示输出偶数,true表示输出奇数
flagChannel := make(chan bool)
// 计数加 3,表示要等待3个goroutine
wg.Add(3)
go producer(oddChannel, evenChannel)
go oddConsumer(oddChannel, flagChannel)
go evenConsumer(evenChannel, flagChannel)
//初始是偶数
flagChannel <- false
// 等待结束
wg.Wait()
}
func producer(odd chan int, even chan int) {
// 在函数退出时调用Done 来通知main 函数工作已经完成
defer wg.Done()
for i := 0; i < 10; i++ {
if i%2 == 0 {
even <- i
} else {
odd <- i
}
}
close(odd)
close(even)
}
func oddConsumer(odd chan int, flag chan bool) {
defer wg.Done()
for {
value, ok := <-odd
if !ok {
fmt.Printf("Consumer odd, work done\n")
return
}
outFlag := <-flag
if outFlag {
fmt.Printf("Consumer odd, get value %d\n", value)
}
flag <- false
}
}
func evenConsumer(even chan int, flag chan bool) {
defer wg.Done()
for {
value, ok := <-even
if !ok {
<-flag //n为偶数,偶数消费者需要在返回时处理掉最后一个flag
fmt.Printf("Consumer even, work done\n")
return
}
outFlag := <-flag
if !outFlag {
fmt.Printf("Consumer even, get value %d\n", value)
}
flag <- true
}
}
并发程序设计的模型:
-
共享变量模型
-
消息传递模型 (目前最灵活,最常用的模型):程序中不同的进程之间通过显式的方法(如函数调用、运算符)等传递消息来互相通信,实现进程之间的数据交换,同步控制等。
-
数据并行模型
-
面向对象模型
-
Go 语言的并发通过 goroutine 特性完成。
-
goroutine 类似于线程,但是可以根据需要创建多个 goroutine 并发工作。
-
goroutine 是由 Go 语言的运行时调度完成,而线程是由操作系统调度完成。
-
Go 语言提供 channel 在多个 goroutine 间进行通信:Go语言中的通道(channel)是一种特殊的类型。goroutine 通过通道就可以通信。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。
-
Go语言带缓冲的通道
OO——Java类
- Q:Java的对象如何算相同?
- A:java的equals。从根上讲,equals关系是说两个对象引用指向一个地址(即同一对象),如果你想比较两个对象的某个属性值是否相等,则需要重写equals方法,虽然有些方法已经给你重新写好了,但是你要确定这点。类似的问题:深拷贝和浅拷贝。
- Q:Java类的多态,内部类访问外部类的私有变量
- A:这里的TimePrinter类使用了外围类TalkingClock类的变量beep,而自己没有beep这个实例域或变量。对内部类,它即可以访问自身的数据域,也可以访问创建它的外围类的数据域。这是因为内部类的对象总有一个隐式引用,这个引用指向了创建它的外部类对象。内部类和外部类其实是处于同一个级别,内部类不会因为定义在方法中就会随着方法的执行完毕而跟随者被销毁。内部类引用的外部变量的生命周期可能在内部类方法还未结束的时候就已经被销毁了,然而内部类的某个方法还没有执行完,这个时候他所引用的外部变量已经找不到了。
Scheme
7个原始操作符:
- quote x:返回x。可以简记为 'x
$ (quote a)
a
$ 'a
a
$ (quote (a b c))
(a b c)
- atom x:如果x是一个原子或是空表,返回原子t;否则返回()。Lisp中用原子t表示真,空表表示假。
$ (atom 'a)
t
$ (atom '(a b c))
()
$ (atom '())
t
既然有了一个自变量需要求值的操作符,我们可以看一下quote的作用。通过引用(quote)一个表,我们避免它被求值。
一个未被引用的表作为自变量传给像atom这样的操作符将被视为代码:
$ (atom (atom 'a))
t
反之一个被引用的表仅被视为表,在此例中就是有两个元素的表:
$ (atom '(atom 'a))
()
// '(atom 'a) 不会被执行(求值),这是一个表,有两个元素。
// 如果一个表达式是表,我们称第一个元素为操作符,其余的元素为自变量。
- eq x y:如果x和y的值是同一个原子或都是空表,返回t;否则返回()。
$ (eq 'a 'a)
t
$ (eq 'a 'b)
()
$ (eq '() '())
t
- car x:期望x的值是一个表并且返回x的第一个元素。
$ (car '(a b c))
a
- cdr x:期望x的值是一个表并且返回x的第一个元素之后的所有元素。
$ (cdr '(a b c))
(b c)
- cons x y:期望y的值是一个表并且返回一个新表,它的第一个元素是x的值,后面跟着y的值的各个元素。
$ (cons 'a '(b c))
(a b c)
$ (cons 'a (cons 'b (cons 'c '())))
(a b c)
$ (car (cons 'a '(b c)))
a
$ (cdr (cons 'a '(b c)))
(b c)
- cond (…)…(…):p表达式依次求值直到有一个返回t。如果能找到这样的p表达式,相应的e表达式的值作为整个cond表达式的返回值。
$ (cond ((eq 'a 'b) 'first)
((atom 'a) 'second))
second
- map:把对元素的函数拓展到一个表
(map f ‘( )) = ‘( )
(map f (cons a y) = (cons ( f a ) (map f y ))
$ define (square n) (* n n)
$ (map square ‘ (1 2 3 4 5))
(1 4 9 16 25)
- reduce(fold):接收一个列表、一个初始值以及一个函数,将该函数作为特定的组合方式,将其递归地 应用于列表的所有成员,并返回最终结果。
(reduce f ‘( ) v) = v
(reduce f (cons a y) v = f(a, reduce f y v)
fold + ‘0 ‘(1,2,3),其具体的操作流程是(((0+1)+2)+3),并最终返回其总和。
练习题:
- 链表原子元素求和
(DEFINE (adder lis)
(COND
((NULL? lis) 0)
(ELSE (+ (CAR lis) (adder (CDR lis))))
))
- 求两个链表中的元素的交集
member函数:原子是否在表中
( DEFINE ( member atm lis )
( COND
(( NULL? lis) #F )
(( EQ? atm ( CAR lis )) #T)
(ELSE ( member atm (CDR lis)))
))
(DEFINE (guess lis1 lis2)
(COND
((NULL? lis1) ‘())
((member (CAR lis1) lis2)
(CONS (CAR lis1) (guess (CDR lis1) (lis2)))
( ELSE (guess (CDR lis1) lis2)
- 将给定的链表中最后一个元素移除
(DEFINE (tlrm lis)
( COND
((NULL? lis) '())
((NULL? CDR lis) '())
(ELSE
(CONS CAR lis (tlrm CDR lis))
)
))
另一种做法:表reverse,然后cdr,表再reverse
(define (reverse lst)
(reverse-helper lst '()))
(define (reverse-helper lst acc)
(cons
((null? lst) acc)
(else
reverse-helper (cdr lst) (cons (car lst) acc))
))
如果写成这样会有问题:
(define (reverseList lst)
(COND
((NULL? lst) '())
(ELSE (CONS (reverseList(CDR lst)) (CAR lst)))
))
The problem I'm having is that if I input a list, lets say (a b c) it gives me (((() . c) . b) . a).
因为cons的第二个参数需要时lis,第一个参数需要是值,如果(cons '(3 2) (1)) 则返回 ((3 2) 1).
那么删除列表最后一个元素就是
(define (tlrm lis)
(reverse (cdr reverse(lis)))
)
- 将给定的链表中的最后一个元素移出来//理解错作业了。。
( DEFINE ( last_item lis )
( COND
(( NULL? lis) '())
(( NULL? (CDR lis)) CAR lis)
( ELSE ( last_item (CDR lis)))
)
)
函数式——Scala实现PageRank
var links = sc.parallelize(Array(("A", List("B", "C", "D")), ("B", List("A")), ("C", List("A", "B")), ("D", List("B", "C"))), 1)
var ranks = links.mapValues(x => 1.0)
for (i<-1 to 10) {
var c = links.join(ranks).flatMap {
case (pageId, (links, rank)) => links.map(dest => (dest, rank / links.size))
}
ranks = c.reduceByKey((x, y) => x + y).mapValues(x => 0.15 + 0.85*x)
}
ranks.saveAsTextFile("ranks")
- map操作是针对集合的典型变换操作,它将某个函数应用到集合中的每个元素,并产生一个结果集合。与scheme的map类似。
比如,给定一个字符串列表,我们可以通过map操作对列表的中每个字符串进行变换,让每个字符串都变成大写字母,这样变换后就可以得到一个新的集合。下面我们在Scala命令解释器中,演示这个过程:
scala> val books = List("Hadoop", "Hive", "HDFS")
books: List[String] = List(Hadoop, Hive, HDFS)
scala> books.map(s => s.toUpperCase)
res0: List[String] = List(HADOOP, HIVE, HDFS)
s => s.toUpperCase
这种表达式被称为“Lamda表达式”,在Java8以后有引入这种新的特性,Scala也拥有该特性。
(参数) => 表达式 //如果参数只有一个,参数的圆括号可以省略
可以看出,Lambda表达式实际上是一种匿名函数,大大简化代码编写工作。s => s.toUpperCase,它的含义是,对于输入s,都都执行s.toUpperCase操作。
-
flatMap是map的一种扩展。在flatMap中,我们会传入一个函数,该函数对每个输入都会返回一个集合(而不是一个元素),然后,flatMap把生成的多个集合“拍扁”成为一个集合。flatMap之后c变成List[(String, Double)]
-
join操作将两个RDD按key连接value,c的形式是Array[(String, (List[String], Double))]
-
mapValues按links的分区方式改变value
-
reduceByKey将相同key的value按定义的规则合并。
-
Scala的条件选择
def gcd(a: Int,b: Int):Int = {
if(a>b) gcd(a-b,b)
else if (a<b) gcd(a,b-a)
else a
}
- Scala的基本数据类型与Java中的基本数据类型是一一对应的,不同的是Scala的基本数据类型头字母必须大写,比如Int、Long、String、Char、Double、Float等,数据结构有列表、元组、集、映射等。
- Scala中::, +:, :+, :::, ++的区别
-
::
该方法被称为cons,意为构造,向队列的头部追加数据,创造新的列表。用法为x::list
,其中**x
为加入到头部的元素,无论x
**是列表与否,它都只将成为新生成列表的第一个元素,也就是说新生成的列表长度为list的长度+1(btw,x::list
等价于list.::(x)
):+
和+:
两者的区别在于:+
方法用于在尾部追加元素,+:
方法用于在头部追加元素,和::
很类似,但是::
可以用于pattern match ,而+:
则不行. 关于+:
和:+
,只要记住冒号永远靠近集合类型就OK了。- **
:::
**该方法只能用于连接两个List类型的集合。 ++
该方法用于连接两个集合,list1++list2
。
Python MRO生成算法
BFS,DFS。类图上的广度优先和深度优先
C3,按基类出现顺序执行的一个拓扑排序(?)
L(Child(Base1,Base2))= [ Child + merge( L(Base1), L(Base2),Base1Base2 )]
L(object)= [ object ]
L的性质:结果为列表,列表中至少有一个元素即类自己。
+: 添加到列表的末尾,即 [ A + B ] = [ A,B ]
merge:
- ① 如果列表空则结束,非空,读merge中第一个列表的表头,
- ② 查看该表头是否在merge中所有列表的表尾中。
- ②–>③ 不在,则放入最终的L中,并从merge中的所有列表中删除,然后回到①中
- ②–>④ 在,查看当前列表是否是merge中的最后一个列表
- ④–>⑤ 不是,跳过当前列表,读merge中下一个列表的表头,然后回到 ②中
- ④–>⑥ 是,异常。类定义失败。
表头: 列表的第一个元素 (列表:ABC,那么表头就是A,B和C就是表尾)
表尾: 列表中表头以外的元素集合(可以为空)
merge 简单的说即寻找合法表头(也就是不在表尾中的表头),如果所有表中都未找到合法表头则异常。
L(D) = L(D(O))
= D + merge(L(O))
= D + O
= [D,O]
L(B) = L(B(D,E))
= B + merge(L(D) , L(E))
= B + merge(DO , EO) # 第一个列表DO的表头D,其他列表比如EO的表尾都不含有D,所以可以将D提出来,即D是合法表头
= B + D + merge(O , EO) #从第一个开始表头是O,但是后面的列表EO的表尾中含有O所以O是不合法的,所以跳到下一个列表EO
= B + D + E + merge(O , O)
= [B,D,E,O]
同理:
L(C) = [C,E,F,O]
L(A(B,C)) = A + merge(L(B),L(C),BC)
= A + merge(BDEO,CEFO,BC) # B是合法表头
= A + B + merge(DEO,CEFO,C) # D是合法表头
= A + B + D + merge(EO,CEFO,C) # E不是合法表头,跳到下一个列表CEFO,此时C是合法表头
= A + B + D + C + merge(EO,EFO) # 由于第三个列表中的C被删除,为空,所以不存在第三个表,只剩下两个表;此时E是合法表头
= A + B + D + C + E + merge(O,FO) # O不是合法表头,跳到下一个列表FO,F是合法表头,
= A + B + D + C + E + F + merge(O,O) # O是合法表头
= A + B + D + C + E + F + O
= [A,B,D,C,E,F,O]
指称语义 IMP小语言
- 存储域
Store = Location → ( stored Storable + undefined + unused)
empty-store : Store
allocate : Store →Store × Location
deallocate : Store ×Location → Store
update : Store × Location × Storable→Store
fetch : Store × Location→Storable
empty_store = λloc.unused
allocate sto =
let loc = any_unused_location (sto) in (sto [loc→ undefined],loc)
deallocate (sto,loc) = sto [loc → unused]
update (sto,loc,stble) = sto [loc→stored stble]
fetch (sto,loc) =
let stored_value (stored stble) = stble
stored_value (undefined) = fail
stored_value (unused) = fail
in
stored-value (sto(loc))
- 环境域
Environ = ldentifier→(bound Bindable + unbound)
empty-environ : Environ
bind : ldentifier×Bindable → Environ
overlay : Environ×Environ → Environ
find : Environ×ldentifier→Bindable
enpty-environ = λI. unbound
bind (I,bdble) = λI'. if I'=I then bound bdble else unbound
overlay (env',env) = λI. if env' (I)/=unbound then env' (I) else env (I)
find (env,I) =
let bound_value (bound bdble) = bdble
bound_value (unbound) = ⊥
in
bound_value (env (I))
- 过程式小语言IMP
Command ::= Skip
| ldentifier := Expression
| let Declaration in Command
| Command; Command
| if Expression then Command else Command
| while Expression do Command
Expression ::= Numeral
| false
| true
| Ldentifier
| Expression + Expression
| Expression < Expression
| not Expression
| ...
Declaration ::= const ldentifier = Expression
| var ldentifier : Type_denoter
Type_denoter ::= bool
| int
Value = truth_value Truth_Value + integer lnteger
Storable = Value
Bindable = value Value + variable Location
execute: Command →(Environ→Store→Store)
execute〖C〗 env sto = sto’
evaluate: Expression → (Environ→Store →Value)
evaluate 〖E〗 env sto= …
elaborate: Declaration→ (Environ→Store→Environ×store)
elaborate 〖D〗 env sto =
辅助函数有如前所述的empty-environ,find,overlay,bind,empty-store,allocate,deallocate,update,fetch。以及sum,less,not等辅助函数。此外,再增加一个取值函数:
coerce: Store×Bindable→Value
coerce (sto,find (env,I))
= val
= fetch (sto,loc)
execute 〖Skip〗 env sto = sto
execute 〖I:= E〗 env sto =
let val = evaluate E env sto in
let variable loc = find (env,I) in
update(sto,loc,val)
execute 〖let D in C〗 env sto =
let (env',sto') = elaborate D env sto in
execute C (overlay (env',env)) sto’
execute 〖C1; C2〗 env sto =
execute C2 env (execute C1 env sto)
execute 〖 if E then C1 else C2〗 env sto =
if evaluate E env sto = truth_value true
then execute C1 env sto
else execute C2 env sto
execute 〖while E do C〗=
let execute_while env sto =
if evaluate E env sto = truth_value true
then execute_while env (execute C env sto)
else sto
in
execute_while
elaborate 〖 const I = E 〗 env sto =
let val = evaluate E env sto in
(bind (I,value val),sto)
elaborate 〖 var I:T〗 env sto =
let (sto', loc)= allocate sto in
(bind (I,variable loc),sto')
evaluation 〖*I〗 env sto =
let loc = find( env, I) in
let loc1 = fetch (sto, loc) in
sto(loc1)
evaluation 〖&I〗 env sto =
find(env, I)
execute 〖*I = E〗 env sto =
let val = evaluate E env sto in
let loc = find( env, I) in
let loc1 = fetch (sto,loc) in
sto(loc1)
update(sto, val, loc)
逻辑式语言
-
归结演绎推理
-
-
把前提中所有命题换成子句形式。
-
取结论的反,并转换成字句形式,加入1中的子句集。
-
在子句集中选择含有互逆命题的命题归结。用合一算法得出新子句(归结式),再加入到子句集。
-
重复3,若归结式为空则表示此次证明的逻辑结论是矛盾,原待证结论若不取反则恒真。命题得证。否则继续重复3。
-
过程式
左值和右值:将 L-value 的 L, 理解成 Location,表示定位,地址。将 R-value 的 R 理解成 Read,表示读取数据。
束定:把声明名字和存储对象或语义实体连接起来,如常量、变量的存储体、函数体、过程体、类型、异常。
- 静态束定:编译按数据类型为名字分配合适大小的存储对象, 该对象首地址填入符 号表即为静态束定, 编译后变量表销毁, 故静态束定不能变。
- 动态束定:运行时将名(字)束定于其体(得其语义操作),解释型语言一般保留名字表,运行中束定。
束定可以看作是完成名字指向存储对象指针的过程,但它与指针不同,首先束定是编译器(解释器)做的。为每一个名字分配其语法要求的存储,也可以跨越时间,编译时占个位置,运行时再分配。
较老的非结构化语言一般采用静态束定。
- 动态束定的例子
词法作用域:程序正文给出的嵌套声明的作用域。
动态作用域:递归每展开一层束定一次,且保留至递归终止。运行中动态束定。
参数机制:
- 传值调用:实参表达式先求值;将值复制给对应的形参。形参和实参有同样大小的存储。过程运行后一般不再将改变了的形参值复制回实参。
- 传名调用:将未计算的参数表达式直接应用到函数内部,每次都会重新计算。
- 引用调用:引用参数实现时, 编译器在子程序堆栈中设许多中间指针,将形参名束定于此指针,而该指针的值是实参变量的地址(在主程序堆栈框架内),在子程序中每次对形参变量的存取都是自动地递引用到实参存储对象上。
当表达式有副作用的时候,传名调用会多次触发这个副作用。