Subject: Re: [PATCH v8 03/41] guix: node-build-system: Add JSON utilities.

From: Liliana Marie Prikler <[email-masked]>
Date: Sat, 08 Jan 2022 08:00:32 +0100

Hi,

Am Freitag, dem 07.01.2022 um 23:13 -0500 schrieb Philip McGrath:
> FWIW, while I don't feel strongly about `let` vs. `define` in
> general, I find SRFI-71's overloaded `let` less clear than internal
> definitions and `define-values`, which are supported by core Guile.
Internal definitions are an implicit letrec, which might be useful to
have, but I prefer explicit let-binding.  It has the added benefit of
being shorter to write and having clearer scope.

> I would prefer (KEY [DEFAULT] PROC). In my experience, DEFAULT is
> often something simple like #f, and writing it after a multi-line
> lambda expression is not very pleasant. 
The reason to have DEFAULT be an optional final argument is imo
symmetry with how you'd call the underlying alist functions.  It makes
the code a little easier to grok, but you're right that inline DEFAULT
looks nicer.

> The docstring no longer specifies left-to-right evaluation or that
> the default DEFAULT is #f.
That's a bug.

>  (And I still think '(@) is a better default DEFAULT.)
I don't think so.  Values could be of any type, not necessarily object
type (e.g. suppose you're updating a key:string mapping).  So explicit
'(@) is preferred in my opinion.

> I don't especially like all of the explicit quasiquotation of lists
> in the rest argument.
Two things: First it makes it easier to understand where each update
begins and where each update ends.  Second, I'm not satisfied with
quasi-quote either (I'd like to have substitute-json* syntax ideally),
but it's a fair middle ground between my ideal solution and what I
could do on top of your submission in a short time.

> > +(define (jsobject-union combine seed . objects)
> > +  "Merge OBJECTS into SEED by applying (COMBINE KEY VAL0 VAL),
> > where VAL0
> > +is the value found in the (possibly updated) SEED and VAL is the
> > new value
> > +found in one of the OBJECTS."
> > +  (match seed
> > +    (('@ . aseed)
> > +     (match objects
> > +       (() seed)
> > +       ((('@ . alists) ...)
> > +        (cons
> > +         '@
> > +         (fold (lambda (alist aseed)
> > +                 (if (null? aseed) alist
> > +                     (fold
> > +                      (match-lambda*
> > +                        (((k . v) aseed)
> > +                         (let ((pair tail (alist-pop alist k)))
> > +                           (match pair
> > +                             (#f (acons k v aseed))
> > +                             ((_ . v0) (acons k (combine k v0 v)
> > aseed))))))
> > +                      aseed
> > +                      alist)))
> > +               aseed
> > +               alists)))))))
> > +
> > +;; Possibly useful helper functions:
> > +;; (define (newest key val0 val) val)
> > +;; (define (unkeyed->keyed proc) (lambda (_key val0 val) (proc
> > val0 val)))
> 
> I much prefer a keyword argument #:combine, and I still think the 
> key-agnostic case is so much more common that the separation of 
> #:combine/key is useful.
I think we could define an (alist-flatten alist #:key combine default-
combine), where again combine is your combine/key and default-combine
is your combine.  Then we could define (json-object-union objs ...)
as (alist-flatten (append-map json-object->alist objs) #:combine ...).
Explicit conversion of json-object to alist and back seems to be the
wiser option to me than defining everything twice.  WDYT?