How to change values in Scheme but just using purely functional paradigm

Viewed 473

I'm trying to change values of some variables but I can not do it without leaving the functional paradigm, for example using set!. Is there any way it can be done?

Example code:

(lambda ()
      (let ((more a))
        (set! a b)
        (set! b (+ more b))

I want to change a taking the value of b and i want to change b taking the value (+ more b) but using purely functional paradigm, without set!.

4 Answers

You can't do this, but you can do something equivalent. Let's say I have some function f1 in which a and b are bound (let's say they are arguments as this is going to make things easier). And at some point I want to swap a and b. So I start with this:

(define f1
  (λ (a b)
    ... code that uses a and b ...
    (let ([tmp a])
      (set! a b)
      (set! b tmp))
    ... code that uses a and b, now swapped ...))

And this code is clearly not functional as it has assignment in it. But I can do this, inventing a new function, f2:

(define f1
  (λ (a b)
    ... code that uses a and b ...
    (f2 b a)))

(define f2
  (λ (a b)
    ... code that uses a and b, now swapped ...))

And this code is functional, and it does the same thing. And I can then just get rid of f2's name, because functions are first-class:

(define f1
  (λ (a b)
    ... code that uses a and b ...
    ((λ (a b)
       ... code that uses a and b, now swapped ...)
     b a))

(And obviously we'd write this as:

(define f1
  (λ (a b)
    ...
    (let ([b a] [a b])
      ...)))

which is the same thing.)

So this code now does exactly the same thing as the original code does, except it is purely functional (well: so long as the code in ellipsis is, which it probably is not, since the first chunk can really only do something by side-effect).

Now here's the clever bit: Scheme is required to be properly tail-recursive. What this means is that tail calls must be eliminated by implementations. Function call, in Scheme, is essentially GO TO passing arguments, as famously described in Debunking the 'Expensive Procedure Call' Myth, or, Procedure Call Implementations Considered Harmful, or, Lambda: The Ultimate GOTO, one of the famous 'Lambda the ultimate' papers. The function call to what was initially f2 and became an anonymous function is a tail call, and must therefore be eliminated. Any reasonable Scheme implementation will very likely turn this code into code which is the same as or possibly better than the naive code with assignment.


A note on the lambda-the-ultimate papers: the site which used to host copies of them them and to which there are still many links, including from Wikipedia has turned into spam: do not follow those links (the site had a name which included the words 'read' and 'scheme'). The best place to look for them now seems to be the AI Memos repository at MIT. It's pretty annoying that they have become so hard to find as they are absolutely foundational papers.

The moment you modify a variable you're stepping out of the functional paradigm. The procedures marked with a bang (!) at the end are procedural in nature, and you should avoid them if you intend to write purely functional code.

What you can do is call another function (possibly the same function, if writing a loop) passing as parameters the new values of the "variables". For example:

(define a 26)
(define b 16)

(define (print-new-values a b)
  ; the modified values exist
  ; only inside this procedure
  (printf "a is ~s~n" a)
  (printf "b is ~s~n" b))

(let ((more a))
  ; notice that the parameter a = b
  ; and that the parameter b = more + b
  ; we didn't reassign anything, instead
  ; the parameters got bound to new values  
  (print-new-values b (+ more b)))

=> a is 16
   b is 42

The above code has the exact same output of what you intended to write, but without using set!. For comparison:

(define a 26)
(define b 16)

(let ((more a))
  (set! a b)
  (set! b (+ more b))
  (printf "a is ~s~n" a)
  (printf "b is ~s~n" b))

=> a is 16
   b is 42

It's doable with shadowing. Eg.

(let ((a 10)) (b 20))
  (let ((b a) (a b))
    (list a b))) ; ==> (20 10)

However if you want to be able to have full flexibilith you do it with objects:

(define (fib-node a b)
  (cons a b))

(define (fib-b f)
  (cdr f))

(define (fib-value f)
  (car f))

(define (fib-next f)
  (let ((b (fib-b f)))
    (fib-node b (+ b (fib-value f)))))

(define fib-null (fib-node 0 1))

;; iterating Fibonacci numbers without setting anything
(let loop ((n 10) (cur fib-null) (acc '()))
  (if (zero? n)
      (reverse acc)
      (loop (- n 1) (fib-next cur) (cons (fib-value cur) acc))))
; ==> (0 1 1 2 3 5 8 13 21 34)

There are many patterns to do it, the most well known is the monad. But here is a way named continuation passing style:

(define set/a/b
  (lambda (a b return)
    ;; new/a <= b
    ;; new/b <= a+b
    (return b (+ a b))))

(define new/values
  (lambda (a b return)
    (set/a/b a b return)))

(new/values 10 20
  (lambda (new/a new/b)
    (display new/a)(newline)
    (display new/b)(newline)))
Related