lilypond-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: Scheme pattern for retrieving data in objects


From: Jean Abou Samra
Subject: Re: Scheme pattern for retrieving data in objects
Date: Sun, 3 Apr 2022 18:17:11 +0200
User-agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Thunderbird/91.7.0

Le 02/04/2022 à 13:38, Han-Wen Nienhuys a écrit :
It's shorter, but is it easier to understand and discover? Is the
former code (which is somewhat verbose) a real barrier to getting
coding/typesetting done?

Over the years, I've become extremely wary of syntactic sugar: it adds
an extra barrier to usage/development because everyone not only has to
learn Scheme, they also have to learn the (lilypond specific) idioms
involved.



Yes. On the other hand, LilyPond also has idioms that nobody
would think of *not* using -- I think we can agree that it's
a good thing we write engravers as

(make-engraver
 ((initialize engraver)
    ...)
 (acknowledgers
  ((note-head-interface engraver grob source-engraver)
     ...)))

rather than

`((initialize
   . ,(lambda (engraver)
        ...))
  (acknowledgers
   . ((note-head-interface
       . ,(lambda (engraver grob source-engraver)
            ...)))))

So I think it depends on the usage frequency and the gain
in ease of use and conciseness. My thinking is that we have
similarly annoying patterns in callbacks in particular (also
music functions). What I'm musing with is essentially
a 'make-callback' in disguise -- something that would eventually
become the natural way to do it, like nobody (I believe)
would think of writing an engraver the alist way.

I'd also say that the nature of Scheme makes it such that
there is not all that much of a difference between syntax
and APIs -- if you wanted a language where the whole game
is not to build a language for your needs on top of the
basics, the time to choose Python was 20 years ago :-)


[Dan]
I'm not enthusiastic about multiple variable declarations in one line or variable declarations that don't even look like declarations.


I hear you, but I do not see a preexisting consistent look
for lvalues.


(let ((x ...)
      (y ...)
      (z ...))
  ...)

(lambda (x y z)
  ...)

(define-values (x y z)
  ...)

(match-let* (((x y z) ...))
  ...)



+1.  I can imagine frustration after imitating some working code that calls `ly:grob-property` and being asked in review to use `fetch` instead because it is better.


Well, at the risk of sounding like a jerk, the same already
applies to auto *const vs. Smob_type *, C++11 member init
vs. initialize (), range-based loops vs. index-based loops, ...
I guess everyone has their favorite languages where they would like
more productive idioms and their less-favorite languages where
they want less features to learn. I am not a stranger to the
learning curve issue either: cf.
https://gitlab.com/lilypond/lilypond/-/merge_requests/1102#note_805070851
From my point of view, the difference is that that it doesn't
take 2 minutes to learn what 'fetch' does once you've read an
example.  (Whereas, searching about C++ lambdas on
cppreference.com, I read things like "The lambda expression is a
prvalue expression of unique unnamed non-union non-aggregate
class type, known as closure type, which is declared (for the
purposes of ADL) in the smallest block scope, class scope, or
namespace scope that contains the lambda expression."
<https://en.cppreference.com/w/cpp/language/lambda>. And I need
to understand a C++ feature that I use or I could introduce flaky
segfaults and the like ...)

Coming back to the topic of reviews: see above, I'd hope to find something
that ultimately becomes more natural a thing to do than the current
idiom.




If the basic syntax is too verbose, could it be revised rather than added to?

  (ly:get grob 'X-offset)
  (ly:get grob 'left-bound-info 'padding)
  (ly:get context 'alterationGlyphs 1/2)


A single function operating on many "types" is a Scheme antipattern: since
Scheme only has latent typing, a pure Scheme implementation would have to
test the value against a predefined set of type predicates on every access.
It might be possible to do in C++ with dynamic dispatch on our smob classes,
but it's still not really how Scheme thinks (and it won't work with alists
or ly:grob-object vs. ly:grob-property). A ly:grob-nested-property similar
to ly:grob-set-nested-property! does look reasonable.


---

I've been thinking about this more and came up with something a little
different, as it appeared on examples that the nesting of let* and
fetch was annoying. This version of it is a kitchen sink, basically
serving as an experiment. Ultimately I don't think we'd want a macro
with all of its features, but I'm not sure yet what selected features
could be the most useful. It is an extended version of let*, called
"with", that accepts clauses of the form

(name <= ly:grob-property grob)

which translates into

(name (ly:grob-property grob 'name))

I've chosen "with" for now because it's the same number of characters as "let*", saving from reindenting (the usual issue with switching between let and let*).
The use of <= is inspired from => in cond clauses.

There can be several names:

(element elements articulations <= ly:music-property music)

There can be a default:

((padding 0.5) (shorten-pair '(0 . 0)) <= ly:grob-property grob)

Other than that, it is equivalent to (ice-9 match) match-let*,
with the SRFI 71 feature of accepting multiple values.


\version "2.23.8"

#(use-modules ((ice-9 match) #:select (match-let match-let*))
              ((srfi srfi-71) #:select (values->list)))

#(define-syntax with
   (lambda (St-Ax)
     (syntax-case St-Ax ()
       ((_ (binding ...) . body)
        #`(match-let*
           (#,@(let loop ((bindings #'(binding ...))
                          (acc '()))
                 (syntax-case bindings ()
                   (()
                    (reverse! acc))
                   ((b . rest)
                    (loop #'rest
                           (syntax-case #'b (<=)
                             ((target ... <= getter obj)
                              (let loop2 ((targets #'(target ...))
                                          (result
                                           (cons* #'(evald-getter getter)
                                                  #'(evald-obj obj)
                                                  acc)))
                                (syntax-case targets ()
                                  (()
                                   result)
                                  (((name default) . others)
                                   (loop2 #'others
                                          (cons #'(name (evald-getter evald-obj 'name default))
                                                result)))
                                  ((t . others)
                                   (loop2 #'others
                                          (cons #'(t (evald-getter evald-obj 't))
                                                result))))))
                             ((target ... value-source)
                              (cons #'((target ...) (values->list value-source))
                                    acc))))))))
           . body)))))


%% Examples, with alists

#(define (assoc-ref-dflt alist key . rest)
   ;; sigh ...
   (apply assoc-get key alist rest))


#(with ((alist '((a . A) (b . B) (c . C) (d . (D1 . D2)) (e . (E1 . E2))))
        (a b <= assoc-ref-dflt alist)
        (c d e f (g 0.1) <= assoc-ref-dflt alist)
        ;; pattern matching
        ((e1 . e2) e)
        ;; unpacking values
        (d1 d2 (car+cdr d)))
   (ly:message "a=~a b=~a c=~a d1=~a d2=~a e1=~a e2=~a f=~a g=~a" a b c d1 d2 e1 e2 f g))



And sorry for the disorganized braindump.


Jean




reply via email to

[Prev in Thread] Current Thread [Next in Thread]