SICP - Imperative versus Functional implementation of factorial

时间:2016-10-20 18:45:00

标签: functional-programming lisp racket sicp imperative-programming

I am studying the SICP book with Racket and Dr. Racket. I am also watching the lectures on:

https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-001-structure-and-interpretation-of-computer-programs-spring-2005/video-lectures/5a-assignment-state-and-side-effects/

At chapter 3, the authors present the concept of imperative programming.

Trying to illustrate the meaning, they contrast an implementation of a factorial procedure using functional programming and to one which used imperative programming.

Bellow you have a recursive definition of an iterative procedure using functional programming:

(define (factorial-iter n)
  (define (iter n accu)
    (if (= n 0)
        accu
        (iter (- n 1) (* accu n))))
  ; (trace iter)
  (iter n 1))

Before the professor was going to present an imperative implementation, I tried myself.

I reached this code using the command "set!":

(define (factorial-imp n count product)
  (set! product 1)
  (set! count 1)
  (define (iter count product)
    (if (> count n)
        product
        (iter (add1 count) (* product count))))
  (iter count product))

However, the professor's implementation is quite different of my imperative implementation:

(define (factorial-imp-sicp n)
  (let ((count 1) (i 1))
    (define (loop)
      (cond ((> count n) i)
            (else (set! i (* count i))
                  (set! count (add1 count))
                  (loop))))
    (loop)))

Both codes, my implementation and the professor's code, reach the same results. But I am not sure if they have the same nature.

Hence, I started to ask myself: was my implementation really imperative? Just using "set!" guaranties that?

I still use parameters in my auxiliary iterative procedure while the professor's auxiliary iterative function does not have any argument at all. Is this the core thing to answer my question?

Thanks! SO users have been helping me a lot!

2 个答案:

答案 0 :(得分:3)

Your solution is splendidly mad, because it looks imperative, but it really isn't. (Some of what follows is Racket-specific, but not in any way that matters.)

Starting with your implementation:

(define (factorial-imp n count product)
  (set! product 1)
  (set! count 1)
  (define (iter count product)
    (if (> count n)
        product
        (iter (add1 count) (* product count))))
  (iter count product))

Well, the only reason for the count and product arguments is to create bindings for those variables: the values of the arguments are never used. So let's do that explicitly with let, and I will bind them initially to an undefined object so it's clear the binding is never used (I have also renamed the arguments to the inner function so it is clear that these are different bindings):

(require racket/undefined)

(define (factorial-imp n)
  (let ([product undefined]
        [count undefined])
    (set! product 1)
    (set! count 1)
    (define (iter c p)
      (if (> c n)
          p
          (iter (add1 c) (* p c))))
    (iter count product)))

OK, well, now it is obvious that any expression of the form (let ([x <y>]) (set! x <z>) ...) can immediately be replaced by (let ([x <z>]) ...) so long as whatever <y> is has no side effects and terminates. That is the case here, so we can rewrite the above as follows:

(define (factorial-imp n)
  (let ([product 1]
        [count 1])
    (define (iter c p)
      (if (> c n)
          p
          (iter (add1 c) (* p c))))
    (iter count product)))

OK, so now we have something of the form (let ([x <y>]) (f x)): this can be trivially be replaced by (f <y>):

(define (factorial-imp n)
  (define (iter c p)
    (if (> c n)
        p
        (iter (add1 c) (* p c))))
  (iter 1 1))

And now it is quite clear that your implementation is not, in fact, imperative in any useful way. It does mutate bindings, but it does so only once, and never uses the original binding before the mutation. This is essentially something which compiler writers call 'static single assignment' I think: each variable is assigned once and not used before it is assigned to.


PS: 'splendidly mad' was not meant as an insult, I hope it was not taken as such, I enjoyed answering this!

答案 1 :(得分:1)

使用set!通过更改绑定引入副作用,但是在不使用传递的值的情况下将其从传递的值更改为1,之后您永远不会更改值,可能会将其视为11是传递给助手的常量,如下所示:

(define (factorial-imp n ignored-1 ignored-2)
  (define (iter count product)
    (if (> count n)
        product
        (iter (add1 count) (* product count))))
  (iter 1 1))

帮助程序通过递归更新它的countproduct,因此100%正常运行。

如果你要用命令式语言做同样的事情,那么你就可以在一个循环之外创建一个变量,你可以在循环的每一步更新,就像教授的实现一样。

在您的版本中,您修改了合同。用户需要传递两个不用于任何内容的参数。我通过称呼ignored-1ignored-2来说明这一点。