Racket 学习

Posted on Sep 12, 2021

为什么要学

吃饱了撑的

基本语法

racket 的语法和 lisp 有点像(我没学过 lisp ),都是由一堆括号组成的,可以看一下例子

1
2
(+ 1 1) ; means 1 + 1
(* 1 2) ; means 1 * 2

; 表示单行注释的开头。

racket 有一个非常宽松的关键字系统,除了

( ) [ ] { } " , ' ``` ; # | \

以外和一些数字相关的字符外。

基本类型

racket 支持 boolean,number,string,bytestring。

number 的范围就广了…

举个例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
> 1
1
> 1/2
1/2
> 1+2i
1+2i
> 3.14
3.14
> 1e5
100000.0
> "QWERASD"
"QWERASD"
> "Bugs \u0022Figaro\u0022 Bunny"
"Bugs \"Figaro\" Bunny"
>

racket 中的 true 为 #t false 为 #f

define 和简单的表达式,函数

define 可以定义一个常量,一个函数等

1
2
3
4
5
(define pi 3.14)
(define (square x)
  (* x x)
  )
(square pi)

这里我们就定义了一个 pi 当然标准库里也有 pi,我们这样就相当于把库里的 pi 替换了,我们甚至可以 (define define ...) 这样可以把 define 搞没~

回到正题,我们定义了一个 pi ,然后定义了一个函数 square ,这个函数有一个参数,形参是 x ,函数体是 (* x x)

最后,我们执行了 square 这个函数,参数是之前定义的 pi

这段代码换成命令式大概长这样:

1
2
3
4
5
double pi = 3.14;
void square(double x) {
	return x * x;
}
square(pi);

当然 racket 里的 x 可能是 “泛型” 。

racket 中还有一些看起来有意思的函数

1
2
3
4
(eq? 1 3) ; #f
(eq? 1 1) ; #t
(>= 1 1) ; #t
(> 1 1) ; #f

再举一个有意思的例子,我们写一个把输入参数翻倍的函数,是字符串就重复自身,是数字就乘2。

一般我们会这么写:

1
2
3
4
5
6
7
(define (double x)
  (if
   (string? x)
   (string-append x x)
   (* 2 x)
  )
 )

其实,你甚至能返回 “操作符

1
2
3
4
5
6
(define (double x)
  (
   (if (string? x) string-append *) ; 返回了操作符,对后面的参数进行操作
   x x
   )
  )

set!

set! 就是对之前定义过的变量再次赋值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
(define a 1)
(define (add) (set! a (+ a 1)))
(add)
a      ; 2
(add)
a      ; 3
(add)
a      ; 4
(add)
a      ; 5

set!-values

emmmm不知道怎么形容,写个例子吧

1
2
3
4
5
(define (swap a b) (set!-values (a b) (values b a))
  (display a)
  (newline)
  (display b))
(swap 1 2) ; 2 1

lambda

看一下就懂啦

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(lambda (x y z) (+ x y z)) ; 简单的 3个数相加,这是没调用的情况,输出 #<procedure>
((lambda (x y z) (+ x y z)) 1 2 3) ; 6
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (double x)
  (if
   (string? x)
   (string-append x x)
   (* 2 x)
  )
 )
(double ((lambda (x) (+ x x)) 6)
        )

可选的参数列表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
(define opt (lambda (x [arg1 1] [arg2 2])
          (+ x arg1 arg2)
          ))
(opt 1)
(opt 1 2)
(opt 1 2 3)(define opt (lambda (x [arg1 1] [arg2 2])
          (+ x arg1 arg2)
          ))
(opt 1)      ; 4
(opt 1 2)    ; 5
(opt 1 2 3)  ; 6

关键字参数

也就是给参数指定个“名字”,使用的时候无关参数顺序

1
2
3
4
5
6
7
8
9
(define opt (lambda (x #:arg1 [arg1 1] #:arg2 [arg2 2])
          (+ x arg1 arg2)
          ))
(opt 1)
(opt #:arg1 1 2)
(opt 1 #:arg2 2 #:arg1 3)
(opt 1)      ; 4
(opt 1 2)    ; 5
(opt 1 2 3)  ; 6

if

现在来看看 if 该如何写

首先, if 是个有 3 个参数的 函数 (我随便叫的),所以我们应该这么写

1
2
3
(if (> 1 2)
    "1 > 2"
    "1 <= 2")

显然的,第一个参数是条件,第二个参数是 true 的时候的走向,第三个参数是 false 的时候的走向。

由于 racket 中 没有 else if 所以我们只能嵌套的写 if。

1
2
3
4
5
6
7
8
(define (what x)
   (if (> x 90) ; x > 90 ?
       "NB"     ; x > 90!
       (if (> x 60)    ; x <= 90
           "PASS"      ; x > 60!
           "NOT PASS") ; x <= 60!
       )
   )

when & unless

when 就是一个没有 else 的 if

1
(when (< 1 0) (display "< 1 0")) ; nothing to to

unlesswhen 的相反啦

1
(unless (< 1 0) (display "< 1 0")) ; < 1 0

提到了 if 必然会有逻辑与和逻辑或。在 racket 中,andor 也很有意思:

1
2
3
4
5
6
7
8
(and #t #t) ; #t
(and #t #f) ; #f
(or #t #t) ; #t
(or #t #f) ; #f
(and #t #t 6 7) ; 7
(and #t #f 6 7) ; #f
(or #f #t 6 7) ; #t
(or #f #f 6 7) ; 6

可以这么理解:

and 会寻找 #f ,进行返回并停止求值,如果找不到,那就返回最后一个值。

or 会寻找一个不为 #f 的值,然后返回并停止求值。

cond

上面的 if 写起来又臭又长,所以我们可以换种方法写

cond 给了一种非常方便的方式,可以写无限多的条件,如果第一个 condition expr 返回为 true,程序就会终止向下求值。

1
2
3
4
5
6
7
8
9
(define (gt x)
  (cond
    [(>= x 90) ">= 90"]
    [(>= x 80) ">= 80"]
    [(>= x 70) ">= 70"]
    [(>= x 60) ">= 60"]
    [else "< 60"]
  )
  )

case

看例子吧,都是和分支跳转相关的,值得注意的是,最后一条语句可以使用 else

1
2
3
4
5
6
7
8
9
> (let ([v (random 6)])
    (printf "~a\n" v)
    (case v
      [(0) 'zero]
      [(1) 'one]
      [(2) 'two]
      [(3 4 5) 'many]))
5
'many

let 声明

define 在函数中声明终归不太方便,我们可以使用 let 声明变量,看看官方的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(let ([x (random 4)]
        [o (random 4)])
    (cond
     [(> x o) "X wins"]
     [(> o x) "O wins"]
     [else "cat's game"]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
> (let ([me "Tarzan"]
        [you "Jane"])
    (let ([me you]
          [you me])
      (list me you)))
'("Jane" "Tarzan")

(let ( [id (expr)]* ) expr+ )

let*

let 无法使用之前声明的值,比如我想这么写

1
2
3
4
5
6
(let ([a 6]
      [b 7]
      [c (+ a b)]
      )
  (display c)
  )

是行不通的,c = a + b 我们需要使用 let*

1
2
3
4
5
6
(let* ([a 6]
      [b 7]
      [c (+ a b)]
      )
  (display c)
  )

有意思的多次声明,看官方的例子:

1
2
3
4
5
6
7
(define f          ; f 是一个 lambda 函数
  (lambda (append) ; append 是参数
    (define cons (append "ugly" "confusing")) ; cons 被赋值为 '("ugly" "confusing")
    (let ([append 'this-was]) ; 让 append 赋值为 'this-was
      (list append cons))))   ; 返回一个 '(append cons)
> (f list)
'(this-was ("ugly" "confusing"))

let-values let*-values

要是我们要声明一大堆变量的话,写一堆let或者方括号们显然不是很方便,或者说一个函数返回两个值,我们想对这两个值进行相应的变量绑定,比如说 (quotient/remainder 14 3) 就会返回两个值。

1
2
(let-values ([(a b c d) (values 1 2 3 4)])
  (+ a b c d))

values

用于返回多个值

1
2
3
4
> (values 1 2 3)
1
2
3

pair

pair 是一对值,他只有两个参数,或者说,他是一个特殊的list。

1
2
'(1 . 3)
(cons 1 3)

我们可以使用 cdr 获取 pair 的第一个元素,用 cdr 获取 pair 的最后一个元素

list

如何定义一个list:

1
2
3
4
(list 1 "2" 3.14)
'(1 2 3.14)
(quote (1 2 3 4))
(cons 1 (cons 2 (cons 3 empty))) ; '(1 2 3) cons  `construct` 的缩写

一些 list 相关的函数,看官方的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
> (length (list "hop" "skip" "jump"))        ; count the elements
3
> (list-ref (list "hop" "skip" "jump") 0)    ; extract by position
"hop"
> (list-ref (list "hop" "skip" "jump") 1)
"skip"
> (append (list "hop" "skip") (list "jump")) ; combine lists
'("hop" "skip" "jump")
> (reverse (list "hop" "skip" "jump"))       ; reverse order
'("jump" "skip" "hop")
> (member "fall" (list "hop" "skip" "jump")) ; check for an element
#f
> (first '(1 2 3)) ; 取头节点 car 也是ok的
1
> (rest '(1 2 3)) ; 除了头节点剩下的部分 cdr 也是ok的
'(2 3)

我们可以用 length 获取列表长度,那么如何自己写一个 my_len

1
2
3
4
5
6
7
(define (my_len lst)
  (cond 
        [(empty? lst) 0]
        [else (+ 1 (my_len(rest lst)))]
        )
  )
(my_len '(1 2 3)) ; 3

这么写效率可能不是很ok,我们可以看到其实递归的过程是这样的:

1
2
3
4
5
6
7
8
(my_len (list "a" "b" "c"))
= (+ 1 (my_len (list "b" "c")))
= (+ 1 (+ 1 (my_len (list "c"))))
= (+ 1 (+ 1 (+ 1 (my_len (list)))))
= (+ 1 (+ 1 (+ 1 0)))
= (+ 1 (+ 1 1))
= (+ 1 2)
= 3

我们可以写一个尾递归的版本:

1
2
3
4
5
6
(define (my_len lst)
  (define (it lst len)
    (cond
     [(empty? lst) len]
     [else (it (rest lst) (+ len 1))]))
  (it lst 0))

可以看出函数的执行过程是这样的:

1
2
3
4
5
6
(my_len (list "a" "b" "c"))
= (it (list "a" "b" "c") 0)
= (it (list "b" "c") 1)
= (it (list "c") 2)
= (it (list) 3)
3

apply

看官方用例吧

1
2
3
4
5
6
7
8
(define (avg lst)
  (/ (apply + lst) (length lst)))
> (avg '(1 2 3))
2
> (avg '(1 2))
3/2
> (avg '(1 2 3 4))
5/2

vector

vector 是一个定长的数组,和 list 不同的是,他对元素有一个常数时间的查询和更新。

和 list 相同的是,他的元素也可以是不同类型的。

如何声明一个 vector

1
2
#(1 2 3)
#(1 "2" 3.14)

对 vector 操作:

1
(vector-ref #(1 "2" 3.14) 2) ; 3.14

vector 可以和 list 进行相互的转换,可以看官方的例子:

1
2
3
> (list->vector (map string-titlecase
                     (vector->list #("three" "blind" "mice"))))
'#("Three" "Blind" "Mice")

hashmap / hashtable

racket 中哈希表 key 的比较取决于创建哈希表使用的是哪个函数。

在 racket 中,任何的 racket值 都能作为 key

  1. equal?make-hash
  2. eqv?make-hasheqv
  3. eq?make-hasheq
1
2
3
4
5
6
7
8
9
(define hs (make-hash))
(hash-set! hs "6" 7)
(hash-set! hs '666 777)
(hash-set! hs '(789,654) 1)

(hash-set! hs 7 6)
(hash-ref hs 7) ; 6
(hash-set! hs 7 8)
(hash-ref hs 7) ; 8

为了方便,我们还可以这样创建哈希表

1
2
3
(define hs #hash((1 . 2) (2 . 3))) 
; 当然你也可以用 #hasheq #hasheqv 创建相应的 hashmap 类型
(hash-ref hs 1) ; 2

hashmap 里面的键值对是 pair 所以我们做初始化的时候可以使用 pair 的形式初始化。

可变 hashtable 可以选择创建弱保留键的 hashtable,看官方的例子:

1
2
3
4
5
> (define ht (make-weak-hasheq))
> (hash-set! ht (gensym) "can you see me?")
> (collect-garbage)
> (hash-count ht)
0

boxes

这玩意相当于只有一个元素的vector。

看官方例子吧:

1
2
3
4
5
6
7
8
> (define b (box "apple"))
> b
'#&"apple"
> (unbox b)
"apple"
> (set-box! b '(banana boat))
> b
'#&(banana boat)

map

map 函数就是对个列表内的元素干一遍活,然后返回一个修改后的列表。

1
(map sqrt '(1 4 9 16 25 36)) ; '(1 2 3 4 5 6)

此外,还提供了 andmap ormap 函数,返回一个 bool值,看例子就明白了

1
2
(andmap string? (list "a" "b" "c")) ; #t
(ormap string? (list "a" "b" 6)) ; #t

当然啦

map andmap ormap 的第二个参数(也就是那个函数)都是可以接受多个列表作为参数的,但是这些列表都必须有相同的长度

(map/ormap/andmap (function name) (arguments...))

看个例子就懂了

1
(map (lambda (a b) (+ a b)) '(1 2 3) '(4 5 6)) ; '(5 7 9)

filter

字如其名,就是过滤掉列表中一些不想要的东西,返回过滤后的结果。

1
(filter number? '(1 2 3 4 "666" 3.14 '(1 2 3))) ; '(1 2 3 4 3.14)

foldl

foldl 就是给一个初始值,然后给一个列表,他会把列表里的东西用函数和初始值“混合”在一起,下面是一个对列表里的数进行 乘2求和,结果加1的例子

1
2
3
4
(foldl (lambda(elem n) (+ n (* 2 elem))) ; n + 2 * elem
       1                                 ; 初始值为 1
       '(0 1 2 3 4))                     ; elements
; 21

quote 和 '

之前在声明列表的时候用过这个玩意 quote '

'quote 的一个语法糖罢了。

如果你使用 ' 后接一个 identifier,那么这个 identifier 就会变成一个所谓的 “符号”。

看官方的例子吧

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
> map
#<procedure:map>
> (quote map)
'map
> (symbol? (quote map))
#t
> (symbol? map)
#f
> (procedure? map)
#t
> (string->symbol "map")
'map
> (symbol->string (quote map))
"map"

众所周知,racket使用前缀表达式进行计算,如果我们想用中缀怎么做呢

1
2
3
(1 . + . 2) ; (+ 1 2)
(1 . < . 2) ; (< 1 2)
'(1 . + . 2) ; '(+ 1 2) 中缀转前缀