虚函数的 Scheme 视角

虚函数

引言

C++ 里有虚函数这个概念,虚函数根据参数的类型,从一组函数中选择一个调用。注意,这里将对象方法理解为第一个参数是对象本身的函数。

为了支持虚函数特性,C++ 会在使用多态特性的时候设置虚函数表。虚表在背后起作用,为了理解它的作用,可以在 scheme 中模拟这个过程。

Scheme 没有静态类型系统,所以需要给变量打上类型标签。

;; tag operations
(define (attach-tag type-tag contents)
  (cons type-tag contents))

(define (type-tag datum)
  (if (pair? datum)
      (car datum)
      (error "Bad tagged datum -- TYPE-TAG" datum)))

(define (contents datum)
  (if (pair? datum)
      (cdr datum)
      (error "Bad tagged datum -- CONTENTS" datum)))

虚表

可以将虚表理解为一个映射:(操作名 参数类型列表)-> (具体函数),从这里就可以看出,虚函数其实是一组操作的名称,虚函数在概念上是一种通用型操作,对不同类型的对象可能有不同的行为。

;; v table operations
(define vtable (make-hash))

(define (put op type item)
  (hash-set! vtable (list op type) item))

(define (get op type)
  (hash-ref vtable (list op type)))

为了让虚函数利用虚表,需要先提取参数的类型,用虚函数和参数类型从虚表中取出相应函数。

;; vtable lookup
(define (apply-generic op . args)
  (let* ((type-tags (map type-tag args))
         (proc (get op type-tags)))
    (apply proc (map contents args))))

最后,将得到的函数应用在脱去标签的数据上。注意,虚表中取出的不一定就是最终作用在数据上的函数,也有可能是另一个虚函数,将它应用在数据上会引发另一次虚表查找。这种基于标签的分发可以无限地进行下去。

下面是一些虚函数地例子:

;; virtual functions
(define (add x y)
  (if (and (number? x) (number? y))
      (+ x y)
      (apply-generic 'add x y)))

(define (sub x y)
  (if (and (number? x) (number? y))
      (- x y)
      (apply-generic 'sub x y)))

(define (mul x y)
  (if (and (number? x) (number? y))
      (* x y)
      (apply-generic 'mul x y)))

(define (div x y)
  (if (and (number? x) (number? y))
      (/ x y)
      (apply-generic 'div x y)))

(define (equ? x y)
  (if (and (number? x) (number? y))
      (= x y)
      (apply-generic 'equ? x y)))

(define (=zero? x)
  (if (number? x)
      (zero? x)
      (apply-generic '=zero? x)))

具体函数可以通过如下方式注册到虚表上:

;; rational-package
(define (install-rational-package)
  ;; internal procedures
  (define (numer x) (car x))
  (define (denom x) (cdr x))
  (define (make-rat n d)
    (let ((g (gcd n d)))
      (cons (/ n g) (/ d g))))
  (define (add-rat x y)
    (make-rat (+ (* (numer x) (denom y))
                 (* (numer y) (denom x)))
              (* (denom x) (denom y))))
  (define (sub-rat x y)
    (make-rat (- (* (numer x) (denom y))
                 (* (numer y) (denom x)))
              (* (denom x) (denom y))))
  (define (mul-rat x y)
    (make-rat (* (numer x) (numer y))
              (* (denom x) (denom y))))
  (define (div-rat x y)
    (make-rat (* (numer x) (denom y))
              (* (denom x) (numer y))))

  (define (equ? x y)
    (and (= (numer x) (numer y))
         (= (denom x) (denom y))))

  (define (=zero? x)
    (= (numer x) 0))

  ;; interface to rest of the system
  ;; add to v table
  (define (tag x) (attach-tag 'rational x))
  (put 'add '(rational rational)
       (lambda (x y) (tag (add-rat x y))))
  (put 'sub '(rational rational)
       (lambda (x y) (tag (sub-rat x y))))
  (put 'mul '(rational rational)
       (lambda (x y) (tag (mul-rat x y))))
  (put 'div '(rational rational)
       (lambda (x y) (tag (div-rat x y))))
  
  (put 'equ? '(rational rational)
       (lambda (x y) (equ? x y)))
  (put '=zero? '(raional)
       (lambda (x) (=zero? x)))

  (put 'make 'rational
       (lambda (args)
         (let ((n (car args)) (d (cadr args)))
           (tag (make-rat n d))))))

虚表的分解

问题的核心是清晰的:要通过某种方式,根据操作名称和操作数类型决定操作。借助一个全局的查找表,让不同的类型在其中填写和自己有关的部分,就实现了。

请想象,将虚表按类型拆分,放在不同的类型里。对数据应用函数变为调用数据对象的分派函数并传入函数名。这就是面向对象语言的一般做法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值