records: Separate default-value handling.
[jackhill/guix/guix.git] / guix / records.scm
CommitLineData
c0cd1b3e 1;;; GNU Guix --- Functional package management for GNU
fb519bd8 2;;; Copyright © 2012, 2013, 2014, 2015 Ludovic Courtès <ludo@gnu.org>
c0cd1b3e
LC
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)
fdc1bf65
LC
24 #:use-module (ice-9 regex)
25 #:use-module (ice-9 rdelim)
c0cd1b3e
LC
26 #:export (define-record-type*
27 alist->record
fdc1bf65
LC
28 object->fields
29 recutils->alist))
c0cd1b3e
LC
30
31;;; Commentary:
32;;;
33;;; Utilities for dealing with Scheme records.
34;;;
35;;; Code:
36
b1353e7a
LC
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
39fc041a
LC
45(define-syntax make-syntactic-constructor
46 (syntax-rules ()
47 "Make the syntactic constructor NAME for TYPE, that calls CTOR, and
48expects all of EXPECTED fields to be initialized. DEFAULTS is the list of
49FIELD/DEFAULT-VALUE tuples, THUNKED is the list of identifiers of thunked
50fields, and DELAYED is the list of identifiers of delayed fields."
51 ((_ type name ctor (expected ...)
52 #:thunked thunked
53 #:delayed delayed
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 (delayed-field? f)
85 (memq (syntax->datum f) 'delayed))
86
87 (define (wrap-field-value f value)
88 (cond ((thunked-field? f)
89 #`(lambda () #,value))
90 ((delayed-field? f)
91 #`(delay #,value))
92 (else value)))
93
b9c86473
LC
94 (define default-values
95 ;; List of symbol/value tuples.
96 (map (match-lambda
97 ((f v)
98 (list (syntax->datum f) v)))
99 #'defaults))
100
101 (define (field-default-value f)
102 (car (assoc-ref default-values (syntax->datum f))))
103
39fc041a
LC
104 (define (field-bindings field+value)
105 ;; Return field to value bindings, for use in 'let*' below.
106 (map (lambda (field+value)
107 (syntax-case field+value ()
108 ((field value)
109 #`(field
110 #,(wrap-field-value #'field #'value)))))
111 field+value))
112
113 (syntax-case s (inherit expected ...)
114 ((_ (inherit orig-record) (field value) (... ...))
115 #`(let* #,(field-bindings #'((field value) (... ...)))
116 #,(record-inheritance #'orig-record
117 #'((field value) (... ...)))))
118 ((_ (field value) (... ...))
b9c86473 119 (let ((fields (map syntax->datum #'(field (... ...)))))
39fc041a
LC
120 (define (field-value f)
121 (or (and=> (find (lambda (x)
122 (eq? f (car (syntax->datum x))))
123 #'((field value) (... ...)))
124 car)
b9c86473 125 (wrap-field-value f (field-default-value f))))
39fc041a 126
b9c86473 127 (let ((fields (append fields (map car default-values))))
39fc041a
LC
128 (cond ((lset= eq? fields '(expected ...))
129 #`(let* #,(field-bindings
130 #'((field value) (... ...)))
131 (ctor #,@(map field-value '(expected ...)))))
132 ((pair? (lset-difference eq? fields
133 '(expected ...)))
134 (record-error 'name s
135 "extraneous field initializers ~a"
136 (lset-difference eq? fields
137 '(expected ...))))
138 (else
139 (record-error 'name s
140 "missing field initializers ~a"
141 (lset-difference eq?
142 '(expected ...)
143 fields)))))))))))))
cf4efb39 144
c0cd1b3e
LC
145(define-syntax define-record-type*
146 (lambda (s)
147 "Define the given record type such that an additional \"syntactic
148constructor\" is defined, which allows instances to be constructed with named
e2540884
LC
149field initializers, à la SRFI-35, as well as default values. An example use
150may look like this:
151
152 (define-record-type* <thing> thing make-thing
153 thing?
154 (name thing-name (default \"chbouib\"))
155 (port thing-port
156 (default (current-output-port)) (thunked)))
157
158This example defines a macro 'thing' that can be used to instantiate records
159of this type:
160
161 (thing
162 (name \"foo\")
163 (port (current-error-port)))
164
165The value of 'name' or 'port' could as well be omitted, in which case the
166default value specified in the 'define-record-type*' form is used:
167
168 (thing)
169
170The 'port' field is \"thunked\", meaning that calls like '(thing-port x)' will
171actually compute the field's value in the current dynamic extent, which is
172useful when referring to fluids in a field's value.
173
310b32a2
LC
174A field can also be marked as \"delayed\" instead of \"thunked\", in which
175case its value is effectively wrapped in a (delay …) form.
176
e2540884
LC
177It is possible to copy an object 'x' created with 'thing' like this:
178
179 (thing (inherit x) (name \"bar\"))
180
181This expression returns a new object equal to 'x' except for its 'name'
182field."
183
c0cd1b3e
LC
184 (define (field-default-value s)
185 (syntax-case s (default)
186 ((field (default val) _ ...)
187 (list #'field #'val))
188 ((field _ options ...)
189 (field-default-value #'(field options ...)))
190 (_ #f)))
191
310b32a2
LC
192 (define (delayed-field? s)
193 ;; Return the field name if the field defined by S is delayed.
194 (syntax-case s (delayed)
195 ((field (delayed) _ ...)
196 #'field)
197 ((field _ options ...)
198 (delayed-field? #'(field options ...)))
199 (_ #f)))
200
c0cd1b3e
LC
201 (define (thunked-field? s)
202 ;; Return the field name if the field defined by S is thunked.
203 (syntax-case s (thunked)
204 ((field (thunked) _ ...)
205 #'field)
206 ((field _ options ...)
207 (thunked-field? #'(field options ...)))
208 (_ #f)))
209
310b32a2
LC
210 (define (wrapped-field? s)
211 (or (thunked-field? s) (delayed-field? s)))
212
213 (define (wrapped-field-accessor-name field)
c0cd1b3e 214 ;; Return the name (an unhygienic syntax object) of the "real"
310b32a2 215 ;; getter for field, which is assumed to be a wrapped field.
c0cd1b3e
LC
216 (syntax-case field ()
217 ((field get options ...)
218 (let* ((getter (syntax->datum #'get))
219 (real-getter (symbol-append '% getter '-real)))
220 (datum->syntax #'get real-getter)))))
221
222 (define (field-spec->srfi-9 field)
223 ;; Convert a field spec of our style to a SRFI-9 field spec of the
224 ;; form (field get).
225 (syntax-case field ()
226 ((name get options ...)
227 #`(name
310b32a2
LC
228 #,(if (wrapped-field? field)
229 (wrapped-field-accessor-name field)
c0cd1b3e
LC
230 #'get)))))
231
232 (define (thunked-field-accessor-definition field)
233 ;; Return the real accessor for FIELD, which is assumed to be a
234 ;; thunked field.
235 (syntax-case field ()
236 ((name get _ ...)
310b32a2 237 (with-syntax ((real-get (wrapped-field-accessor-name field)))
c0cd1b3e
LC
238 #'(define-inlinable (get x)
239 ;; The real value of that field is a thunk, so call it.
240 ((real-get x)))))))
241
310b32a2
LC
242 (define (delayed-field-accessor-definition field)
243 ;; Return the real accessor for FIELD, which is assumed to be a
244 ;; delayed field.
245 (syntax-case field ()
246 ((name get _ ...)
247 (with-syntax ((real-get (wrapped-field-accessor-name field)))
248 #'(define-inlinable (get x)
249 ;; The real value of that field is a promise, so force it.
250 (force (real-get x)))))))
251
c0cd1b3e
LC
252 (syntax-case s ()
253 ((_ type syntactic-ctor ctor pred
254 (field get options ...) ...)
9b543456
LC
255 (let* ((field-spec #'((field get options ...) ...))
256 (thunked (filter-map thunked-field? field-spec))
310b32a2 257 (delayed (filter-map delayed-field? field-spec))
9b543456
LC
258 (defaults (filter-map field-default-value
259 #'((field options ...) ...))))
c0cd1b3e
LC
260 (with-syntax (((field-spec* ...)
261 (map field-spec->srfi-9 field-spec))
262 ((thunked-field-accessor ...)
263 (filter-map (lambda (field)
264 (and (thunked-field? field)
265 (thunked-field-accessor-definition
266 field)))
310b32a2
LC
267 field-spec))
268 ((delayed-field-accessor ...)
269 (filter-map (lambda (field)
270 (and (delayed-field? field)
271 (delayed-field-accessor-definition
272 field)))
c0cd1b3e
LC
273 field-spec)))
274 #`(begin
275 (define-record-type type
276 (ctor field ...)
277 pred
278 field-spec* ...)
310b32a2
LC
279 (begin thunked-field-accessor ...
280 delayed-field-accessor ...)
39fc041a
LC
281 (make-syntactic-constructor type syntactic-ctor ctor
282 (field ...)
283 #:thunked #,thunked
284 #:delayed #,delayed
285 #:defaults #,defaults))))))))
c0cd1b3e 286
c8772a7a
LC
287(define* (alist->record alist make keys
288 #:optional (multiple-value-keys '()))
289 "Apply MAKE to the values associated with KEYS in ALIST. Items in KEYS that
290are also in MULTIPLE-VALUE-KEYS are considered to occur possibly multiple
291times in ALIST, and thus their value is a list."
292 (let ((args (map (lambda (key)
293 (if (member key multiple-value-keys)
294 (filter-map (match-lambda
295 ((k . v)
296 (and (equal? k key) v)))
297 alist)
298 (assoc-ref alist key)))
299 keys)))
c0cd1b3e
LC
300 (apply make args)))
301
302(define (object->fields object fields port)
303 "Write OBJECT (typically a record) as a series of recutils-style fields to
304PORT, according to FIELDS. FIELDS must be a list of field name/getter pairs."
305 (let loop ((fields fields))
306 (match fields
307 (()
308 object)
309 (((field . get) rest ...)
310 (format port "~a: ~a~%" field (get object))
311 (loop rest)))))
312
fb519bd8
LC
313(define %recutils-field-charset
314 ;; Valid characters starting a recutils field.
315 ;; info "(recutils) Fields"
316 (char-set-union char-set:upper-case
317 char-set:lower-case
318 (char-set #\%)))
836d10f1 319
fdc1bf65
LC
320(define (recutils->alist port)
321 "Read a recutils-style record from PORT and return it as a list of key/value
322pairs. Stop upon an empty line (after consuming it) or EOF."
323 (let loop ((line (read-line port))
324 (result '()))
b7b88288 325 (cond ((eof-object? line)
fdc1bf65 326 (reverse result))
b7b88288
LC
327 ((string-null? line)
328 (if (null? result)
329 (loop (read-line port) result) ; leading space: ignore it
330 (reverse result))) ; end-of-record marker
fdc1bf65 331 (else
fb519bd8
LC
332 ;; Now check the first character of LINE, since that's what the
333 ;; recutils manual says is enough.
334 (let ((first (string-ref line 0)))
335 (cond
336 ((char-set-contains? %recutils-field-charset first)
337 (let* ((colon (string-index line #\:))
338 (field (string-take line colon))
339 (value (string-trim (string-drop line (+ 1 colon)))))
340 (loop (read-line port)
341 (alist-cons field value result))))
342 ((eqv? first #\#) ;info "(recutils) Comments"
343 (loop (read-line port) result))
344 ((eqv? first #\+) ;info "(recutils) Fields"
345 (let ((new-line (if (string-prefix? "+ " line)
346 (string-drop line 2)
347 (string-drop line 1))))
348 (match result
349 (((field . value) rest ...)
350 (loop (read-line port)
351 `((,field . ,(string-append value "\n" new-line))
352 ,@rest))))))
353 (else
354 (error "unmatched line" line))))))))
fdc1bf65 355
c0cd1b3e 356;;; records.scm ends here