Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Class star #889

Merged
merged 79 commits into from Sep 3, 2020
Merged

Class star #889

merged 79 commits into from Sep 3, 2020

Conversation

Ambrevar
Copy link
Member

@Ambrevar Ambrevar commented Aug 7, 2020

Unfinished, do not merge!

This is to show-case the new define-class macro.

I decided to base it on top of hu.dwim.defclass-star because it saved me hours of work and I'm a bit short on time. If we ever need to replace defclass-star in the future, it won't be a problem.
hu.dwim.defclass-star has only 1 dependency, it's a rather light addition and it's already packaged for Guix.

Features

  • All the defclass-star goodies, including global/local class/slot exports, automatic initargs and automatic accessors.

  • Inferred initforms. "Do what I mean" by default (i.e. use the zero value for simple types, nil otherwise). The initform can always be specified manually. I'll make the inference fully customizable later.

  • Cycle detection in superclasses: We can now extend classes like this

  (define-class foo (foo)
   ((extra-slot...))

Result

I've applied the change to the window class. As a result, we save more than 20 annoying lines. For bigger classes, the gain is even bigger.

No more redundancies which means no more risks for typos.

What's left to be done
The tests are not finished and I need to polish the zero-value inference.

Feedback welcome!

@Ambrevar
Copy link
Member Author

To do: Move replace-class, with-class, etc. to this new library. Remove all the defclass-export macros.
This would save even more lines of code in the Nyxt code base ;)

@Ambrevar
Copy link
Member Author

More to do: Add helpers to retrieve original class.

@Ambrevar
Copy link
Member Author

I've implemented all the missing features.

I still need to do some more testing, then I'll merge.

@jmercouris: What do you think of my zero-value-inference customization? See the readme.

@jmercouris
Copy link
Member

This is one of the first times I have just been absolutely speechless! I cannot stress enough how awesome this is. This is just fantastic!

I really like how we can utilize these objects so seamlessly for configuration of the program. After all, all of our code surface is API, so this really helps in deciding how we expose it, and automating that.

As per the zero-value inference- brilliant. Why isn't this done elsewhere? It seems so obvious now!

Of course, not everything is sunshine and rainbows. You forgot the spdx license header :-P
otherwise, I am excited for this to join master!

@Ambrevar
Copy link
Member Author

Ambrevar commented Aug 27, 2020 via email

@Ambrevar
Copy link
Member Author

Ambrevar commented Aug 27, 2020 via email

@Ambrevar
Copy link
Member Author

Ambrevar commented Aug 27, 2020 via email

@jmercouris
Copy link
Member

Please, merge as soon as ready!

@@ -0,0 +1,220 @@
(in-package :class*)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

license header missing

@@ -0,0 +1,17 @@
(in-package :cl-user)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

license header missing

@@ -0,0 +1,138 @@
(in-package :cl-user)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

header

@@ -882,10 +882,10 @@ As a second value, return the current buffer index starting from 0."
(webkit-history-entry-gtk-object history-entry)))

(defun set-renderer ()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about this, why is this a function instead of all of the invokations being at the top level? The idea being that you can set the renderer at runtime to something else?

@Ambrevar
Copy link
Member Author

Ambrevar commented Aug 27, 2020 via email

@Ambrevar
Copy link
Member Author

Ambrevar commented Aug 27, 2020 via email

@jmercouris
Copy link
Member

OK, that makes some sense. Perhaps we'll drop it into start at some point then :-)

@Ambrevar Ambrevar force-pushed the class-star branch 2 times, most recently from 885fab2 to b769fe7 Compare August 27, 2020 17:22
@Ambrevar
Copy link
Member Author

Ambrevar commented Aug 27, 2020 via email

@Ambrevar Ambrevar force-pushed the class-star branch 2 times, most recently from 396ae64 to 1b5ebc3 Compare August 27, 2020 18:04
@Ambrevar
Copy link
Member Author

Ambrevar commented Aug 27, 2020 via email

@jmercouris
Copy link
Member

I can test later today.

@jmercouris
Copy link
Member

When trying to show the buffer list.

This is because of the fdeclaration:

debugger invoked on a TYPE-ERROR in thread
#<THREAD "main thread" RUNNING {10009E8083}>:
  The value
    #<NYXT:GTK-INTERNAL-BUFFER {101040C6A3}>
  is not of type
    NYXT:BUFFER
  when binding NYXT:BUFFER

Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.

restarts (invokable by number or by possibly-abbreviated name):
  0: [RETURN-FROM-G-CLOSURE] Return value from closure
  1: [ABORT                ] Exit debugger, returning to top level.

(NYXT:SET-CURRENT-BUFFER #<NYXT:GTK-INTERNAL-BUFFER {101040C6A3}>) [external]
   source: (DEFUN SET-CURRENT-BUFFER (BUFFER)
             "Set the active buffer for the active window."
             (UNLESS (EQ 'MINIBUFFER (CLASS-NAME (CLASS-OF BUFFER)))
               (IF (CURRENT-WINDOW)
                   (WINDOW-SET-ACTIVE-BUFFER (CURRENT-WINDOW) BUFFER)
                   (MAKE-WINDOW BUFFER))
               BUFFER))
0] 0

@jmercouris
Copy link
Member

I can confirm that the minibuffer is blank. I have no idea why that could be. It does function as expected...

@Ambrevar
Copy link
Member Author

Ambrevar commented Aug 30, 2020

I've been boggling my mind around this issue for a while. It seems that
with our current model it's hard to avoid over complexity. Besides our
current class replacement within renderer-gtk already creates a mess with
class definitions, even when the user has no config.

Then I remembered a paradigm I once read: composition is often better
than inheritance because it's more versatile while it eliminates a whole
class of complexity.

Structure/Class composition is often done with mixins or equivalents.
In CLOS, we can leverage multiple inheritance.

I suggestion the following paradigm:

  • For "abstract" (also known as "virtual") classes:

    • Never inherit from any other (Nyxt) class. (Currently it's the
      reason we break method specialization.)
    • Never redefine them. Currently this is our biggest induced complexity.
    • Never instantiate them.
  • For "concrete" classes:

    • Don't define any slot, only specify the superclasses.
    • While I think it's possible to specialize methods against them, it
      might not be necessary.
    • These are the only classes that should be instantiated.

In practice, we would have the following classes which don't inherit
from anything:

  • browser
  • gtk-browser
  • buffer
  • gtk-buffer
  • minibuffer
  • internal-buffer
  • web-buffer
  • window
  • gtk-window

And so on.

Then we would define the classes:

  • (defclass user-browser (browser) ())
  • (defclass user-web-buffer (web-buffer buffer) ())
  • (defclass user-internal-buffer (internal-buffer buffer) ())

All calls to make-instance would use these last classe name, e.g. 'user-browser, etc.

Then in renderer-gtk:

  • (defclass user-browser (gtk-browser browser) ())
  • (defclass user-web-buffer (web-buffer gtk-buffer buffer) ())
  • (defclass user-internal-buffer (internal-buffer gtk-buffer buffer) ())

We override the user-classes. Note that it does not modify the browser and buffer classes so it minimizes the side effects, it does not break method specialization and it does not mess up with class inheritance since gtk-buffer does not inherit from buffer.

If the user wants to change, say, the list of default modes for both web
buffers and internal buffers, they can do:

(defclass my-buffer ()
  ((default-modes ...)))

(defclass user-web-buffer (my-buffer web-buffer gtk-buffer buffer) ())
(defclass user-internal-buffer (my-buffer internal-buffer gtk-buffer buffer) ())

The define-configuration helper could generate my-buffer for us
(e.g. with (gentemp "BUFFER-CUSTOM")) and automatically redefine the
specified user-CLASSES, e.g.

(define-configuration (user-web-buffer user-internal-buffer)
  ((default-modes ...)))

which would roughly expand to the above, by adding the generated class
first in the list of superclasses (for highest precedence).

If we want to customize a class multiple times, no problem:

(defclass my-buffer2 ()
  ((default-url...)))

(defclass user-web-buffer (my-buffer2 my-buffer web-buffer gtk-buffer buffer) ()

Since define-configuration prepends the generated class to the list of
existing superclasses, this would work equivalently:

(define-configuration (user-web-buffer user-internal-buffer)
  ((default-modes ...)))
(define-configuration (user-web-buffer user-internal-buffer)
  ((default-url ...)))

@jmercouris Let me know if this approach seems sound to you and I'll
implement it.

EDIT: Added details about the user-CLASSES definitions in renderer-gtk.

@Ambrevar
Copy link
Member Author

Ambrevar commented Aug 31, 2020 via email

@jmercouris
Copy link
Member

I need some more time to think about this. Thank you for a very deep investigation. I think that we are close to a good solution.

@jmercouris
Copy link
Member

My first thought is that we can get a large quantity of simplification by reducing the requirements. For example, updating inheritees automatically may not be necessary.

@Ambrevar
Copy link
Member Author

Ambrevar commented Aug 31, 2020 via email

@Ambrevar
Copy link
Member Author

Ambrevar commented Aug 31, 2020 via email

@phoe
Copy link

phoe commented Aug 31, 2020

(define-final-class buffer (delete 'user-buffer1 (mopu:direct-superclasses 'REPLACEME-buffer)))

Calling the destructive function DELETE on the result of DIRECT-SUPERCLASSES, which returns the value of MOP:CLASS-DIRECT-SUPERCLASSES, is undefined behavior.

Use REMOVE instead for a non-destructive version, or explicitly call REMOVE-DIRECT-SUBCLASS. All in all, I'd suggest using the functional interface of ENSURE-CLASS instead of using macrology.

@Ambrevar
Copy link
Member Author

Ambrevar commented Aug 31, 2020 via email

This allowed us to override a class by itself, e.g.

(define-class foo (foo)
  ((new-slot ...)))

This is brittle however: typing is not well specified then on the various
implementations.
The children of FOO might not be of the new FOO type.
This may also break method dispatch.

Overall, it's a bad idea.
@Ambrevar
Copy link
Member Author

Ambrevar commented Aug 31, 2020 via email

((subtypep type 'hash-table) (make-hash-table))
;; Order matters for numbers:
((subtypep type 'integer) 0)
((subtypep type 'complex) #c(0 0))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This decays into the integer 0. (Try it in the REPL!)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I've changed the logic to partition between floats and the rest of the number types

;; Order matters for numbers:
((subtypep type 'integer) 0)
((subtypep type 'complex) #c(0 0))
((subtypep type 'number) 0.0)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you support rationals? Perhaps you might want to form an exhaustive partition of reals into floats and rationals?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this is a zero-value inference, not a type inference. (Only exceptionally useful.)

ratio is a subtype of number, so the zero value is handled here (see the latest commits which partitions between floats and the rest).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rational zero is 0, not 0.0, which is why I've actually made this comment. Sorry for being unclear.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem, you were right, this didn't do the right thing for rationals. The latest commit should fix it.

(handler-case (basic-type-zero-values type)
(error ()
;; Compile-time error:
(error "Missing initform.")))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this here? The capture-and-resignal behavior seems silly, especially since 1) you explicitly discard information coming from the original error, 2) you destroy the stack coming from the original error by using handler-case instead of handler-bind.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This trick is was not meant to expose conditions to the user, only to save me some 10 lines of code :/

It's not very useful and arguably confusing so I've replaced it with multiple return values (consistent with get-properties).

Comment on lines 347 to 348
(let ((*package* *package*))
(in-package :nyxt/auto-mode)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is equivalent to (let ((*package* (find-package :nyxt/auto-mode))) ...)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Fixed.

@aartaka
Copy link
Contributor

aartaka commented Aug 31, 2020

@aartaka: If I merge now it will create a few conflict with your current patch, is that OK?

It's okay, especially given that it will save lots of lines in the code of my patch at the cost of minor fixes :)

Complex #c(0 0) is decayed to 0, so it's the same as any other number except for
floats.
…ltiple values.

This is less confusing and more functional.
@jmercouris
Copy link
Member

I've done some testing, it looks good to me. I like this solution. It is a good place that we can build on top of!

@jmercouris jmercouris merged commit 9aa1cfa into master Sep 3, 2020
@Ambrevar Ambrevar deleted the class-star branch September 28, 2020 09:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants