Code and data
Reading through SICP I have been thinking a lot about abstraction as well as the 'code as data and data as code' mindset.
A quick note for those not too familiar with lisp-syntax; when I say (operation arg1 arg2 arg3)
simply pretend I am saying operation(arg1, arg2, arg3)
.
Typically I think of procedures (code
) as the things that operate on data and data as the thing we give code to work on; that is that
the code is the do-er
and is active, where as data is passive.
This distinction starts to break down once we are able to pass around procedures as values.
Consider a simple object representing a bank balance (written in scheme)
;; constructor for an object which represents a bank account
;; supports "withdraw", "deposit" and "balance" operations
(define mk-account
(lambda (balance)
;; deposit into account
(define deposit
(lambda (amt)
(set! balance (+ balance amt))))
;; withdraw from account
(define withdraw
(lambda (amt)
(set! balance (- balance amt))))
;; query account balance
(define get-balance
(lambda ()
balance))
;; provide an object-like interface
;; takes an argument (`message`) which determines
;; which procedure we return to the caller
;; the caller then calls this procedure to perform
;; tasks.
(define dispatch
(lambda (op)
(if (equal? op "withdraw")
withdraw
(if (equal? op "deposit")
deposit
get-balance))))
dispatch))
This provided quite a simple interface, at this point we still have the typical code-data distinction.
;; construct new account with initial balance of 1000
(define my-account (mk-account 1000))
;; withdraw 200
((my-account "withdraw") 200)
;; deposit 75
((my-account "deposit") 75)
;; print balance => 875
(display ((my-account "balance"))) ;; => 875
Notice that at this point we still have the form of (procedure data...)
, the only difference is that we
have intermediary procedure calls ((procedure data...) data...)
We can of course abstract this further; and at this point the distinction begins to blur.
;; take an account and return procedure
;; for querying balance
(define get-balance
(lambda (acc)
(acc "balance")))
;; take an account and return procedure
;; for withdrawing from account
(define withdraw
(lambda (acc)
(acc "withdraw")))
;; take an account and return procedure
;; for depositing into account
(define deposit
(lambda (acc)
(acc "deposit")))
and the usage becomes
;; construct a new account
(define my-account (mk-account 1000))
;; perform a withdrawel and deposit
((withdraw my-account) 200)
((deposit my-account) 75)
;; print balance
(display ((get-balance my-account))) ;; => 875
notice that we now have (withdraw my-account)
; both withdraw
and my-account
are procedures, we now have
two equivalent ways to express the same idea
(my-account "withdraw") ;; (procedure data)
(withdraw my-account) ;; (procedure procedure)
But the withdraw
procedure is purely syntactic sugar for the first form, so we can also think of this as
(withdraw my-account) ;; (data procedure)
We still have the intermediary procedure call, we can of course trivially remove this.
(define get-balance
(lambda (acc)
((acc "balance"))))
(define withdraw
(lambda (acc amt)
((acc "withdraw") amt)))
(define deposit
(lambda (acc amt)
((acc "deposit") amt)))
and the usage becomes
(define my-account (mk-account 1000))
(withdraw my-account 200)
(deposit my-account 75)
(display (get-balance my-account)) ;; => 875
now we have procedure calls of the form (withdraw my-account 200)
which is (procedure procedure data)
or possibly (data procedure data)
.
I find it very interesting that once we start to deal with higher order functions the typical data-code disinction not only breaks down, but becomes very muddled; in a single procedure call we can freely mix the two.
[1] Structure and Interpretation of Computer Programs http://mitpress.mit.edu/sicp/
[2] All examples in this post are written in scheme and were tested in my toy interpreter https://github.com/mkfifo/plot