5. Hashes

Introduction

;; ---------------------------------------------------------------------
;; Scheme offers two dictionary types:
;;
;; * Association list [list of pairs e.g. '((k1 . v1) (k2 . v2) ...)]
;; * Hash table [vector of pairs plus hash algorithm]
;;
;; Implementation differences aside, they are remarkably similar in that
;; the functions operating on them are similar named, and offer the same
;; interface. Examples:
;;
;; * Retrieve an item: (assoc-ref hash key)
;;                     (hash-ref hash key)
;;
;; * Update an item:   (assoc-set! hash key value)
;;                     (hash-set! hash key value) 
;;
;; Hash tables would tend to be used where performance was critical e.g.
;; near constant-time lookups, or where entry updates are frequent,
;; whilst association lists would be used where table-level traversals
;; and manipulations require maximum flexibility
;;
;; Many of the sections include examples using both association lists
;; and hash tables. However, where only one of these is shown,
;; implementing the other is usually a trivial exercise. Finally, any
;; helper functions will be included in the Appendix
;; ---------------------------------------------------------------------

; Association lists
(define *age*
  (list
    (cons 'Nat 24)
    (cons 'Jules 25)
    (cons 'Josh 17)))

;; or, perhaps more compactly:
(define *age*
  (list
    '(Nat . 24)
    '(Jules . 25)
    '(Josh . 17)))

;; ------------

; Guile built-in association list support
(define *age* (acons 'Nat 24 '()))
(set! *age* (acons 'Jules 25 *age*))
(set! *age* (acons 'Josh 17 *age*))

;; ----

; SRFI-1 association list support
(use-modules (srfi srfi-1))

(define *age* (alist-cons 'Nat 24 '()))
(set! *age* (alist-cons 'Jules 25 *age*))
(set! *age* (alist-cons 'Josh 17 *age*))

;; ------------

(define *food-colour*
  (list
    '(Apple . "red")
    '(Banana . "yellow")
    '(Lemon . "yellow")
    '(Carrot . "orange")))

;; ----------------------------

; Hash tables. Guile offers an implementation, and it is also 
; possible to use SRFI-69 hash tables; only the former will be
; illustrated here

(define *age* (make-hash-table 20))
; or
(define *age* (make-vector 20 '()))

(hash-set! *age* 'Nat 24)
(hash-set! *age* 'Jules 25)
(hash-set! *age* 'Josh 17)

(hash-for-each
  (lambda (key value) (print key))
  *age*)

; or, if vector used as hash table, can also use:

(array-for-each
  (lambda (pair)
    (if (not (null? pair)) (print (car pair))))
  *age*)

;; ------------

(define *food-colour* (make-hash-table 20))

(hash-set! *food-colour* 'Apple "red")
(hash-set! *food-colour* 'Banana "yellow")
(hash-set! *food-colour* 'Lemon "yellow")
(hash-set! *food-colour* 'Carrot "orange")

Adding an Element to a Hash

(set! *hash* (acons key value *hash*))

;; ------------

(set! *food-colour* (acons 'Raspberry "pink" *food-colour*))

(print "Known foods:")
(for-each
  (lambda (pair) (print (car pair)))
  *food-colour*)

;; ----------------------------

(hash-set! *hash* key value)

;; ------------

(hash-set! *food-colour* 'Raspberry "pink")

(print "Known foods:")
(hash-for-each
  (lambda (key value) (print key))
  *food-colour*)

Testing for the Presence of a Key in a Hash

; 'assoc' returns the pair, (key . value)
(if (assoc key hash)
  ;; ... found ...
  '()
;else
  ;; ... not found ...
  '()

; 'assoc-ref' returns the value only
(if (assoc-ref hash key)
  ;; ... found ...
  '()
;else
  ;; ... not found ...
  '()

;; ------------

; *food-colour* association list from an earlier section

(for-each
  (lambda (name)
    (let ((pair (assoc name *food-colour*)))
      (if pair
        (print (symbol->string (car pair)) "is a food")
      ;else
        (print (symbol->string name) "is a drink") )))
  (list 'Banana 'Martini))

;; ----------------------------

; 'hash-get-handle' returns the pair, (key . value)
(if (hash-get-handle hash key)
  ;; ... found ...
  '()
;else
  ;; ... not found ...
  '()

; 'hash-ref' returns the value only
(if (hash-ref hash key)
  ;; ... found ...
  '()
;else
  ;; ... not found ...
  '()

;; ------------

; *food-colour* hash table from an earlier section

(for-each
  (lambda (name)
    (let ((value (hash-ref *food-colour* name)))
      (if value
        (print (symbol->string name) "is a food")
      ;else
        (print (symbol->string name) "is a drink") )))
  (list 'Banana 'Martini))

;; ----------------------------

(define *age* (make-hash-table 20))

(hash-set! *age* 'Toddler 3)
(hash-set! *age* 'Unborn 0)
(hash-set! *age* 'Phantasm '())

(for-each
  (lambda (thing)
    (let ((value (hash-ref *age* thing)))
      (display thing)
      (if value (display " Exists"))
      (if (and value (not (string-null? value))) (display " Defined"))
      ; Testing for non-zero as true is not applicable, so testing
      ; for non-equality with zero 
      (if (and value (not (eq? value 0))) (display " True"))
      (print "") ))
  (list 'Toddler 'Unborn 'Phantasm 'Relic))

Deleting from a Hash

(assoc-remove! hash key)

;; ------------

(use-modules (srfi srfi-1))

; *food-colour* association list from an earlier section

(define (print-foods)
  (let ((foods
          (fold-right
            (lambda (pair accum) (cons (car pair) accum))
            '()
            *food-colour*)))
    (display "Keys: ") (print foods)
    (print "Values:")
    (for-each
      (lambda (food)
        (let ((colour (assoc-ref *food-colour* food)))
          (cond
            ((string-null? colour) (display "(undef) "))
            (else (display (string-append colour " "))) )))
      foods))
    (newline))

(print "Initially:")
(print-foods)

(print "\nWith Banana undef")
(assoc-set! *food-colour* 'Banana "")
(print-foods)

(print "\nWith Banana deleted")
(assoc-remove! *food-colour* 'Banana)
(print-foods)

;; ----------------------------

(hash-remove! hash key)

;; ------------

(use-modules (srfi srfi-1))

; *food-colour* hash table from an earlier section

(define (print-foods)
  (let ((foods
          (hash-fold
            (lambda (key value accum) (cons key accum))
            '()
            *food-colour*)))
    (display "Keys: ") (print (reverse foods))
    (print "Values:")
    (for-each
      (lambda (food)
        (let ((colour (hash-ref *food-colour* food)))
          (cond
            ((string-null? colour) (display "(undef) "))
            (else (display (string-append colour " "))) )))
      foods))
    (newline))

(print "Initially:")
(print-foods)

(print "\nWith Banana undef")
(hash-set! *food-colour* 'Banana "")
(print-foods)

(print "\nWith Banana deleted")
(hash-remove! *food-colour* 'Banana)
(print-foods)

Traversing a Hash

; Since an association list is nothing more than a list of pairs, it
; may be traversed using 'for-each'
(for-each
  (lambda (pair)
    (let ((key (car pair))
          (value (cdr pair)))
      ;; ... do something with key / value ...
      '()))
  hash)

;; ----------------------------

; A 'for-each'-like function is available for hash table traversal
(hash-for-each
  (lambda (key value)
    ;; ... do something with key / value ...
    '())
  hash)

; If the hash table is directly implemented as a vector, then it is
; also possible to traverse it using, 'array-for-each', though a 
; check for empty slots is needed 
(array-for-each
  (lambda (pair)
    (if (not (null? pair)) ... do something with key / value ...))
  hash)

;; ----------------------------

; *food-colour* association list from an earlier section

(for-each
  (lambda (pair)
    (let ((food (car pair))
          (colour (cdr pair)))
      (print (symbol->string food) "is" colour) ))
  *food-colour*)

;; ------------

; *food-colour* association list from an earlier section

(for-each
  (lambda (food)
    (print (symbol->string food) "is" (assoc-ref *food-colour* food)))
  (sort
    (fold-right
      (lambda (pair accum) (cons (car pair) accum))
      '()
      *food-colour*)
    (lambda (left right)
      (string<? (symbol->string left) (symbol->string right)))))

;; ----------------------------

(use-modules (srfi srfi-1) (ice-9 rdelim) (ice-9 regex))

(define *filename* "from.txt")
(define *from* '())

(let ((port (open-input-file *filename*)))
  (let loop ((line&terminator (read-line port 'split)))
    (cond
      ((eof-object? (cdr line&terminator)) '())
      (else
        (let* ((key (string->symbol
                      (match:substring
                        (string-match
                          "^From: (.*)" (car line&terminator))
                        1) ))
               (value (assoc-ref *from* key)))
          (if (not value) (set! value 0))
          (set! *from* (assoc-set! *from* key (+ 1 value))))
        (loop (read-line port 'split)) ))) 
  (close-input-port port))

(for-each
  (lambda (person)
    (print (symbol->string person) ":"
           (number->string (assoc-ref *from* person))))
  (sort
    (fold-right
      (lambda (pair accum) (cons (car pair) accum))
      '()
      *from*)
    (lambda (left right)
      (string<? (symbol->string left) (symbol->string right)))))

Printing a Hash

; All approaches shown in the previous section apply here also, so
; there is little to be gained by repeating those examples [i.e. the
; use of 'for-each' and similar]. It is always possible, of course,
; to directly recurse over an association list:

; *food-colour* association list from an earlier section

(define *sorted-food-colour*
  (sort
    *food-colour*
    (lambda (left right)
      (string<?
        (symbol->string (car left))
        (symbol->string (car right)))) ))

(let loop ((hash *sorted-food-colour*))
  (cond
    ((null? hash) '())
    (else  
      (print
        (symbol->string (car (car hash))) "=>" (cdr (car hash)) )
      (loop (cdr hash))) ))

Retrieving from a Hash in Insertion Order

; AFAIK, Scheme doesn't offer a facility similar to Perl's 'Tie::IxHash'.
; Therefore, use an association list if retrieval [from a dictionary
; type container] in insertion order is required.

(define *food-colour* (acons 'Banana "Yellow" '()))
(set! *food-colour* (acons 'Apple "Green" *food-colour*))
(set! *food-colour* (acons 'Lemon "yellow" *food-colour*))

(print "In insertion order, the foods are:")
(for-each
  (lambda (pair)
    (let ((food (car pair))
          (colour (cdr pair)))
      (print "  " (symbol->string food)) ))
  *food-colour*)

(print "Still in insertion order, the food's colours are:")
(for-each
  (lambda (pair)
    (let ((food (car pair))
          (colour (cdr pair)))
      (print (symbol->string food) "is coloured" colour) ))
  *food-colour*)

;; ----------------------------

; Of course, insertion order is lost if the association list is sorted,
; or elements removed, so if maintaining insertion order is vital, it
; might pay to associate data with a timestamp [e.g. create a timestamped
; record / structure], and manipulate those entities [no example given]

Hashes with Multiple Values Per Key

(define *ttys* '())

(for-each
  (lambda (user-tty-pair)
    (let* ((user-tty-pair (string-split user-tty-pair #\space))
           (user (string->symbol (car user-tty-pair)))
           (newtty (cadr user-tty-pair))
           (current-ttys (assoc-ref *ttys* user)))
      (set! *ttys*
        (assoc-set! *ttys* user
          (if (not current-ttys)
            newtty
            (string-append current-ttys " " newtty)) ))))
  (string-split (qx "who|cut -d' ' -f1,2") #\newline))

(for-each
  (lambda (user-ttys)
    (print (symbol->string (car user-ttys)) ":" (cdr user-ttys)))
  (sort
    *ttys*
    (lambda (left right)
      (string<?
        (symbol->string (car left))
        (symbol->string (car right))))) )

;; ----------------------------

(use-modules (ice-9 regex))

(define (multi-hash-delete hash key value)
  (let ((value-found (assoc-ref hash key)))
    (if value-found
      (assoc-ref hash key
        (regexp-substitute/global
          #f (string-match value value-found) 'pre "" 'post "")))))

Inverting a Hash

; Alternate implementatons of a hash inversion function; both assume
; key is a symbol, value is a string

(define (assoc-invert assoc)
  (map
    (lambda (pair)
      (cons
        (string->symbol (cdr pair))
        (symbol->string (car pair))))
    assoc))

;; ------------

(define (assoc-invert assoc)
  (let loop ((assoc assoc) (new-assoc '()))
    (cond
      ((null? assoc) new-assoc)
      (else 
        (loop (cdr assoc)
              (acons
                (string->symbol (cdar assoc))
                (symbol->string (caar assoc)) new-assoc)) )) ))

;; ----------------------------

(define *surname*
  (list
    '(Mickey . "Mantle")
    '(Babe . "Ruth")))

(define *first-name* (assoc-invert *surname*))

(print (assoc-ref *first-name* 'Mantle))

;; ----------------------------

; foodfind

(define *given* (string->symbol (cadr (command-line))))

(define *colour*
  (list
    '(Apple . "red")
    '(Lemon . "yellow")
    '(Carrot . "orange")))

(define *food* (assoc-invert *colour*))

(if (assoc-ref *colour* *given*)
  (print
    (symbol->string *given*) 
    "is a food with colour"
    (assoc-ref *colour* *given*)))

(if (assoc-ref *food* *given*)
  (print
    (assoc-ref *food* *given*)
    "is a food with colour"
    (symbol->string *given*)))

Sorting a Hash

; *food-colour* association list from an earlier section

; Use 'sort' to sort the entire hash, on key or on value, ascending or
; descending order
(define *sorted-on-key:food-colour*
  (sort
    *food-colour*
    (lambda (left right)
      (string<?
        (symbol->string (car left))
        (symbol->string (car right)))) ))

(define *sorted-on-value:food-colour*
  (sort
    *food-colour*
    (lambda (left right)
      (string<?
        (cdr left)
        (cdr right))) ))

;; ------------

(for-each
  (lambda (pair)
    (let ((food (car pair))
          (colour (cdr pair)))
      (print
        (symbol->string food)
        "is"
        colour)))
  *sorted-on-key:food-colour*)

;; ----------------------------

; Alternatively, generate a list of keys or values, sort as required,
; and use list to guide the hash traversal

(define *sorted-food-colour-keys*
  (sort
    (fold-right
      (lambda (pair accum) (cons (car pair) accum))
      '()
      *food-colour*)
    (lambda (left right)
      (string<?
        (symbol->string left)
        (symbol->string right))) ))

(define *sorted-food-colour-values*
  (sort
    (fold-right
      (lambda (pair accum) (cons (cdr pair) accum))
      '()
      *food-colour*)
    (lambda (left right)
      (string<? left right)) ))

;; ------------

(for-each
  (lambda (food)
    (print (symbol->string food) "is" (assoc-ref *food-colour* food)))
  *sorted-food-colour-keys*)

Merging Hashes

; If merging is defined as the combining of the contents of two or more
; hashes, then it is simply a matter of copying the contents of each
; into a new hash

; Association lists can simply be appended together
(define *food-colour*
  (list
    '(Apple . "red")
    '(Banana . "yellow")
    '(Lemon . "yellow")
    '(Carrot . "orange")))

(define *drink-colour*
  (list
    '(Galliano . "yellow")
    '(Mai Tai . "blue")))

(define *ingested-colour* (append *food-colour* *drink-colour*))

;; ----------------------------

; Hash tables built from vectors can be copied element by element into
; a new vector, or spliced together using 'vector-join' [see Chapter 4]

(define *food-colour* (make-vector 20 '())
; ...
(define *drink-colour* (make-vector 20 '())
; ...

(define *ingested-colour*
  (vector-join *food-colour* *drink-colour*))

Finding Common or Different Keys in Two Hashes

(define *common* '())
(define *this-not-that* '())

;; ------------

(define *dict1*
  (list
    '(Apple . "red")
    '(Lemon . "yellow")
    '(Carrot . "orange")))

(define *dict2*
  (list
    '(Apple . "red")
    '(Carrot . "orange")))

;; ------------

; Find items common to '*dict1*' and '*dict2*'
(for-each
  (lambda (pair)
    (let ((key (car pair))
          (value (cdr pair)))
      (if (assoc-ref *dict2* key)
        (set! *common* (cons key *common*))) ))
  *dict1*)

;; ------------

; Find items in '*dict1*' but not '*dict2*'
(for-each
  (lambda (pair)
    (let ((key (car pair))
          (value (cdr pair)))
      (if (not (assoc-ref *dict2* key))
        (set! *this-not-that* (cons key *this-not-that*))) ))
  *dict1*)

;; ----------------------------

(define *non-citrus* '())

(define *citrus-colour*
  (list
    '(Lemon . "yellow")
    '(Orange . "orange")
    '(Lime . "green")))

(define *food-colour*
  (list
    '(Apple . "red")
    '(Banana . "yellow")
    '(Lemon . "yellow")
    '(Carrot . "orange")))

(for-each
  (lambda (pair)
    (let ((key (car pair))
          (value (cdr pair)))
      (if (not (assoc-ref *citrus-colour* key))
        (set! *non-citrus* (cons key *non-citrus*))) ))
  *food-colour*)

Hashing References

; All objects [including functions] are first class entities, so there
; is no problem / special treatment needed to use any object, including
; those classed as 'references' [e.g. file handles or ports] as keys

(use-modules (srfi srfi-1) (srfi srfi-13))

(define *ports* '())

(for-each
  (lambda (filename)
    (let ((port (open-input-file filename)))
      (set! *ports* (assoc-set! *ports* port filename)) ))
  '("/etc/termcap" "/vmlinux" "/bin/cat"))

(print
  (string-append "open files: "
    (string-drop
      (fold-right
        (lambda (pair accum) (string-append ", " (cdr pair) accum))
        ""
        *ports*)
      2)))

(for-each
  (lambda (pair)
    (let ((port (car pair))
          (filename (cdr pair)))
      (seek port 0 SEEK_END)
      (print filename "is" (number->string (ftell port)) "bytes long.")
      (close-input-port port) ))
  *ports*)

Presizing a Hash

; An association list takes on the size of the number of elements with
; which it is initialised, so presizing is implicit

(define *hash* '())         ; zero elements

;; ------------

(define *hash*              ; three elements
  (list
    '(Apple . "red")
    '(Lemon . "yellow")
    '(Carrot . "orange")))

;; ----------------------------

; A size [i.e. number of entries] must be specified when a hash table
; is created, so presizing is implicit

(define *hash* (make-hash-table 100))

;; ------------

(define *hash* (make-vector 100 '()))

Finding the Most Common Anything

(define *array*
  (list 'a 'b 'c 'd 'd 'a 'a 'c 'd 'd 'e))

;; ----------------------------

(define *count* '())

(for-each
  (lambda (element)
    (let ((value (assoc-ref *count* element)))
      (if (not value) (set! value 0))
      (set! *count* (assoc-set! *count* element (+ 1 value)))))
  *array*)

;; ----------------------------

(define *count* (make-hash-table 20))

(for-each
  (lambda (element)
    (let ((value (hash-ref *count* element)))
      (if (not value) (set! value 0))
      (hash-set! *count* element (+ 1 value))))
  *array*)

Representing Relationships Between Data

(define *father*
  (list
    '(Cain . Adam)
    '(Abel . Adam)
    '(Seth . Adam)
    '(Enoch . Cain)
    '(Irad . Enoch)
    '(Mehujael . Irad)
    '(Methusael . Mehujael)
    '(Lamech . Methusael)
    '(Jabal . Lamech)
    '(Jubal . Lamech)
    '(Tubalcain . Lamech)
    '(Enos . Seth)))

;; ------------

(use-modules (srfi srfi-1) (ice-9 rdelim))

(let ((port (open-input-file *filename*)))
  (let loop ((line&terminator (read-line port 'split)))
    (cond
      ((eof-object? (cdr line&terminator)) '())
      (else
        (let ((person (string->symbol (car line&terminator))))
          (let loop ((father (assoc-ref *father* person)))
            (if father
            (begin
              (print father)
              (loop (assoc-ref *father* father)) )))
        (loop (read-line port 'split)) ))))
  (close-input-port port))

;; ------------

(use-modules (srfi srfi-1) (ice-9 rdelim))

(define (assoc-invert-N:M assoc)
  (let ((new-assoc '()))
    (for-each
      (lambda (pair)
        (let* ((old-key (car pair))
               (new-key (cdr pair))
               (new-key-found (assoc-ref new-assoc new-key)))
          (if (not new-key-found)
            (set! new-assoc (acons new-key (list old-key) new-assoc)) 
          ;else
            (set! new-assoc (assoc-set! new-assoc new-key
                            (cons old-key new-key-found))) )))
      assoc)
  new-assoc))

(define *children* (assoc-invert-N:M *father*))

(let ((port (open-input-file *filename*)))
  (let loop ((line&terminator (read-line port 'split)))
    (cond
      ((eof-object? (cdr line&terminator)) '())
      (else
        (let* ((person (string->symbol (car line&terminator)))
               (children-found (assoc-ref *children* person)))
          (print (symbol->string person) "begat:")
          (if (not children-found)
            (print "nobody")
          ;else
            (for-each
              (lambda (child) (print (symbol->string child) ","))
              children-found))
        (loop (read-line port 'split)) ))))
  (close-input-port port))

Program: dutree

;; @@INCOMPLETE@@
;; @@INCOMPLETE@@