records: Move 'make-syntactic-constructor' to the top level.
[jackhill/guix/guix.git] / guix / records.scm
1 ;;; GNU Guix --- Functional package management for GNU
2 ;;; Copyright © 2012, 2013, 2014, 2015 Ludovic Courtès <ludo@gnu.org>
3 ;;;
4 ;;; This file is part of GNU Guix.
5 ;;;
6 ;;; GNU Guix is free software; you can redistribute it and/or modify it
7 ;;; under the terms of the GNU General Public License as published by
8 ;;; the Free Software Foundation; either version 3 of the License, or (at
9 ;;; your option) any later version.
10 ;;;
11 ;;; GNU Guix is distributed in the hope that it will be useful, but
12 ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
13 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 ;;; GNU General Public License for more details.
15 ;;;
16 ;;; You should have received a copy of the GNU General Public License
17 ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
18
19 (define-module (guix records)
20 #:use-module (srfi srfi-1)
21 #:use-module (srfi srfi-9)
22 #:use-module (srfi srfi-26)
23 #:use-module (ice-9 match)
24 #:use-module (ice-9 regex)
25 #:use-module (ice-9 rdelim)
26 #:export (define-record-type*
27 alist->record
28 object->fields
29 recutils->alist))
30
31 ;;; Commentary:
32 ;;;
33 ;;; Utilities for dealing with Scheme records.
34 ;;;
35 ;;; Code:
36
37 (define-syntax record-error
38 (syntax-rules ()
39 "Report a syntactic error in use of CONSTRUCTOR."
40 ((_ constructor form fmt args ...)
41 (syntax-violation constructor
42 (format #f fmt args ...)
43 form))))
44
45 (define* (make-syntactic-constructor type name ctor fields
46 #:key thunked defaults)
47 "Make the syntactic constructor NAME for TYPE, that calls CTOR, and expects
48 all of FIELDS to be initialized. DEFAULTS is the list of FIELD/DEFAULT-VALUE
49 tuples, and THUNKED is the list of identifiers of thunked fields."
50 (with-syntax ((type type)
51 (name name)
52 (ctor ctor)
53 (expected fields)
54 (defaults defaults))
55 #`(define-syntax name
56 (lambda (s)
57 (define (record-inheritance orig-record field+value)
58 ;; Produce code that returns a record identical to ORIG-RECORD,
59 ;; except that values for the FIELD+VALUE alist prevail.
60 (define (field-inherited-value f)
61 (and=> (find (lambda (x)
62 (eq? f (car (syntax->datum x))))
63 field+value)
64 car))
65
66 ;; Make sure there are no unknown field names.
67 (let* ((fields (map (compose car syntax->datum) field+value))
68 (unexpected (lset-difference eq? fields 'expected)))
69 (when (pair? unexpected)
70 (record-error 'name s "extraneous field initializers ~a"
71 unexpected)))
72
73 #`(make-struct type 0
74 #,@(map (lambda (field index)
75 (or (field-inherited-value field)
76 #`(struct-ref #,orig-record
77 #,index)))
78 'expected
79 (iota (length 'expected)))))
80
81 (define (thunked-field? f)
82 (memq (syntax->datum f) '#,thunked))
83
84 (define (field-bindings field+value)
85 ;; Return field to value bindings, for use in 'let*' below.
86 (map (lambda (field+value)
87 (syntax-case field+value ()
88 ((field value)
89 #`(field
90 #,(if (thunked-field? #'field)
91 #'(lambda () value)
92 #'value)))))
93 field+value))
94
95 (syntax-case s (inherit #,@fields)
96 ((_ (inherit orig-record) (field value) (... ...))
97 #`(let* #,(field-bindings #'((field value) (... ...)))
98 #,(record-inheritance #'orig-record
99 #'((field value) (... ...)))))
100 ((_ (field value) (... ...))
101 (let ((fields (map syntax->datum #'(field (... ...))))
102 (dflt (map (match-lambda
103 ((f v)
104 (list (syntax->datum f) v)))
105 #'defaults)))
106
107 (define (field-value f)
108 (or (and=> (find (lambda (x)
109 (eq? f (car (syntax->datum x))))
110 #'((field value) (... ...)))
111 car)
112 (let ((value
113 (car (assoc-ref dflt (syntax->datum f)))))
114 (if (thunked-field? f)
115 #`(lambda () #,value)
116 value))))
117
118 (let ((fields (append fields (map car dflt))))
119 (cond ((lset= eq? fields 'expected)
120 #`(let* #,(field-bindings
121 #'((field value) (... ...)))
122 (ctor #,@(map field-value 'expected))))
123 ((pair? (lset-difference eq? fields 'expected))
124 (record-error 'name s
125 "extraneous field initializers ~a"
126 (lset-difference eq? fields
127 'expected)))
128 (else
129 (record-error 'name s
130 "missing field initializers ~a"
131 (lset-difference eq? 'expected
132 fields))))))))))))
133
134 (define-syntax define-record-type*
135 (lambda (s)
136 "Define the given record type such that an additional \"syntactic
137 constructor\" is defined, which allows instances to be constructed with named
138 field initializers, à la SRFI-35, as well as default values. An example use
139 may look like this:
140
141 (define-record-type* <thing> thing make-thing
142 thing?
143 (name thing-name (default \"chbouib\"))
144 (port thing-port
145 (default (current-output-port)) (thunked)))
146
147 This example defines a macro 'thing' that can be used to instantiate records
148 of this type:
149
150 (thing
151 (name \"foo\")
152 (port (current-error-port)))
153
154 The value of 'name' or 'port' could as well be omitted, in which case the
155 default value specified in the 'define-record-type*' form is used:
156
157 (thing)
158
159 The 'port' field is \"thunked\", meaning that calls like '(thing-port x)' will
160 actually compute the field's value in the current dynamic extent, which is
161 useful when referring to fluids in a field's value.
162
163 It is possible to copy an object 'x' created with 'thing' like this:
164
165 (thing (inherit x) (name \"bar\"))
166
167 This expression returns a new object equal to 'x' except for its 'name'
168 field."
169
170 (define (field-default-value s)
171 (syntax-case s (default)
172 ((field (default val) _ ...)
173 (list #'field #'val))
174 ((field _ options ...)
175 (field-default-value #'(field options ...)))
176 (_ #f)))
177
178 (define (thunked-field? s)
179 ;; Return the field name if the field defined by S is thunked.
180 (syntax-case s (thunked)
181 ((field (thunked) _ ...)
182 #'field)
183 ((field _ options ...)
184 (thunked-field? #'(field options ...)))
185 (_ #f)))
186
187 (define (thunked-field-accessor-name field)
188 ;; Return the name (an unhygienic syntax object) of the "real"
189 ;; getter for field, which is assumed to be a thunked field.
190 (syntax-case field ()
191 ((field get options ...)
192 (let* ((getter (syntax->datum #'get))
193 (real-getter (symbol-append '% getter '-real)))
194 (datum->syntax #'get real-getter)))))
195
196 (define (field-spec->srfi-9 field)
197 ;; Convert a field spec of our style to a SRFI-9 field spec of the
198 ;; form (field get).
199 (syntax-case field ()
200 ((name get options ...)
201 #`(name
202 #,(if (thunked-field? field)
203 (thunked-field-accessor-name field)
204 #'get)))))
205
206 (define (thunked-field-accessor-definition field)
207 ;; Return the real accessor for FIELD, which is assumed to be a
208 ;; thunked field.
209 (syntax-case field ()
210 ((name get _ ...)
211 (with-syntax ((real-get (thunked-field-accessor-name field)))
212 #'(define-inlinable (get x)
213 ;; The real value of that field is a thunk, so call it.
214 ((real-get x)))))))
215
216 (syntax-case s ()
217 ((_ type syntactic-ctor ctor pred
218 (field get options ...) ...)
219 (let* ((field-spec #'((field get options ...) ...))
220 (thunked (filter-map thunked-field? field-spec))
221 (defaults (filter-map field-default-value
222 #'((field options ...) ...))))
223 (with-syntax (((field-spec* ...)
224 (map field-spec->srfi-9 field-spec))
225 ((thunked-field-accessor ...)
226 (filter-map (lambda (field)
227 (and (thunked-field? field)
228 (thunked-field-accessor-definition
229 field)))
230 field-spec)))
231 #`(begin
232 (define-record-type type
233 (ctor field ...)
234 pred
235 field-spec* ...)
236 (begin thunked-field-accessor ...)
237 #,(make-syntactic-constructor #'type #'syntactic-ctor #'ctor
238 #'(field ...)
239 #:thunked thunked
240 #:defaults defaults))))))))
241
242 (define* (alist->record alist make keys
243 #:optional (multiple-value-keys '()))
244 "Apply MAKE to the values associated with KEYS in ALIST. Items in KEYS that
245 are also in MULTIPLE-VALUE-KEYS are considered to occur possibly multiple
246 times in ALIST, and thus their value is a list."
247 (let ((args (map (lambda (key)
248 (if (member key multiple-value-keys)
249 (filter-map (match-lambda
250 ((k . v)
251 (and (equal? k key) v)))
252 alist)
253 (assoc-ref alist key)))
254 keys)))
255 (apply make args)))
256
257 (define (object->fields object fields port)
258 "Write OBJECT (typically a record) as a series of recutils-style fields to
259 PORT, according to FIELDS. FIELDS must be a list of field name/getter pairs."
260 (let loop ((fields fields))
261 (match fields
262 (()
263 object)
264 (((field . get) rest ...)
265 (format port "~a: ~a~%" field (get object))
266 (loop rest)))))
267
268 (define %recutils-field-charset
269 ;; Valid characters starting a recutils field.
270 ;; info "(recutils) Fields"
271 (char-set-union char-set:upper-case
272 char-set:lower-case
273 (char-set #\%)))
274
275 (define (recutils->alist port)
276 "Read a recutils-style record from PORT and return it as a list of key/value
277 pairs. Stop upon an empty line (after consuming it) or EOF."
278 (let loop ((line (read-line port))
279 (result '()))
280 (cond ((eof-object? line)
281 (reverse result))
282 ((string-null? line)
283 (if (null? result)
284 (loop (read-line port) result) ; leading space: ignore it
285 (reverse result))) ; end-of-record marker
286 (else
287 ;; Now check the first character of LINE, since that's what the
288 ;; recutils manual says is enough.
289 (let ((first (string-ref line 0)))
290 (cond
291 ((char-set-contains? %recutils-field-charset first)
292 (let* ((colon (string-index line #\:))
293 (field (string-take line colon))
294 (value (string-trim (string-drop line (+ 1 colon)))))
295 (loop (read-line port)
296 (alist-cons field value result))))
297 ((eqv? first #\#) ;info "(recutils) Comments"
298 (loop (read-line port) result))
299 ((eqv? first #\+) ;info "(recutils) Fields"
300 (let ((new-line (if (string-prefix? "+ " line)
301 (string-drop line 2)
302 (string-drop line 1))))
303 (match result
304 (((field . value) rest ...)
305 (loop (read-line port)
306 `((,field . ,(string-append value "\n" new-line))
307 ,@rest))))))
308 (else
309 (error "unmatched line" line))))))))
310
311 ;;; records.scm ends here