Gauche Devlog

< Method call optimization - skipping sort-applicable-methods |

2017/05/23

A heads-up for an incompatible fix in util.match

TL;DR: If you match records with inheritance using $ or struct match pattern, you need to change the code for 0.9.6.

We fixed a bug in the positional record matching pattern of match, existed in 0.9.5 and before. The fix actually breaks previously documented behavior, but we believe the previous behavior was incorrect and decided it's better to fix now.

Background

The $, or struct pattern allows you to extract slot values from objects using match (ref:match):

(define-class <point> ()
  ((x :init-keyword :x)
   (y :init-keyword :y)
   (z :init-keyword :z)))

(match (make <point> :x 1 :y 2 :z 3)
  [($ <point> a b c)
   (list a b c)])  => (1 2 3)

However, Gauche's object system isn't designed to access slots with their positions. You use slot names instead. In match, you can use object pattern (or @ in sort) to match with slot values, using slot names.

(match (make <point> :x 1 :y 2 :z 3)
  [(@ <point> (x a) (y b) (z c))
   (list a b c)])  => (1 2 3)

The reason we provided $ was for the compatibility of original Wright's match, which aimed at struct types provided in some Scheme impelementations. We didn't give much thought to it; just made the pattern match with the slot values of the order of class-slots (ref:class-slots). It works just fine with srfi:9 records:

(define-record-type pare (make-pare fst snd) pare?
  (fst get-fst)
  (snd get-snd))

(match (make-pare 1 2)
  [($ pare a b)
   (list a b)])  => (1 2)

Problem

Things got complicated when inheritance enters the picture. How the inherited slots are laid out depends on the implementation of metaclass (ref:compute-slots generic function), and because of multiple inheritance, the slot layout of class S doesn't necessarily a subsequence of the layout of class T that inheriting S. This is highly confusing, and we've always recommended using object match in such a case, in the manual.

However, srfi:99 records only allows single inheritance chain, and the default constructor takes initial value of inherited slot first. So it is a natural call to make positional match in the same way.

(define-record-type S make-S S?
  a b)

(define-record-type (T S) make-T T?   ;; inherit S
  c d)

(make-S 1 2)      ;; Initialize a=1, b=2

(make-T 1 2 3 4)  ;; Initialize a=1, b=2, c=3, d=4

;; Then, ($ T w x y z) should match with w=1, x=2, y=3, z=4.

It hadn't been so. The compute-slots method of <record> placed the direct slots first, followed by the inherited slots. It needs to do so to be consistent with that "fields in derived record types shadow fields of the same name in a parent record type", as defined in srfi-99.

Thus, ($ T w x y z) pattern in the above example matched w=3, x=4, y=1, and z=2. This wasn't inconsistent with the manual, which stated that positional match was done with the order of class-slots. It was an unintended artifact of implementation that was overlooked, unfortunately.

It also had a defect when duplicate slot names existed. When a subclass defines a slot with the same name as inherited slot, the standard compute-slots merges them into one, which is also CLOS's behaviro. However, srfi:99 record types allow subtype to have slots with the same name but as independent slots.

(define-record-type S #t #t
  a)

(define-record-type (T S) #t #t
  a)

(define t (make-T 1 2))

(T-a t)  ;=> 2    ; accesses T's a
(S-a t)  ;=> 1    ; accesses S's a in T

(slot-ref t 'a) ;=> 2  ; named access takes the subtype's slot

The existing implementation of positional matching needed to rely on named slot access, and didn't work on such record types.

Fix

We introduced a generic function to be specialized with metaclass, that handles positional access within match. We keep the underlying mechanism undocumented for now; changing the way of positional matching should be rare and based on well-established customs. The order of record types fits this criteria, and made to work as expected:

(define-record-type S make-S S?
  a b)

(define-record-type (T S) make-T T?   ;; inherit S
  c d)

(match (make-T 1 2 3 4)
  [($ T w x y z) (list w x y z)]) => (1 2 3 4)

Now it also works with record types having duplicate slot names:

(define-record-type S #t #t
  a)

(define-record-type (T S) #t #t
  a)

(match (make-T 1 2)
  [($ T x y) (list x y)])   ;=> (1 2);  was (2 2) before

We hope few have used positional match with inherited records--- the old behavior seems apparently wrong---so we decided to fix this now.

If you happen to have the code that relies on the previous behavior, and need to make it work with both versions, you can switch to use named match (object or @).

Tags: 0.9.6, util.match, gauche.record

Post a comment

Name: