guile-records


The core Guile interpreter has two APIs to handle high level aggregate data types: records and structures. Records are the easiest. While GOOPS offers a more advanced and convenient API to handle aggregate data types, the simplicity of records is ideal for users new to Scheme and Guile. Once working with records is understood, a matter of an afternoon, it is easy to step up to the usage of GOOPS.

uriel

This page is meant to be also an introductory page to the comprehension of Guile's structures (because records are implemented as structures) which in turn is an introductory topic on the comprehension of GOOPS internals (because GOOPS classes and instances are impelemented as structures).


What we do

To show how records are used we implement a module that lets us execute an inferior process from Guile with a customised set of environment variables. It is a simplified reimplementation of a module distributed with GEE (post alpha--0.3--patch-600 releases).

The idea is to build a record holding the command name and a set of default arguments:

 (define C (make-inpro "guile" '("-c"))) 

and then execute it with additional, optional, arguments:

 (inpro-run-with-system* C "(display 123)(newline)") 

Guile has different ways to launch an inferior process, here we implement only a method that mimics the system* procedure.

To set the environment for inferior process execution we use another record type: one that wraps an association list (alist) using symbols as keys and strings as values; each pair in the alist represents a variable/value assignment. To make an environment record we will do:

 (define envi (make-inpro-envi '((PATH . "/bin:/usr/bin") 
                                 (USERNAME . "operator")) #f)) 

to set the environment we will adopt the well known with- approach:

 (with-inpro-envi envi 
  (inpro-run-with-system* C "(display 123)")) 

Inferior process environment record

Let's see form by form how to prepare the environment record. First we need to define the record type:

 (define inpro-envi-record 
   (make-record-type 'inferior-process-environment 
                     '(variables exclusive))) 

make-record-type returns a handle that holds everything required to manage a record type. The name of the type is inferior-process-environment and there are two fields named variables and exclusive.

The variables field will hold the alist of environment variables.

The exclusive field will hold a boolean value: if #t the environment will be set using a single call to Guile's environ function, else the environment will be set using a call to Guile's setenv function for each variable.

In the first case the current process's environment will be erased and the record's environment will be set; in the second case the record's environment is superimposed to the process's environment.


Next we need a constructor for the record instances:

 (define make-inpro-envi (record-constructor inpro-envi-record 
                                             '(variables exclusive))) 

record-constructor returns a procedure that, when evaluated, returns a new instance of a record. By supplying the argument (variables exclusive), we tell it to build a constructor that accepts two arguments: the first will go into the variables field and the second will go into the exclusive field.

The following form:

 (define envi (make-inpro-envi '((PATH . "/bin:/usr/bin:/usr/local/bin") 
                                 (USERNAME . "operator")) #f)) 

makes a new record, storing it in envi, with two environment variables: PATH and USERNAME. The exclusive flag is set to false.


It is nice to have a predicate to test if a value is an instance of the environment record:

 (define inpro-envi? (record-predicate inpro-envi-record)) 

record-predicate returns such a predicate, it can be used like this:

(inpro-envi? envi)
=> #t
(inpro-envi? 123)
=> #f

Another feature is the possibility to set and retrieve the current exclusive flag value:

 (define inpro-envi-exclusive? 
   (make-procedure-with-setter 
  
    (record-accessor inpro-envi-record 'exclusive) 
    (record-modifier inpro-envi-record 'exclusive))) 

here we use a procedure with setter which provides a nice syntax:

(inpro-envi-exclusive? envi)
=> #f
(set! (inpro-envi-exclusive? envi) #t)
(inpro-envi-exclusive? envi)
=> #t

record-accessor returns a getter procedure that, applied to a record, returns the value of a field. record-modifier returns a setter procedure that, when applied to a record and a value, stores the value in a record's field.


We want to set the variables' alist, too:

 (define inpro-envi-variables 
   (make-procedure-with-setter 
    (record-accessor inpro-envi-record 'variables) 
    (record-modifier inpro-envi-record 'variables))) 
(inpro-envi-variables envi)
=> ((PATH . "/bin:/usr/bin:/usr/local/bin")
    (USERNAME . "operator"))

That is the end of it: we have finished to see the functions that make use of the record's API; from now on the juicy parts that implement the actual environment handling.


We need a setter and getter for a single variable:

 (define (inpro-envi-set! envi name value) 
   (set! (inpro-envi-variables envi) 
         (assq-set! (inpro-envi-variables envi) name value))) 
  
 (define (inpro-envi-ref envi name) 
   (assq-ref (inpro-envi-variables envi) name)) 

to be used like:

(inpro-envi-set! envi 'OTHER "123")
(inpro-envi-ref envi 'OTHER)
=> "123"

and it is nice to be able to remove a variable from the environment:

 (define (inpro-envi-unset! envi name) 
   (set! (inpro-envi-variables envi) 
         (assq-remove! (list-copy (inpro-envi-variables envi)) name))) 

to remove OTHER:

(inpro-envi-unset! envi 'OTHER)

If we want to commit the thing ourselves we need to extract from the record the environment in the format required by the environ function: a list of strings, each of which in the format NAME=VALUE:

 (define (inpro-envi-environ envi) 
   (map (lambda (pair) 
          (format #f "~A=~A" (symbol->string (car pair)) (cdr pair))) 
     (inpro-envi-variables envi))) 

applied to a record:

(inpro-envi-environ envi)
=> ("PATH=/bin:/usr/bin:/usr/local/bin"
    "USERNAME=operator")

The module's API must offer a way to commit the environment honouring the exclusive flag:

 (define (inpro-envi-commit envi) 
   (if (inpro-envi-exclusive? envi) 
       (environ (inpro-envi-environ envi)) 
     (for-each (lambda (p) 
                 (setenv (symbol->string (car p)) (cdr p))) 
       (inpro-envi-variables envi)))) 

if the flag is true we use environ, else we map each pair in the alist using setenv.


Now the most complicated function: we need a way to commit the environment, execute the inferior process, restore the previous environment.

We do not want a different thread to interrupt us between the environment commit and the process execution, so we lock a mutex just before committing the environment.

This is a work for dynamic-wind:

 (define inpro-envi-mutex (make-mutex)) 
  
 (define-macro (with-inpro-envi ENVI . FORMS) 
   `(with-inpro-envi-worker ,ENVI (lambda () ,@FORMS))) 
  
 (define (with-inpro-envi-worker envi thunk) 
   (let ((current-environ #f)) 
     (dynamic-wind 
         (lambda () 
           (lock-mutex inpro-envi-mutex) 
           (if envi 
               (begin 
                 (set! current-environ (environ)) 
                 (inpro-envi-commit envi)))) 
         thunk 
         (lambda () 
           (if current-environ 
               (environ current-environ)) 
           (unlock-mutex inpro-envi-mutex))))) 

the purpose of the macro is to let us put our forms directly in the body of the with- form without using lambda () ...); if we go directly with with-inpro-envi-worker we have to do:

 (with-inpro-envi-worker envi 
  (lambda () 
    (form-a) 
    (form-b) 
  
    (form-c))) 

while with with-inpro-envi we do:

 (with-inpro-envi envi 
  (form-a) 
  (form-b) 
  (form-c)) 

To see how it works:

(setenv "USERNAME" "marco")
(getenv "USERNAME")
=> "marco"
(with-inpro-envi envi
  (getenv "USERNAME"))
=> "operator"
(getenv "USERNAME")
=> "marco"

Inferior process record

We have seen enough to understand the record type definition:

 (define inpro-record (make-record-type 'inferior-process 
                                        '(pathname arguments))) 

with two fields: one for the command pathname as string and one for the command line arguments as a list of strings. If there are no command line arguments we have to use an empty list.

The record constructor definition:

 (define make-inpro (record-constructor inpro-record 
                                        '(pathname arguments))) 

that accepts two parameters: the pathname and the list of arguments as strings. The type predicate:

 (define inpro? (record-predicate inpro-record)) 

the setter/getter for the pathname:

 (define inpro-pathname 
   (make-procedure-with-setter 
    (record-accessor inpro-record 'pathname) 
    (record-modifier inpro-record 'pathname))) 

the setter/getter for the arguments:

 (define inpro-arguments 
   (make-procedure-with-setter 
    (record-accessor inpro-record 'arguments) 
    (record-modifier inpro-record 'arguments))) 

The most interesting function is the one that actually executes the inferior process:

 (define (inpro-run-with-system* inpro . additional-arguments) 
   (let ((pid    (primitive-fork))) 
     (if (= 0 pid) 
         (let ((command          (inpro-pathname inpro)) 
               (arguments        (if (inpro-arguments inpro) 
                                     (inpro-arguments inpro) 
                                   '()))) 
           (if (not (null? additional-arguments)) 
               (set! arguments (append arguments additional-arguments))) 
           (apply execlp command command arguments))) 
     pid)) 

we use the classic fork+exec method, and use execlp so that the command name is searched in the PATH environment variable, if it does not contain a slash character.

Notice that we let the user of inpro-run-with-system* set some additional command line arguments, that are put after the ones in the record's field.

Run it!

Now we only need to try it for real. We use the guile program itself as inferior process, because it lets us inspect the execution environment very easily.

First we define a couple of environments:

 (define envi 
   (make-inpro-envi '((PATH . "/bin:/usr/bin:/usr/local/bin") 
                      (USERNAME . "operator")) #f)) 
  
 (define envi/root 
   (make-inpro-envi 
    '((PATH . "/bin:/usr/bin:/usr/local/bin:/sbin:/usr/sbin") 
      (USERNAME . "root")) #f)) 

then we define the process using -c as default argument: it lets us evaluate a Scheme expression:

 (define C (make-inpro "guile" '("-c"))) 

now we run it, with and without enclosing environment:

(inpro-run-with-system*
 C "(format #t \"USERNAME is ~A~%\" (getenv \"USERNAME\"))")

(with-inpro-envi envi
 (inpro-run-with-system*
  C "(format #t \"USERNAME is ~A~%\" (getenv \"USERNAME\"))"))

(with-inpro-envi envi/root
 (inpro-run-with-system*
  C "(format #t \"USERNAME is ~A~%\" (getenv \"USERNAME\"))"))

The full module

 ;; inferior-process.scm -- 
  
 (define-module (inferior-process)) 
  
 ;; ------------------------------------------------------------ 
  
 (define inpro-envi-record (make-record-type 'inferior-process-environment 
                                             '(variables exclusive))) 
  
  
 (define make-inpro-envi (record-constructor inpro-envi-record 
                                             '(variables exclusive))) 
  
 (define inpro-envi? (record-predicate inpro-envi-record)) 
  
 (define inpro-envi-exclusive? 
   (make-procedure-with-setter (record-accessor inpro-envi-record 'exclusive) 
                               (record-modifier inpro-envi-record 'exclusive))) 
  
 (define inpro-envi-variables 
   (make-procedure-with-setter (record-accessor inpro-envi-record 'variables) 
                               (record-modifier inpro-envi-record 'variables))) 
  
 (export 
  
  make-inpro-envi inpro-envi? 
  inpro-envi-exclusive? inpro-envi-variables) 
  
 ;; ------------------------------------------------------------ 
  
 (define (inpro-envi-set! envi name value) 
   (set! (inpro-envi-variables envi) 
         (assq-set! (inpro-envi-variables envi) name value))) 
  
 (define (inpro-envi-ref envi name) 
   (assq-ref (inpro-envi-variables envi) name)) 
  
 (define (inpro-envi-unset! envi name) 
   (set! (inpro-envi-variables envi) 
         (assq-remove! (list-copy (inpro-envi-variables envi)) name))) 
  
 (define (inpro-envi-environ envi) 
   (map (lambda (pair) 
          (format #f "~A=~A" (symbol->string (car pair)) (cdr pair))) 
     (inpro-envi-variables envi))) 
  
 (define (inpro-envi-commit envi) 
   (if (inpro-envi-exclusive? envi) 
       (environ (inpro-envi-environ envi)) 
     (for-each (lambda (p) 
                 (setenv (symbol->string (car p)) (cdr p))) 
       (inpro-envi-variables envi)))) 
  
 (export 
  
  with-inpro-envi with-inpro-envi-worker) 
  
 ;; ------------------------------------------------------------ 
  
 (define inpro-envi-mutex (make-mutex)) 
  
 (define-macro (with-inpro-envi ENVI . FORMS) 
   `(with-inpro-envi-worker ,ENVI (lambda () ,@FORMS))) 
  
 (define (with-inpro-envi-worker envi thunk) 
   (let ((current-environ #f)) 
     (dynamic-wind 
         (lambda () 
           (lock-mutex inpro-envi-mutex) 
           (if envi 
               (begin 
                 (set! current-environ (environ)) 
                 (inpro-envi-commit envi)))) 
         thunk 
         (lambda () 
           (if current-environ 
               (environ current-environ)) 
           (unlock-mutex inpro-envi-mutex))))) 
  
 (export 
  
  inpro-envi-set! inpro-envi-ref inpro-envi-unset! 
  inpro-envi-environ inpro-envi-commit) 
  
 ;; ------------------------------------------------------------ 
  
 (define inpro-record (make-record-type 'inferior-process 
                                        '(pathname arguments))) 
  
  
 (define make-inpro (record-constructor inpro-record 
                                        '(pathname arguments))) 
  
 (define inpro? (record-predicate inpro-record)) 
  
 (define inpro-pathname 
   (make-procedure-with-setter (record-accessor inpro-record 'pathname) 
                               (record-modifier inpro-record 'pathname))) 
  
 (define inpro-arguments 
   (make-procedure-with-setter (record-accessor inpro-record 'arguments) 
                               (record-modifier inpro-record 'arguments))) 
  
 (export 
  
  make-inpro inpro? inpro-pathname inpro-arguments) 
  
 ;; ------------------------------------------------------------ 
  
 (define (inpro-run-with-system* inpro . additional-arguments) 
   (let ((pid    (primitive-fork))) 
     (if (= 0 pid) 
         (let ((command          (inpro-pathname inpro)) 
               (arguments        (if (inpro-arguments inpro) 
                                     (inpro-arguments inpro) 
                                   '()))) 
           (if (not (null? additional-arguments)) 
               (set! arguments (append arguments additional-arguments))) 
           (apply execlp command command arguments))) 
     pid)) 
  
 (export 
  
  inpro-run-with-system*) 
  
 ;;; end of file 

category-guile