Lecture 20 : Programs with Memory [MOVE to advanced language level] Imagine that we are managing a collection of bank accounts, where each bank account has an account number and a balance. ;; An account is (make-account number number) (define-struct account (acctnum balance)) ;; A list-of-account is ;; - empty, or ;; - (cons account list-of-account) Let's create a list of accounts: (define accounts (list (make-account 1 500) (make-account 2 1000) (make-account 3 10))) We can write a function get-balance that takes an account number and a list of accounts and returns the balance in that account: ;; get-balance : number list-of-account -> number ;; consume account number and list of accounts and return balance in account (define (get-balance acctnum aloa) (cond [(empty? aloa) (error "No such account")] [(cons? aloa) (cond [(= acctnum (account-acctnum (first aloa))) (account-balance (first aloa))] [else (get-balance acctnum (rest aloa))])])) > (get-balance 3 accounts) 10 [NOTE the error in the empty case. If Scheme ever hits the empty case, it will stop the program, printing out the error message. We use it here because we expect the account num to be in the list of accounts. You do not need to test error cases.] We can also write a function deposit that takes an account number, an amount and a list of accounts and produces a list of accounts that increases the balance by the given amount. ;; deposit : number number list-of-account -> list-of-account ;; consumes account number, amount of money, and list of accounts and ;; produces list of accounts with given amount added to named account (define (deposit acctnum amt aloa) (cond [(empty? aloa) (error "No such account")] [(cons? aloa) (cond [(= acctnum (account-acctnum (first aloa))) (cons (make-account (account-acctnum (first aloa)) (+ amt (account-balance (first aloa)))) (rest aloa))] [else (cons (first aloa) (deposit acctnum amt (rest aloa)))])])) ;; note: error will stop your entire program from running. You can ;; use it to write programs, but don't put error test cases in because ;; that will prevent all remaining test cases from running. > (deposit 3 200 accounts) [returns accounts with 210 in account 3] Note that if I run get-balance after running deposit, the deposit isn't showing up: > (get-balance 3 accounts) 10 In the past, we've said that to see the new balance, we had to use deposit and get-balance together: > (get-balance 3 (deposit 3 200 accounts)) 210 But this is unsatisfying -- shouldn't we be able to deposit something and have the program _remember_ the deposit, rather than have to chain together functions like this all the time? Yes, and that's what we're going to do today. -------------------------------------------------- Assignment: How To Make Scheme Remember A Result Over Time Accounts are an example of something whose value we expect to change over time (as people make deposits, withdrawals, etc). To update accounts as we deposit or withraw money, we need to tell Scheme to "remember" the result of the change. To do this, we use an operator called set!. set! changes the value of a defined variable. For example, if we do: > (get-balance 3 accounts) 10 > (set! accounts (deposit 3 200 accounts)) > (get-balance 3 accounts) 210 Here, set! says "change accounts to contain the result of doing the deposit". Notice that the same call to get-balance now returns different answers at different times. This is what makes set! interesting and tricky -- programs can return different values for the same inputs (based on what information they are "remembering"). Up until now, when we have given names to data using define, we have called them constants or examples. Names that we intend to change over time are called VARIABLES. We still write them with define, but we will add a comment to them indicating what they are remembering: ;; accounts : list-of-account ;; Remembers the current contents of all the bank accounts (define accounts (list (make-account 1 500) (make-account 2 1000) (make-account 3 10))) Note that there is no -> in the first line because accounts is not a function. Here, the first line says "accounts must always be a list-of-account". This reminds us that when we use set!, we shouldn't change accounts to be a number or a string. We will expect that it always names a list-of-account. ------------------------------------------------- Our banking program would be even nicer if deposit did the set! automatically. We can do this in a function as follows: ;; deposit-funds : number number -> void ;; consumes account number and amount and adds amount to balance in given account ;; EFFECT: changes value of accounts (define (deposit-funds acctnum amt) (set! accounts (deposit acctnum amt accounts))) > (get-balance 3 accounts) 10 > (deposit-funds 3 200 accounts) > (get-balance 3 accounts) 210 Note EFFECT statement -- new part of documentation Note void -- what set! produces (Scheme for "nothing") Note also that deposit-funds does not take accounts as an input -- it uses the accounts name defined outside the function. Why is that? Imagine instead that we had written deposit-funds as: ;; deposit-funds2 : number number list-of-account -> void ;; consumes account number and amount and adds amount to balance in given account ;; EFFECT: changes value of accounts (define (deposit-funds2 acctnum amt aloa) (set! aloa (deposit acctnum amt aloa))) > (get-balance 3 accounts) 10 > (deposit-funds2 3 200 accounts) > (get-balance 3 accounts) 10 What went wrong? Remember that in Scheme an input name is only visible until the end of the function? The same holds for remembering information. If you want to remember something that happens inside a function, the variable must be defined OUTSIDE the function. The original deposit-funds met that criterion. ----------------------------------------------- Testing Programs with Variables and Memory Once programs have memory, we have to adjust how we test them. Why? Because the same call can now produce different answers at different times, and we want to make sure that we are remembering what we want to (without changing data that should have stayed the same). In particular, the _sequence_ of tests we run is now as important as the _individual_ tests that you have written until now. Here's a good sequence of tests for the banking system: 1. (get-balance 2 accounts) 2. (get-balance 3 accounts) 3. (deposit-funds 3 200 accounts) 4. (get-balance 3 accounts) 5. (get-balance 2 accounts) Note that these tests - check that deposit-funds adds money to an account (test 3) - check that deposit-funds left other accounts alone (tests 1 and 5) Note that the latter required us to have at least 2 accounts in our list (one to change and one to leave alone).