-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathsrfi-123.html
164 lines (163 loc) · 17 KB
/
srfi-123.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="pandoc" />
<title>Generic accessor and modifier operators</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/srfi.css" type="text/css" />
<style type="text/css">code{white-space: pre;}</style>
</head>
<body>
<div id="header">
<h1 class="title">Generic accessor and modifier operators</h1>
</div>
<h2 id="author">Author</h2>
<p>Taylan Ulrich Bayırlı/Kammer, taylanbayirli at Google Mail</p>
<h2 id="status">Status</h2>
<p>This SRFI is currently in <em>final</em> status. Here is <a href="https://srfi.schemers.org/srfi-process.html">an explanation</a> of each status that a SRFI can hold. To provide input on this SRFI, please send email to <code><a href="mailto:srfi+minus+123+at+srfi+dotschemers+dot+org">srfi-123@<span class="antispam">nospam</span>srfi.schemers.org</a></code>. To subscribe to the list, follow <a href="https://srfi.schemers.org/srfi-list-subscribe.html">these instructions</a>. You can access previous messages via the mailing list <a href="https://srfi-email.schemers.org/srfi-123">archive</a>.</p>
<ul>
<li>Received: 2015-08-14</li>
<li>Draft #1 published: 2015-08-15</li>
<li>Draft #2 published: 2015-08-16</li>
<li>Draft #3 published: 2015-08-17</li>
<li>Draft #4 published: 2015-08-18</li>
<li>Draft #5 published: 2015-08-23 (code changes only)</li>
<li>Draft #6 published: 2015-08-24</li>
<li>Draft #7 published: 2015-08-26</li>
<li>Draft #8 published: 2015-09-05</li>
<li>Draft #9 published: 2015-09-07</li>
<li>Finalized: 2015-10-04</li>
<li>Revised to fix errata:
<ul>
<li>2021-04-08 (Fix <a href="#register-getter-with-setter-bang">description</a> of <code>register-getter-with-setter!</code>.)</li></ul></li>
</ul>
<h2 id="abstract">Abstract</h2>
<p>Lisp dialects including Scheme have traditionally lacked short, simple, generic syntax for accessing and modifying the fields of arbitrary "collection" objects. We fill this gap for Scheme by defining generalized accessors, and an associated SRFI-17 setter.</p>
<h2 id="rationale">Rationale</h2>
<p>In some types of code-bases, accessing and modifying fields of certain collection objects (such as vectors, hashtables, or records) are ubiquitous operations. Standard Scheme APIs contain verbose procedure names specialized for each data type, which may become very tedious to type, and clutter the code.</p>
<p>In contrast, most other languages offer very short and simple syntax for such operations, such as square bracket and dotted notation: <code>object[field]</code> and <code>object.field</code> for access; <code>object[field] = value</code> and <code>object.field = value</code> for modification.</p>
<p>To accommodate, we define a pair of generic accessor operators that work through type-based dynamic dispatch: <code>(ref object field)</code>, and <code>(ref* object field1 field2 ...)</code> for chained access.</p>
<pre><code>(ref #(a b c) 1) ;=> b
(ref* #(a (x y #u8(1 2 3)) c) 1 2 0) ;=> 1</code></pre>
<p>We define <code>~</code> as a synonym to <code>ref*</code>, and define a SRFI-17 setter for it.</p>
<pre><code>(define struct #(a (x y #u8(1 2 3)) c))
(set! (~ struct 1 2 0) 4)
struct ;=> #(a (x y #u8(4 2 3)) c)</code></pre>
<p>Plain <code>ref</code>, instead of allowing chaining, takes an optional "default" argument for objects such as hashtables.</p>
<pre><code>(define table (make-eqv-hashtable))
(ref table "foo" 'not-found) ;=> not-found
(set! (~ table "foo") "Foobar.")
(ref table "foo" 'not-found) ;=> "Foobar."</code></pre>
<p>Lack of a default argument is an error in this case. Since <code>ref*</code> cannot take default arguments for any fields it accesses, it is an error when a hashtable key in the chain is not found.</p>
<pre><code>(define table (make-eqv-hashtable))
(define lst (list 0 1 table 3))
(ref* lst 2 "foo" 'x) ;error while accessing "foo" from table</code></pre>
<p>We believe the overhead involved in the dynamic dispatch is negligible in most cases, and furthermore a programmer can always fall back to type-specific accessor and modifier procedures in performance-critical sections of code.</p>
<p>The operators are specified to work on bytevectors, R6RS hashtables, lists/pairs, strings, vectors, non-opaque record types, SRFI-4 vectors, and SRFI-111 boxes. (R6RS and SRFI-99 can produce opaque record types; SRFI-9 and R7RS cannot.) Some notes on specific types:</p>
<ul>
<li><p>For bytevectors, 8-bit unsigned integer operations are assumed. There is no obvious way to incorporate other bytevector operations into the generalized API, and a programmer is most likely to have single-byte operations in mind when using a generalized API on bytevectors.</p>
<pre><code>(define bv (bytevector 0 1 2 3))
(ref bv 2) ;=> 2
(set! (~ bv 2) 5)
(ref bv 2) ;=> 5</code></pre></li>
<li><p>However, some implementations provide SRFI-4 vectors by tagging bytevectors, such that SRFI-4 vectors are not disjoint types from bytevectors. In that case, the SRFI-4 type of the bytevector dictates the semantics.</p>
<pre><code>(define bv (s16vector 0 1 2 3))
(bytevector? bv) ;=> #t
(bytevector-u8-ref bv 2) ;=> (result depends on endianness)
(ref bv 2) ;=> 2</code></pre></li>
<li><p>When a pair is encountered, the field argument may be the symbols <code>car</code> or <code>cdr</code>, or an integer index indicating the pair should be viewed as the head of a list.</p>
<pre><code>(ref '(a b c . d) 'cdr) ;=> (b c . d)
(ref '(a b c . d) 2) ;=> c</code></pre></li>
<li><p>For records, the accepted values for the <code>field</code> parameter are symbols corresponding to the record type's field names. The overhead involved in looking up the correct accessor of modifier falls under the same rationale as other kinds of overhead involved with this SRFI.</p>
<pre><code>(define-record-type <foo> (make-foo a b) foo?
(a foo-a set-foo-a!)
(b foo-b))
(define foo (make-foo 0 1))
(ref foo 'a) ;=> 0
(set! (~ foo 'b) 2) ;error: No such assignable field of record.</code></pre></li>
<li><p>For boxes, the symbol <code>*</code> is used to indicate the one value field of the box. This is mainly useful for <code>ref*</code>:</p>
<pre><code>(define struct (list 0 (vector (box (cons 'a 'b)))))
(ref* struct 1 0 '* 'cdr)</code></pre></li>
</ul>
<p>Alists are difficult to support due to the lack of a reliable <code>alist?</code> predicate. (It's ambiguous in that every alist is also a list, and any list may coincidentally have the structure of an alist.) It was considered to support non-integer keyed alists as a special case, but this would lead to silent code breakage when a programmer forgot about the API inconsistency and exchanged a non-integer key for an integer key in existing code. It was also considered to drop list support in favor of alist support, but that idea discarded as well because the hypothetical <code>alist-set!</code> is an exceedingly rare operation. (Prepending an entry to the front, possibly hiding another entry with the same key, is more common.)</p>
<h2 id="integration-with-srfi-105">Integration with SRFI-105</h2>
<p>The <code>ref*</code> procedure is a good candidate for SRFI-105's <code>$bracket-apply$</code>. Indeed the sample implementation exports <code>$bracket-apply$</code> as a synonym to <code>ref*</code>. In code that already uses SRFI-105 heavily, a programmer may additionally define <code>:=</code> as a synonym to <code>set!</code>, and then use the following syntax: <code>{object[field] := value}</code>.</p>
<pre><code>#!curly-infix
(import (rename (only (scheme base) set!) (set! :=)))
(define vec (vector 0 1 2 3))
{vec[1] + vec[2]} ;=> 3
{vec[2] := 4}
{vec[1] + vec[2]} ;=> 5</code></pre>
<p>The square brackets accept a chain of fields, since they have the semantics of <code>ref*</code>: <code>{matrix[i j]}</code>.</p>
<h2 id="specification">Specification</h2>
<p>Within this section, whenever a situation is described as being an error, a Scheme implementation supporting error signaling should signal an error.</p>
<ul>
<li><code>(ref object field)</code> (procedure)</li>
<li><code>(ref object field default)</code></li>
</ul>
<p>Returns the value for <code>field</code> in <code>object</code>. It is an error if <code>object</code> has no field identified by <code>field</code>.</p>
<pre><code>(ref #(0 1 2) 3) ;error: vector-ref: Index out of bounds.</code></pre>
<p>If <code>object</code> is of a "sparse" type, meaning its fields can be "empty" or "unassigned" (e.g. a hashtable), and the requested field is empty, then the value of <code>default</code> is returned. It is an error if the <code>default</code> argument is not provided in this case.</p>
<pre><code>(ref hashtable unassigned-key 'default) ;=> default
(ref hashtable unassigned-key) ;error</code></pre>
<p>If <code>object</code> is not of a sparse type, then providing the <code>default</code> argument is an error.</p>
<pre><code>(ref '(0 1 2) 3 'default) ;error: list-ref: Too many arguments.</code></pre>
<p>Valid types for <code>object</code> are: bytevectors, hashtables, pairs, strings, vectors, non-opaque record types, SRFI-4 vectors, and SRFI-111 boxes. Only hashtables are a sparse type. Implementations are encouraged to expand this list of types with any further types they support.</p>
<p>Valid types for <code>field</code> depend on the type of <code>object</code>. For bytevectors, hashtables, strings, vectors, and SRFI-4 vectors, refer to their respective <code>*-ref</code> procedures. For pairs, the symbols <code>car</code> and <code>cdr</code> are accepted, as well as non-negative integers as with <code>list-ref</code>. For records, symbols that correspond with the record type's field names are allowed. For boxes, the symbol <code>*</code> is used to denote the one value field of the box.</p>
<p>A conforming implementation must be prepared for SRFI-4 vector types and bytevectors not being disjoint types, and treat SRFI-4 vectors suitably and not as regular bytevectors.</p>
<p>A conforming implementation must also be prepared for boxes being a non-opaque record type instead of a disjoint type, and treat them correctly despite that fact.</p>
<p>The <code>ref</code> procedure has an associated SRFI-17 setter, although the one of <code>ref*</code> is strictly more powerful.</p>
<pre><code>(define vec (vector 0 1 2))
(set! (ref vec 0) 3)
vec ;=> #(3 1 2)</code></pre>
<ul>
<li><code>(ref* object field field* ...)</code> (procedure)</li>
<li><code>(~ object field field* ...)</code></li>
</ul>
<p>The semantics of this procedure is as follows:</p>
<pre><code>(ref* object field) = (ref object field)
(ref* object field field+ ...) = (ref* (ref object field) field+ ...)</code></pre>
<p>It has an associated SRFI-17 setter, which does the expected thing:</p>
<pre><code>(set! (~ obj f1 f2 f3) value)</code></pre>
<p>changes the value that would be returned from <code>(~ obj f1 f2 f3)</code> to <code>value</code>. Note that this procedure can be accessed as <code>(setter ref*)</code> when needed:</p>
<pre><code>(define (store-item! field-chain value)
(apply (setter ref*) the-store (append field-chain (list value))))</code></pre>
<ul>
<li id="register-getter-with-setter-bang"><code>(register-getter-with-setter! type getter sparse?)</code> (procedure)</li>
</ul>
<p>Registers a new type/getter/setter triple for the dynamic dispatch. <code>Type</code> is a type predicate, <code>getter</code> is a procedure that has a setter associated with it, and <code>sparse?</code> is a Boolean indicating whether the type is a sparse type (see <code>ref</code> specification).</p>
<p>If <code>sparse?</code> is false, the getter will be called with
two arguments: the object whose field should be accessed, and an
object identifying the field to be accessed. If <code>sparse?</code>
is true, the getter will be called with a third argument, the default
value, which the getter must return in case the given object has no
value associated with the given field.</p>
<p>The setter will always be called with three arguments: the object that
is to be mutated, an object identifying which field of the object is to
be mutated, and the new value that is to be assigned to that field.</p>
<p><strong>Warning:</strong> This procedure is strictly meant for when defining a new disjoint type which isn't already handled by <code>ref</code>. In practice, this means it should only be used with newly defined opaque record types, or types defined with some implementation-specific method which, unlike <code>define-record-type</code>, doesn't automatically register a getter and setter for the type. If any two type predicates registered with the system both return true for any Scheme object, the behavior is undefined. (A custom getter or setter may, however, dispatch to different actions based on some property of the given object, based on the <code>field</code> argument, or based on anything else.)</p>
<p>It is conceivable that this method will become deprecated after a system has been invented which ties together the definition of a new opaque record type with the definitions of its getter and setter. This is considered outside the scope of this SRFI.</p>
<h2 id="considerations-when-using-as-a-library">Considerations when using as a library</h2>
<p>The intent of this SRFI is to encourage Scheme systems to extend their standard library in accordance with the above specification. On the meanwhile, the sample implementation can be used as a separate library, but certain considerations apply.</p>
<p>The <code>define-record-type</code> export of the library conflicts with the one in <code>(scheme base)</code>, so either has to be renamed, or more typically, the one from <code>(scheme base)</code> excluded.</p>
<p>Record types not defined with the <code>define-record-type</code> exported by this library won't work with <code>ref</code>, <code>ref*</code>, or their setters.</p>
<p>This problem does not apply to implementations supporting inspection of records and record types.</p>
<h2 id="implementation">Implementation</h2>
<p>A sample implementation as a library is found in the version control repository of this SRFI.</p>
<p>It might be desirable for Scheme systems to offer a more efficient <code>type-of</code> procedure than the one used in this implementation, which in the worst case consumes linear time with regard to the number of types (including every record type) within the system, albeit with a very small constant factor: one call to each type predicate.</p>
<h2 id="acknowledgments">Acknowledgments</h2>
<p>Thanks to Jorgen Schäfer for inspiring me to write this SRFI and making the initial suggestion for the <code>ref</code> procedure and ternary <code>set!</code> syntax, as well as providing continuous input.</p>
<p>The <code>ref*</code> procedure with its <code>~</code> synonym and SRFI-17 setter (which replaced the initially considered ternary <code>set!</code> syntax) seems to have first appeared in Gauche. Thanks to Shiro Kawai and Issac Trotts: <a href="http://blog.practical-scheme.net/gauche/20100428-shorter-names" class="uri">http://blog.practical-scheme.net/gauche/20100428-shorter-names</a></p>
<p>Thanks to Evan Hanson for the idea of using a throw-away <code>define</code> in the expansion of <code>define-record-type</code> so as not to disturb a sequence of internal definitions.</p>
<p>Thanks to Vincent St-Amour, Eli Barzilay, and others in the Racket IRC channel for raising my awareness against action-at-a-distance bugs that might result from abuse of the imperative <code>register-getter-with-setter!</code>.</p>
<p>Thanks also to everyone else on the discussion mailing list for their input.</p>
<h2 id="copyright-and-license">Copyright and license</h2>
<p>Copyright (C) Taylan Ulrich Bayırlı/Kammer (2015). All Rights Reserved.</p>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
<hr>
<address>Editor: <a href="mailto:srfi-editors+at+srfi+dot+schemers+dot+org">Arthur A. Gleckler</a></address>
</body>
</html>