SICP 笔记 - 0x03

archive time: 2025-02-25

闲了这么久,终于想起来还有两个坑没填

Lecture 3A

Lecture 2B 介绍了 复合数据 的概念

而这一部分列举了几个复合数据的例子,并展示了抽象的优势,即设计与实现分离, 利用统一接口,无需关心具体方法的实现,以此来完成解耦,降低复杂度

除此之外,闭包的概念也被提出,在这里里的闭包更倾向于数学概念,即某个集合在某个运算下封闭:

这里, 集合在运算 下就是封闭的,而 LISP 对象这个集合在 CONS 运算下就是封闭的, 也就是两个任意 LISP 对象通过 CONS 可以再构造出一个 LISP 对象,这赋予了我们构造更复杂结构的能力

在其他语言中,例如 Rust,这个就类似于使用元组也是可以构造复杂的结构的, 但是 Rust 中有更好的选择,即 enumstruct

线段

第一个例子就是用 cons 来模拟线段

一条线段可以由两个点来表示,这里举例使用二维平面, 所以一个线段可以表示为 ,即:

type Point = (f64, f64);
type Seg = (Point, Point);

相应的,我们有如下接口:

fn make_point(x: f64, y: f64) -> Point;
fn make_seg(p1: Point, p2: Point) -> Seg;
fn seg_start(s: Seg) -> Point;
fn seg_end(s: Seg) -> Point;

其中一种可能的实现如下:

fn make_point(x: f64, y: f64) -> Point { (x, y) }
fn make_seg(p1: Point, p2: Point) -> Seg { (p1, p2) }
fn seg_start(s: Seg) -> Point { s.0 }
fn seg_end(s: Seg) -> Point { s.1 }

即便我们将 PointSeg 使用 struct 实现, 用户使用这些接口还是能操线段,而无需关系其具体实现

列表

另一个非常实用的例子就是 LIST,即列表

在 Haskell 和 LISP 以及 Scheme 中,列表的实现都可以视作是链表,即:

[1, 2, 3, 4] ~> (1, (2, (3, (4, nil))))

其中这个 nil 可以视作是空列表,或者 NULL

这个在 Rust 中可以表示为:

enum List<T> {
    Cons(T, Box<List<T>>),
    Nil,
}

impl<T> std::fmt::Display for List<T>
where
    T: std::fmt::Display,
{
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[")?;
        let mut current = self;
        loop {
            match current {
                List::Cons(e, next) => {
                    if let List::Cons(_, _) = **next {
                        current = next;
                        write!(f, "{}, ", e)?;
                    } else {
                        writeln!(f, "{}]", e)?;
                        break;
                    }
                }
                List::Nil => {
                    write!(f, "]")?;
                    break;
                }
            }
        }
        Ok(())
    }
}

fn main() {
    let one_to_four = List::Cons(
        1,
        Box::new(List::Cons(
            2,
            Box::new(List::Cons(3, Box::new(List::Cons(4, Box::new(List::Nil))))),
        )),
    );
    println!("{}", one_to_four);
    // => [1, 2, 3, 4]
}

当然,一直使用 CONS 太繁琐了,所以在 LISP 中有一套简化的方法,即 LIST 函数:

(list 1 2 3 4)

我们在 Rust 中也可以使用宏来实现类似方法

macro_rules! list {
    () => {
        List::Nil
    };
    ($($arg:tt)*) => {{
        [$($arg)*].iter().rev().fold(List::Nil, |acc, e| match acc {
            List::Cons(a, inner) => List::Cons(e, Box::new(List::Cons(a, inner))),
            List::Nil => List::Cons(e, Box::new(List::Nil)),
        })
    }}
}

fn main() {
    let one_to_four = list![1, 2, 3, 4];
    println!("{}", one_to_four);
    // => [1, 2, 3, 4]
}

当然,在 Rust 有一个等价的结构叫做 Vector,所以我们不必自己实现这个功能

由于 LISP 中的列表只是 CONS 的等价物,所以我们可以用 CARCDR 来操作列表:

(define 1-to-4 (list 1 2 3 4))
;; get the second value
(car (cdr 1-to-4))
;; => 2

在 Rust 中表示为:

fn car<T>(lst: &List<T>) -> Option<&T> {
    match lst {
        List::Cons(e, _) => Some(e),
        List::Nil => None,
    }
}

fn cdr<T>(lst: &List<T>) -> Option<&List<T>> {
    match lst {
        List::Cons(_, p) => Some(&*p),
        List::Nil => None,
    }
}

fn main() {
    let one_to_four = list![1, 2, 3, 4];
    let second = car(cdr(&one_to_four).unwrap()).unwrap();
    println!("{}", second);
    // => 2
}

如果我想要对这个列表中的每一个元素都应用某个操作,例如都加上 ,或者都乘以

我们可以有如下操作1

fn trans_list<A, B>(lst: &List<A>, f: impl Fn(A) -> B) -> List<B>
where
    A: Clone,
{
    match lst {
        List::Cons(e, p) => List::Cons(f(e.clone()), Box::new(trans_list(&*p, f))),
        List::Nil => List::Nil,
    }
}

fn main() {
    let one_to_four = list![1, 2, 3, 4];
    let eleven_to_fourteen = trans_list(&one_to_four, |x| x + 10);
    println!("{}", eleven_to_fourteen);
}

这种操作在现在有了一个统一的称呼,即 MAP, 将某个容器中的每一个元素都应用上某种变换,然后得到新的容器

当然,有时候我们只是想要在容器上执行某些东西,例如逐行打印容器内的元素,那么就可以使用另一种方法 FOR-EACH

在 Rust 中,这些可以通过为容器实现 Iter 来实现:

struct ListIter<'a, T> {
    curr: &'a List<T>,
}

impl<'a, T> std::iter::Iterator for ListIter<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        match self.curr {
            List::Cons(e, inner) => {
                self.curr = &*inner;
                Some(e)
            }
            List::Nil => None,
        }
    }
}

impl<T> List<T> {
    pub fn iter<'a>(&'a self) -> ListIter<'a, T> {
        ListIter { curr: self }
    }
}

impl<'a, A> FromIterator<A> for List<A>
where
    A: Clone,
{
    fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
        iter.into_iter()
            .collect::<Vec<_>>()
            .iter()
            .rev()
            .fold(List::Nil, |acc, e| match acc {
                List::Cons(a, inner) => List::Cons(e.clone(), Box::new(List::Cons(a, inner))),
                List::Nil => List::Cons(e.clone(), Box::new(List::Nil)),
            })
    }
}

fn main() {
    let one_to_four = list![1, 2, 3, 4];
    let eleven_to_fourteen = one_to_four.iter().map(|&&e| e + 10).collect::<List<i32>>();
    println!("{}", eleven_to_fourteen);
    // => [11, 12, 13, 14]
    eleven_to_fourteen.iter().for_each(|e| print!("{} ~ ", e));
    // => 11 ~ 12 ~ 13 ~ 14 ~
    println!()
}

元语言抽象

在工程上,解决复杂度问题的一种方法,就是构造一门新的语言,这种语言被统称为 特定领域语言(DSL)

我们可以在前端看到这种方法被大规模应用, 即每过一段时间都会有一个新的前端框架被发明,用于解决某些痛点, 而这些前端框架就可以看成是用 JavaScript 编写的 DSL, 当然,这同时也会引入一定的复杂度,因为大家需要学习新的东西了, 即便新的东西比原来要解决的问题要更简单

我们学习一门新的语言,需要注意的有三点:

  • 原始类型,即基本数据类型
  • 复合类型,如何模拟更复杂的数据
  • 数据抽象,即如何操作数据

其中第二点和第三点就是我们控制复杂度的关键, 因为我们可以通过复合类型和数据抽象,来将要解决的问题进行 抽象

例如我们需要构造一种图片,那么我们可以将图片这种数据抽象出来,作为新语言的原始类型, 然后,对于图片的操作,我们可以简化为缩放、旋转、翻转和拼接这四种操作, 通过这种方式,就可以解决我们的复杂需求

我们对于数据和方法的封装过程实际上就可以看成是构建 DSL 的过程, DSL 不过是原语言的一个库

一个比较出名的 DSL 的例子就是 Ruby 与 HomeBrew2

Lecture 3B

系统健壮性

上面介绍了如何构建系统和解决问题,但是在解决系统复杂度问题上,还有一点非常重要,那就是维护

一个稳健的系统,应当对小的变化不敏感,问题中的小变化应该只导致解决方案中的小变化, 为了做到这点,我们在解决问题时不应该只解决问题本身,应该还需要解决问题的一些临近的相关问题

一种方案就是构建一门可以描述对应问题的语言,通过这门特定语言来解决问题, 这样在问题出现变化的时候,我们同样可以用这门语言去描述这种变化, 只需要较小的修改就可以覆盖变化产生的新问题

符号微分

有了上述的思想以及方法,我们就可以着手解决一个问题,那便是函数求导, 并且与使用 dx 来进行数值模拟不同,这次要做的是对多项式进行符号微分, 其基本原理就是函数求导的法则:

其中 是任意常数

具体的 Rust 实现和分析如下

这里是一个直接的实现,没有考虑各种优化,只是对于微分结果有一定的简化:

#[derive(Clone, Debug)]
enum DiffElement {
    Constant(f64),
    Variable,
    Add(Box<DiffElement>, Box<DiffElement>),
    Mul(Box<DiffElement>, Box<DiffElement>),
    Div(Box<DiffElement>, Box<DiffElement>),
    Pow(Box<DiffElement>, f64),
}

fn is_zero(x: &f64) -> bool {
    x.abs() <= core::f64::EPSILON.sqrt()
}

impl DiffElement {
    pub fn make_add(e1: DiffElement, e2: DiffElement) -> DiffElement {
        match (&e1, &e2) {
            (DiffElement::Constant(a), DiffElement::Constant(b)) => DiffElement::Constant(a + b),
            (DiffElement::Constant(a), _) if is_zero(a) => e2,
            (_, DiffElement::Constant(a)) if is_zero(a) => e1,
            _ => DiffElement::Add(Box::new(e1), Box::new(e2)),
        }
    }

    pub fn make_mul(e1: DiffElement, e2: DiffElement) -> DiffElement {
        match (&e1, &e2) {
            (DiffElement::Constant(a), DiffElement::Constant(b)) => DiffElement::Constant(a * b),
            (DiffElement::Constant(a), _) if is_zero(&(a - 1.0)) => e2,
            (DiffElement::Constant(a), _) if is_zero(a) => DiffElement::Constant(0.0),
            (_, DiffElement::Constant(a)) if is_zero(&(a - 1.0)) => e1,
            (_, DiffElement::Constant(a)) if is_zero(a) => DiffElement::Constant(0.0),
            _ => DiffElement::Mul(Box::new(e1), Box::new(e2)),
        }
    }

    pub fn make_div(e1: DiffElement, e2: DiffElement) -> DiffElement {
        match (&e1, &e2) {
            (DiffElement::Constant(a), DiffElement::Constant(b)) if !is_zero(b) => {
                DiffElement::Constant(a / b)
            }
            (DiffElement::Constant(a), _) if is_zero(a) => DiffElement::Constant(0.0),
            (_, DiffElement::Constant(a)) if is_zero(&(a - 1.0)) => e1,
            _ => DiffElement::Div(Box::new(e1), Box::new(e2)),
        }
    }

    pub fn make_pow(e: DiffElement, n: f64) -> DiffElement {
        match (&e, n) {
            (DiffElement::Constant(a), _) if !is_zero(a) => DiffElement::Constant(a.powf(n)),
            (DiffElement::Pow(u, p), _) => DiffElement::make_pow(*u.clone(), p + n),
            (_, _) if is_zero(&(n - 1.0)) => e,
            _ => DiffElement::Pow(Box::new(e), n),
        }
    }

    pub fn diff(self) -> DiffElement {
        match self {
            DiffElement::Constant(_) => DiffElement::Constant(0.0),
            DiffElement::Variable => DiffElement::Constant(1.0),
            DiffElement::Add(e1, e2) => DiffElement::make_add(e1.diff(), e2.diff()),
            DiffElement::Mul(e1, e2) => DiffElement::make_mul(e1.diff(), e2.diff()),
            DiffElement::Div(e1, e2) => DiffElement::make_div(
                DiffElement::make_add(
                    DiffElement::make_mul(e1.clone().diff(), *e2.clone()),
                    DiffElement::make_mul(
                        DiffElement::Constant(-1.0),
                        DiffElement::make_mul(*e1, e2.clone().diff()),
                    ),
                ),
                DiffElement::make_pow(*e2, 2.0),
            ),
            DiffElement::Pow(e, n) => DiffElement::make_mul(
                DiffElement::Constant(n),
                DiffElement::make_mul(DiffElement::make_pow(*e.clone(), n - 1.0), e.diff()),
            ),
        }
    }
}

fn main() {
    // (x^2 + 3) / (x^3 - 8 x^2 + 3)
    let eq = DiffElement::make_div(
        DiffElement::make_add(
            DiffElement::make_pow(DiffElement::Variable, 2.0),
            DiffElement::Constant(3.0),
        ),
        DiffElement::make_add(
            DiffElement::make_pow(DiffElement::Variable, 3.0),
            DiffElement::make_add(
                DiffElement::make_mul(
                    DiffElement::Constant(-8.0),
                    DiffElement::make_pow(DiffElement::Variable, 2.0),
                ),
                DiffElement::Constant(3.0),
            ),
        ),
    );
    println!("{:?}", eq.diff());
}

手动格式化后的输出如下:

Div(
|   Add(
|   |   Mul(
|   |   |   Mul(Constant(2.0), Variable),
|   |   |   Add(
|   |   |   |   Pow(Variable, 3.0),
|   |   |   |   Add(
|   |   |   |   |   Mul(Constant(-8.0), Pow(Variable, 2.0)),
|   |   |   |   |   Constant(3.0)
|   |   |   |   )
|   |   |   )
|   |   ),
|   |   Mul(
|   |   |   Constant(-1.0),
|   |   |   Mul(
|   |   |   |   Add(Pow(Variable, 2.0), Constant(3.0)),
|   |   |   |   Mul(Constant(3.0), Pow(Variable, 2.0))
|   |   |   )
|   |   )
|   ),
|   Pow(
|   |   Add(
|   |   |   Pow(Variable, 3.0),
|   |   |   Add(
|   |   |   |   Mul(Constant(-8.0), Pow(Variable, 2.0)),
|   |   |   |   Constant(3.0)
|   |   |   )
|   |   ),
|   |    2.0
|   )
)

即:

可以看到,还是有很多优化空间的,我们再手动优化一下可以得到:

可以发现这个答案就是我们想要的答案

在早期的 Maxima3,符号微分的实现就是用类似的 LISP 代码来实现的, 这种实现引出了下一部分所要讲的内容,即 模式匹配


好久没写博客了,重新开始写一下子有些不适应,特别是一些措辞和格式上还是有些不习惯,例如要使用斜体还是粗体

嘛,不过最近我会慢慢恢复更新,最起码一周一更,到时候应该就会好很多了


1

这里可以使用尾递归优化,但是为了直观,故采用直接的方式

2

Cask::DSL.HomeBrew [DB/OL].(2025-01-13)[2025-02-25]. https://rubydoc.brew.sh/Cask/DSL.html

3

Maxima: v5.47 [CP/OL].(2023-05-31)[2025-02-25].https://maxima.sourceforge.io/zh/index.html