乱点天赋点之Scheme

680 阅读4分钟

原文地址,zhuanlan.zhihu.com/p/359661142

Scheme是什么

它是Lisp的一种方言,而Lisp是一门古老的语言。麻省理工曾经将Scheme作为计算机同学的入门语言,更有著名的《计算机程序的构造和解释》作为入门教程。

Lisp是函数式的鼻祖,它是以函数为核心的语言,函数的入参和返回值可以是数据,也可以是函数,在Scheme中,称之为过程。

本文将介绍Scheme基本的语法和JS借鉴Scheme的地方。本文所有的代码都可以work。

语法

1、规则,以括号包裹表达式 (symbol param1 param2 ...)

最简单的表达式

(+ 1 2)   
输出结果 3

2、定义变量

(define size 2)   
(display (+ size 5))   
输出结果 7

3、求圆的面积

(define (area-of-circle r)    
 (* 3.14 (* r r))   )   
(display (area-of-circle 2))
输出结果 12.56

4、条件表达式 关键字有 cond、if、else、and、or、not 这里介绍一下cond用法,其他用法差不多。 这里举一个求绝对值的例子

(define (abs1 x)
    (cond ((> x 0) x)
          ((= x 0) 0)
          ((< x 0>) (- x))
          )
  )

  (display  (abs1 -5))
  输出结果 5
  (display  (abs1 5))
  输出结果 5

5、在结构内部定义子函数 还是以求圆的面积,我们将半径平方的计算当成内部子函数。

(define (area-of-circle r)
    (define (square x)
      (* x x)
    )
    (* 3.14 (square r))
  )


  (display  (area-of-circle 2))

  输出结果 12.56

6、实现递归 我们以斐波那契的求值为例。 斐波那契的定义 F0 = 0 F1 = 1 Fn = Fn-1 + Fn-2(n≧2)

(define (fibonacci n)
    (cond ((= n 0) 0)
          ((= n 1) 1)
          (else (+
            (fibonacci (- n 1))
            (fibonacci (- n 2)))
          )
    )
  )

  (display  (fibonacci 10))
  输入结果55

7、迭代实现斐波那契 实际上斐波那契递归的方式虽然直观,但是存在函数调用栈过多的问题。 当 n >= 2; f(2) = f(1) + f(0); f(3) = f(2) + f(1);

即可得出,从前往后遍历的规律如下。令当前项为a,上一项为b。

a <- a + b

b <- a

(define (fibonacci n)
    (define (fibonacci-iter a b count)
      (if (= count 0) 
           b
          (fibonacci-iter (+ a b) a (- count 1))
      )
    )
    (fibonacci-iter 1 0 n)
  )

  (display  (fibonacci 10))
  输出 55

8、lambda表达式

(define square (lambda (x) (* x x)))
  (display  (square 10))

  输出 100

9、创建局部变量 编程多了你就会了解,局部变量,上下文环境是必不可缺的。

我有一个二元一次方程 f(x) = (x + 1)^2 + x + 1; 在数据里,我们会设 a = x + 1,这个其实就是定义局部变量。同样在编程里我们也使用这种方式,所以lisp很大一部分是由数据衍生而来的。

(define (sum1 x)
    (let ((a (+ a 1)))
      (+ a (* a a)))
  )
(display (sum1 10))
输出132

10、有序对 在Lisp里,本身不具备复杂的数据结构,比如数组、树形结构。我们先以有理数的例子介绍有序对,后续我们会实现一个数组结构。通过这个数组结构我们就能实现map、filter、reduce,这三个应该是纯函数典型的例子。

关键字 cons、car、cdr,分别对应 构造函数(Construct),寄存器地址的部分内容(Content of Address part of Register),寄存器递减的部分内容(Content of Decrement part of Register)。

有理数的定义,能以分子分母表示的数则有有理数

(define (make-rational n d) (cons n d))
  (define (numerator x) (car x))
  (define (denominator x) (cdr x))

  (define (print-rat x)
    (newline)
    (display (numerator x))
    (display "/")
    (display (denominator x))
  )

  (define one-half (make-rational 1 2))

  (print-rat one-half)
  输出 1/2

但是我们输出的结果不一定是最简化的,比如 4/6,可以约分为 2/3,那我们可以在make-rational中添加公约数的逻辑

这里说一下求最大公约数的技巧,大的数除以小的数,如果除尽,则小的数为最大公约数。否则用大的数剪小的数得到a。再把a和小的数带入之前的步骤,递归下去。
  (define (make-gcd a b) (if (= b 0)
      a
      (make-gcd b (remainder a b))))

  (define (make-rational n d)
    (let ((m (make-gcd n d)))
      (cons (/ n m) (/ d m))
    )
  )
  (define (numerator x) (car x))
  (define (denominator x) (cdr x))

  (define (print-rat x)
    (newline)
    (display (numerator x))
    (display "/")
    (display (denominator x))
  )

  (define four-sixth (make-rational 4 6))

  (print-rat four-sixth)
  输出 2/3

11、list对象

它是scheme实现复杂数据结构的基础的,它的结构是这样的 (list 1 2 3),返回的是(1 2 3)集合。它是通过有序对连接的。如下图,cons的第一个指针指向第一个数,第二个指针指向第二个数,这个数也可以是list结构。所以通过最简单的结构就可以构造各种复杂的结构。

(display (list 1 2 3))
输出 (1 2 3)

Scheme的宏

宏是指将代码当作数据一样处理,可以定义自己的语法规则。可能比较抽象。可以参考这个链接。

Common Lisp 的宏(Macro)到底神奇在哪?​

我举一个简单的例子。正常情况下它的功能和函数有点像,你再每个打印后面添加句号。

(define-macro addDotAfterDisplay        
(lambda (x)      
 (display (string-append x "."))))
(addDotAfterDisplay "ok")   

但是上面的功能普通函数也能实现,我们来看看普通函数实现不了的。宏可以对符号进行判断,比如当前是否是if语句。

if结构是Scheme中的最基本的条件分支控制结构,if过程的框架如下:

(if 测试条件
    then-分支
    else-分支)

if语句的例子:

> (define p 100)
> (if (> p 80) "big" "small")
"big"

这个时候使用when语句就更好,它只有then或者else分支。

(define-macro when1     
(lambda (test . branch)       
(list 'if test         
(cons 'begin branch))))

(define x 20)     
(display (when1 (< x 30) x))
 输出 20

如何在线编写Scheme

1、直接通过这个在线网站,Scheme 在线测试

2、本地搭建scheme环境

下面的安装步骤来自于这篇文章,我如何用二十天刷完 SICP

参考 这个回答 安装 MIT-Scheme。Scheme 是 Lisp 的一种方言,也是 SICP 使用的语言。MIT-Scheme 是 MIT 实现的 Scheme 解释器,和 MIT 教材 SICP 更配哦 打开一个你最喜欢的编辑器(我用的 Sublime Text 3),编写一段 Scheme 代码并保存成 test,可以没有后缀名 打开命令行,输入 scheme < test,即可执行程序

JS中借鉴Scheme的地方

如何实现map

在JS中,我们如何实现一个数组的map

const arr = [1, 2, 3];     
arr = arr.map(val => val / 2);

js中如果数组不支持map,自己实现,应该是这样的,map(arr, fn); 而在scheme中,我们自己来构造一个map。它应该是这样的 (define map fn items)。

(define (add2 x)
      (+ x 2))
  (display (map add2 '(1 2 3)))

  // 输出 (3 4 5)

1、首先需要实现一个迭代器,其次要支持方法回调,并把参数传入回调。下面我们实现一个返回绝对值的例子。

 (define (myMap fn items)        
(if (null? items) '()       
(cons (fn (car items))             
(myMap fn (cdr items))))   )

(display (myMap abs (list -1 2 -3)))

这里又用到了有序对,对于一个list,car list返回当前一个引用的值,cdr list返回从第二个值到最后的值。

filter和reduce也是类似的,通过有序对和list,迭代的处理结果。

JS中不得不说的this

有些编程语言的this代表当前上下文,但是JS中的this指向却要在运行时才能知道,并且this还可能指向undefined,就算是一些老程序员,也会写出this的bug。而Scheme中没有this的概念,而React hooks中也尽量避免使用了this,this带来了一些便利,但是会使程序很不稳定。

总结

Lisp或者Scheme是一门有追求的语言,用最少的API构建了几乎最复杂的语言系统,它之后所有的语言,多多少少都参考了这位鼻祖。

下面对Lisp的评价来自阮一峰老师的博客,为什么Lisp语言如此先进?(译文)

Lisp语言诞生的时候,就包含了9种新思想。这9种思想也在各种语言中出现。

  1. 条件结构
  2. 函数也是一种数据类型。
  3. 递归
  4. 变量的动态类型。
  5. 垃圾回收机制。
  6. 程序由表达式组成。
  7. 符号(symbol)类型。
  8. 代码使用符号和常量组成的树形表示法(notation)。
  9. 无论什么时候,整个语言都是可用的。

后话,前领导说我乱点天赋点,之前学了一段时间Android,现在又学函数式语言,不专精,前端首先要把使用的框架(React)理解透彻,然后深入理解JS这门语言,再吃透到JS的宿主环境V8。乱点天赋点虽然视野开阔了,但是基础还是差,我后面会花更多的时间在大前端上,不再乱点天赋点了。

最后附上SICP的英文版下班连接,英文版 SICP。中文版晚上也有免费资源。

参考资料

《计算机程序的构造和解释》

我如何用二十天刷完 SICP

Common Lisp 的宏(Macro)到底神奇在哪?

为什么Lisp语言如此先进?(译文)