git @ Cat's Eye Technologies The-Dossier / 23c3c05
Import Information Hiding in Scheme into this repo. Chris Pressey 1 year, 5 months ago
5 changed file(s) with 256 addition(s) and 2 deletion(s). Raw diff Collapse all Expand all
9696
9797 ### Information Hiding in Scheme
9898
99 * link: [Information Hiding in Scheme](https://github.com/cpressey/Information-Hiding-in-Scheme#readme)
99 * link: [Information Hiding in Scheme](article/Information-Hiding-in-Scheme/README.md)
100100 * publish-date: May 2021
101 * external: true
102101
103102 ### Destructorizers
104103
0 Information Hiding in Scheme
1 ============================
2
3 Say you wanted to implement an [abstract data type (Wikipedia)][]
4 in [R5RS Scheme][]. How would you do it?
5
6 _Could_ you do it? On first blush, it might appear that you can't.
7 There is no module system, and the data structuring facilities that
8 Scheme provides are really primitive. If you represent your data type
9 as a list, any client code could just take it apart with `car` and `cdr`,
10 and, worse, could `cons` together any other thing and pass that off
11 as an instance of your data type.
12
13 But there is a way. More than one, in fact. I'd like to describe some
14 of these ways in this document, starting with one that is I think is
15 easier to grasp, followed by one that is closer to the usual theoretical
16 presentation.
17
18 ### 1. Using Secret Tokens
19
20 The more intuitive approach relies on two properties of Scheme:
21
22 * Scheme _does_ have an opaque data structure: the function value.
23 * `(eq? (list 1) (list 1))` is `#f`, that is, each cons-cell can
24 be distinguished from every other cons-cell.
25
26 (The former would not be true if one could examine the definition of a
27 function value. The latter would not be true if Scheme implemented
28 hash-consing.)
29
30 The basic idea is straightforward. We define a function which we call
31 a _module_. When called, the module generates a few values internally.
32 One of these values is a unique cons-cell which we'll call the
33 _secret token_ of the module. Other values are functions, which we'll
34 call _operations_.
35
36 Each operation requires a token to work, and only performs its intended
37 function when this token matches the secret token defined by the module.
38
39 There are some restrictions on the value that the module returns to its
40 caller. The value may return the operations defined by the module
41 (in fact the value may simply be a list of all these operations), but
42 the value must not contain the secret token.
43
44 Likewise, the values returned by the operations must not contain
45 the secret token.
46
47 (These values may be functions which have _closed over_ the secret
48 token. But they should not _be_ the secret token nor _return_ it.)
49
50 In this way, clients of the module may use the operations of the module
51 without having to know, nor being able to alter or counterfeit, the
52 concrete data format that the operations work on.
53
54 #### `seal` and `open`
55
56 Because every operation follows the same pattern — pass the secret
57 token to a given opaque object to obtain the internal representation,
58 examine or modify the internal representation, then possibly create
59 a new opaque object — it is useful to write a pair of helper functions to
60 "open" and "seal" these objects using the module's secret token, and
61 then write all operations using these helpers.
62
63 `seal` takes some data, and returns a function which does the following:
64 it takes a token, and if that token matches the secret token, it returns
65 the data, otherwise it returns an error value.
66
67 `open` takes an opaque object, and calls it using the secret token
68 returning the data. (The `open` helper "knows" the secret token
69 because they are both defined inside the module; the definition
70 of the function called `open` closes over the secret token value.)
71
72 #### Example Code
73
74 See the file [`information-hiding.scm`](information-hiding.scm) for
75 an example of using this technique to implement a stack ADT.
76
77 ### 2. Using Closures Only
78
79 Since function values in Scheme are an opaque data type, the secret
80 tokens aren't actually necessary, but things do get a little more
81 complicated.
82
83 Actually, if you're willing for instances of your abstract data type
84 to be mutable, it's only a little more complicated. Your module
85 function can simply generate a mutable value internally, and all of
86 the operation functions can close over it. This way, they all have
87 it in scope, and can read it and modify it as appropriate, but their
88 callers can never see it. No secret token is needed to prevent this.
89
90 In this setup, the module function can return, not a list of
91 operations, but a single function; and this single function can take,
92 as its first argument, a symbol which indicates what operation to
93 perform. This resembles object-oriented method dispatch, and can
94 be used to implement object-oriented features such as inheritance.
95
96 See the file [`mutable-adt.scm`](mutable-adt.scm) for
97 an example of using this technique to implement a mutable stack ADT.
98
99 It's if you want instances of your abstract data type to be
100 [immutable data (Wikipedia)][] where it begins to get trickier to
101 think about, and this is because every operation of the ADT that
102 modifies the data has to return a new, immutable instance of the
103 ADT, and by association, all of its operations.
104
105 We can accomplish this by doing two things: sticking with the
106 "method dispatch" approach of having all operations implemented
107 by a single function that takes a symbol to choose the operation,
108 and defining this single function with `letrec` so that it can,
109 essentially, return a new version of itself when it has to.
110
111 See the file [`immutable-adt.scm`](immutable-adt.scm) for
112 an example of using this technique to implement a stack ADT.
113
114 [abstract data type (Wikipedia)]: https://en.wikipedia.org/wiki/Abstract_data_type
115 [immutable data (Wikipedia)]: https://en.wikipedia.org/wiki/Immutable_object
116 [R5RS Scheme]: https://schemers.org/Documents/Standards/R5RS/
0 ; Another technique to accomplish information hiding in R5RS Scheme.
1 ; Chris Pressey, Jan 2023.
2
3 (define new-stack
4 (letrec ( (make-stack (lambda (items)
5 (lambda (op args)
6 (cond
7 ((equal? op 'push)
8 (let* ( (item (car args))
9 (new-items (cons item items)) )
10 (make-stack new-items)))
11 ((equal? op 'top)
12 (car items))
13 ((equal? op 'popped)
14 (make-stack (cdr items)))
15 ))))
16 )
17 (lambda () (make-stack '())))
18 )
19
20 ;
21 ; A transcript of some sample usage
22 ;
23 ; #;1> (define s (new-stack))
24 ; #;2> (define t (s 'push '(4)))
25 ; #;3> (define u (t 'push '(5)))
26 ; #;4> (u 'top '())
27 ; 5
28 ; #;5> ((u 'popped '()) 'top '())
29 ; 4
0 ; A simple technique to accomplish information hiding in R5RS Scheme.
1 ; Chris Pressey, May 2021.
2
3 (define stack-module (lambda ()
4 (let* ((secret-token (list 1))
5 (seal (lambda (data)
6 (lambda (token)
7 (if (eq? token secret-token)
8 data
9 'an-error-occurred))))
10 (open (lambda (opaque-object)
11 (opaque-object secret-token)))
12
13 (new-stack (lambda ()
14 (seal '())))
15 (is-empty? (lambda (stack)
16 (equal? (open stack) '())))
17 (push (lambda (stack item)
18 (let* ((data (open stack))
19 (new-data (cons item data))
20 (new-stack (seal new-data)))
21 new-stack)))
22 (top (lambda (stack)
23 (car (open stack))))
24 (popped (lambda (stack)
25 (let* ((data (open stack))
26 (new-data (cdr data))
27 (new-stack (seal new-data)))
28 new-stack))))
29 (list new-stack is-empty? push top popped))))
30
31 (define skm (stack-module))
32 (define new-stack (car skm))
33 (define is-empty? (cadr skm))
34 (define push (caddr skm))
35 (define top (cadddr skm))
36 (define popped (car (cddddr skm)))
37
38 ;
39 ; A transcript of some sample usage that shows that it implements a stack:
40 ;
41 ; #;1> (define s (new-stack))
42 ; #;2> (define t (push s 4))
43 ; #;3> (define u (push t 5))
44 ; #;4> (top u)
45 ; 5
46 ; #;5> (top (popped u))
47 ; 4
48 ; #;6> (is-empty? s)
49 ; #t
50 ; #;7> (is-empty? (popped t))
51 ; #t
52 ; #;8> (popped s)
53 ;
54 ; Error: (cdr) bad argument type: ()
55 ;
56
57 ;
58 ; And a transcript that shows that it hides the representation:
59 ;
60 ; #;8> s
61 ; #<procedure (? token)>
62 ; #;9> t
63 ; #<procedure (? token)>
64 ; #;10> top
65 ; #<procedure (top stack)>
66 ; #;11> (s 1)
67 ; an-error-occurred
68 ; #;12> (s (list 1))
69 ; an-error-occurred
70 ; #;13> (is-empty? (list 1))
71 ;
72 ; Error: call of non-procedure: (1)
73 ;
0 ; Another technique to accomplish information hiding in R5RS Scheme,
1 ; this one encapsulating a mutable ADT with an OO-like dispatch.
2 ; Chris Pressey, Jan 2023.
3
4 (define new-stack
5 (lambda ()
6 (let* ((stack '()))
7 (lambda (method args)
8 (cond
9 ((equal? method 'push)
10 (begin
11 (set! stack (cons (car args) stack))
12 #t))
13 ((equal? method 'top)
14 (car stack))
15 ((equal? method 'pop)
16 (begin
17 (set! stack (cdr stack))
18 #t)))))))
19
20 ;
21 ; A transcript of some sample usage
22 ;
23 ; #;1> (define s (new-stack))
24 ; #;2> (s 'push '(4))
25 ; #t
26 ; #;3> (s 'push '(5))
27 ; #t
28 ; #;4> (s 'top '())
29 ; 5
30 ; #;5> (s 'pop '())
31 ; #t
32 ; #;6> (s 'top '())
33 ; 4