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
Show file tree
Hide file tree
Changes from 74 commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
af7b278
Init class-star library.
Ambrevar Aug 7, 2020
e783c58
Use define-class for WINDOW.
Ambrevar Aug 7, 2020
12f95d4
class-star: Move replace-class and with-class to library.
Ambrevar Aug 25, 2020
39f5016
class-star: Add original-class helper function.
Ambrevar Aug 25, 2020
78f5810
class-star: Make zero value inference customizable.
Ambrevar Aug 25, 2020
34f66d8
class-star: Update readme with more examples are detailed list of fea…
Ambrevar Aug 27, 2020
2e44c70
class-star: Rename *type-zero-function to initform-inference.
Ambrevar Aug 27, 2020
216fd66
class-star: Add type inference.
Ambrevar Aug 27, 2020
747d1ca
class-star: Simplify process-slot-initform logic.
Ambrevar Aug 27, 2020
1f35810
class-star: Fix package exports.
Ambrevar Aug 27, 2020
0bc5ab3
class-star: Add tests for initform inference.
Ambrevar Aug 27, 2020
a158321
class-star: Add tests for type inference.
Ambrevar Aug 27, 2020
b748ee5
class-star: Fix type inference to support symbols, empty lists and ar…
Ambrevar Aug 27, 2020
89714f6
Use define-class for BUFFER.
Ambrevar Aug 27, 2020
ed77eb6
class-star: Add license headers.
Ambrevar Aug 27, 2020
ad2080b
Use define-class for BROWSER.
Ambrevar Aug 27, 2020
825ef46
Use define-class for AUTO-MODE-RULE.
Ambrevar Aug 27, 2020
abd968f
Use define-class for HOSTLIST.
Ambrevar Aug 27, 2020
f469b0c
Use define-class for BOOKMARK-ENTRY.
Ambrevar Aug 27, 2020
4808d14
Use define-class for PROXY.
Ambrevar Aug 27, 2020
0fb89c9
Use define-class for REQUEST-DATA.
Ambrevar Aug 27, 2020
9a54fe0
Use define-class for BUFFER-DESCRIPTION.
Ambrevar Aug 27, 2020
19791b5
Use define-class in define-mode.
Ambrevar Aug 27, 2020
0bfc28b
Use define-class in renderer-gtk.
Ambrevar Aug 27, 2020
20d1bd6
Use define-class in renderer-qt.
Ambrevar Aug 27, 2020
370751a
Use define-class in SEARCH-ENGINE.
Ambrevar Aug 27, 2020
c6e332b
Use define-class in define-configuration.
Ambrevar Aug 27, 2020
646cdd3
Use define-class in data-storage.
Ambrevar Aug 27, 2020
f9cfa6d
Remove defclass-export from data-storage.
Ambrevar Aug 27, 2020
100a2bd
Fix missing initforms in emacs-mode, vi-insert-mode and vi-normal-mode.
Ambrevar Aug 27, 2020
8221dc5
Use define-class syntax in application-mode.
Ambrevar Aug 27, 2020
784c229
Use define-class syntax in auto-mode.
Ambrevar Aug 27, 2020
64e6cec
Use define-class syntax in base-mode.
Ambrevar Aug 27, 2020
a3f74fb
Use define-class syntax in blocker-mode.
Ambrevar Aug 27, 2020
8c0707f
Use define-class syntax in set-tag-mode.
Ambrevar Aug 27, 2020
6123587
Use define-class syntax in certificate-exception-mode.
Ambrevar Aug 27, 2020
2624f0f
Use define-class syntax in emacs-mode.
Ambrevar Aug 27, 2020
ab050db
Use define-class syntax in file-manager-mode.
Ambrevar Aug 27, 2020
e9a7046
Use define-class syntax in force-https-mode.
Ambrevar Aug 27, 2020
490ebc0
Use define-class syntax in manual.
Ambrevar Aug 27, 2020
22110e5
Use define-class syntax in minibuffer-mode.
Ambrevar Aug 27, 2020
6a30f18
Use define-class syntax in root-mode.
Ambrevar Aug 27, 2020
0189f43
Use define-class syntax in noimage-mode.
Ambrevar Aug 27, 2020
c987040
Use define-class syntax in noscript-mode.
Ambrevar Aug 27, 2020
a96f9e3
Use define-class syntax in proxy-mode.
Ambrevar Aug 27, 2020
ae97892
Use define-class syntax in reading-line-mode.
Ambrevar Aug 27, 2020
3336279
Use define-class syntax in repl-mode.
Ambrevar Aug 27, 2020
84824aa
Use define-class syntax in vi-mode.
Ambrevar Aug 27, 2020
4b375ba
Use define-class syntax in web-mode.
Ambrevar Aug 27, 2020
8deaa92
Use define-class for MINIBUFFER.
Ambrevar Aug 27, 2020
5047e19
class-star: Don't generate `:type' when inference returns nil.
Ambrevar Aug 28, 2020
7df8381
class-star: Fix tests with CCL.
Ambrevar Aug 28, 2020
d4209a4
Remove `minibuffer' forward declaration.
Ambrevar Aug 28, 2020
ab7e561
Remove default-search-engine duplicate.
Ambrevar Aug 28, 2020
6d55189
class-star: Fix typo in test.
Ambrevar Aug 29, 2020
f86fe16
class-star: Support external symbols for type inference.
Ambrevar Aug 29, 2020
1e525da
GTK: Fix web-context type.
Ambrevar Aug 29, 2020
ac8791a
auto-mode: Fix current package to avoid deserialization errors on mat…
Ambrevar Aug 29, 2020
ef35b56
Fix browser bookmarks-data and last-active-buffer types.
Ambrevar Aug 29, 2020
1c1d06c
Fix minibuffer callback type.
Ambrevar Aug 29, 2020
53b65c3
class-star: Don't export the temp name of class override.
Ambrevar Aug 29, 2020
3732f04
class-star: Properly replace overriden class type with CCL.
Ambrevar Aug 29, 2020
4b97876
GTK: Fix duplicate web-context accessor.
Ambrevar Aug 29, 2020
e95d838
class-star: Set temporary class name to overridden class name.
Ambrevar Aug 29, 2020
14fa9e9
Compose classes instead of using inheritance.
Ambrevar Aug 31, 2020
f414f9f
configuration: Maintain backward compatibility in define-configuratio…
Ambrevar Aug 31, 2020
0521a20
Extract original-class function.
Ambrevar Aug 31, 2020
764a5fa
Use define-REPLACEME-class macro.
Ambrevar Aug 31, 2020
fde9127
Replace last manual REPLACEME- definitions with define-REPLACEME-class.
Ambrevar Aug 31, 2020
464a6bc
Use closer-mop:ensure-class instead of defclass in define-REPLACEME-c…
Ambrevar Aug 31, 2020
269352b
Rename REPLACEME to "user".
Ambrevar Aug 31, 2020
7c0d278
class-star: Remove superclasses cycle detection.
Ambrevar Aug 31, 2020
6aa6a16
Export define-user-class.
Ambrevar Aug 31, 2020
03391ae
Add with-user-class helper.
Ambrevar Aug 31, 2020
3056284
Fix define-configuration example.
Ambrevar Aug 31, 2020
3e0f6ea
Qt: Use define-user-class.
Ambrevar Aug 31, 2020
b16bc03
class-star: Infer zero value of float and numbers.
Ambrevar Sep 1, 2020
1945934
Let-bind package with find-package instead of calling in-package.
Ambrevar Sep 1, 2020
d439805
class-star: Replace catch-and-resignal zero inferrance juggle with mu…
Ambrevar Sep 1, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions build-scripts/guix.scm
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
("dexador" ,cl-dexador)
("enchant" ,cl-enchant)
("fset" ,cl-fset)
("hu.dwim.defclass-star" ,cl-hu.dwim.defclass-star)
("iolib" ,cl-iolib)
("local-time" ,cl-local-time)
("log4cl" ,cl-log4cl)
Expand Down
187 changes: 187 additions & 0 deletions libraries/class-star/class-star.lisp
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
;;;; SPDX-FileCopyrightText: Atlas Engineer LLC
;;;; SPDX-License-Identifier: BSD-3-Clause

(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


(defun name-identity (name definition)
(declare (ignore definition))
name)

(defun initform (definition)
"Return (BOOLEAN INITFORM) when initform is found."
(let ((definition (rest definition)))
(if (oddp (length definition))
(values t (first definition))
(multiple-value-bind (found? value)
(get-properties definition '(:initform))
(values (not (null found?)) value)))))

(defun definition-type (definition)
"Return definition's TYPE.
Return nil if not found."
(let ((definition (rest definition)))
(when (oddp (length definition))
(setf definition (rest definition)))
(getf definition :type)))

(defun basic-type-zero-values (type)
"Return TYPE zero value.
An error is raised if the type is unsupported."
(cond
((subtypep type 'string) "")
((subtypep type 'boolean) nil)
((subtypep type 'list) '())
((subtypep type 'array) (make-array 0))
((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

((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.

(t (error "Unknown type"))))

(defun basic-type-inference (definition)
"Return general type of VALUE.
This is like `type-of' but returns less specialized types for some common
subtypes, e.g. for \"\" return 'string instead of `(SIMPLE-ARRAY CHARACTER
\(0))'.

Note that in a slot definition, '() is infered to be a list while NIL is infered
to be a boolean.

Non-basic form types are not infered (returns nil).
Non-basic scalar types are derived to their own type (with `type-of')."
(multiple-value-bind (found? value)
(initform definition)
(when found?
(cond
((and (consp value)
(eq (first value) 'quote)
(symbolp (second value)))
(if (eq (type-of (second value)) 'null)
'list ; The empty list.
'symbol))
((and (consp value)
(eq (first value) 'function))
'function)
((and (consp value)
(not (eq (first value) 'quote)))
;; Non-basic form.
nil)
(t (let* ((type (if (symbolp value)
(handler-case
;; We can get type of externally defined symbol.
(type-of (eval value))
(error ()
;; Don't infer type if symbol is not yet defined.
nil))
(type-of value))))
(if type
(flet ((derive-type (general-type)
(when (subtypep type general-type)
general-type)))
(or (some #'derive-type '(string boolean list array hash-table integer
complex number))
;; Only allow objects of the same type by default.
;; We could have returned nil to inhibit the generation of a
;; :type property.
type))
nil)))))))

(defun type-zero-initform-inference (definition)
"Infer basic type zero values.
See `basic-type-zero-values'.
Raise a condition at macro-expansion time when initform is missing for unsupported types."
(let ((type (definition-type definition)))
(if type
(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).

;; Default initform when type is missing:
nil)))

(defun no-unbound-initform-inference (definition)
"Infer basic type zero values.
Raise a condition when instantiating if initform is missing for unsupported types."
(let ((type (definition-type definition)))
(if type
(handler-case (basic-type-zero-values type)
(error ()
;; Run-time error:
'(error "Slot must be bound.")))
;; Default initform when type is missing:
nil)))

(defun nil-fallback-initform-inference (definition)
"Infer basic type zero values.
Fall back to nil if initform is missing for unsupported types."
(let ((type (definition-type definition)))
(if type
(handler-case (basic-type-zero-values type)
(error ()
;; Fall-back to nil:
nil))
;; Default initform when type is missing:
nil)))

(defvar *initform-inference* 'type-zero-initform-inference
"Fallback initform inference function.
Set this to nil to disable inference.")
(defvar *type-inference* 'basic-type-inference
"Fallback type inference function.
Set this to nil to disable inference.")

(defun process-slot-initform (definition &key ; See `hu.dwim.defclass-star:process-slot-definition'.
initform-inference
type-inference)
(unless (consp definition)
(setf definition (list definition)))
(cond
((and (initform definition)
(or (definition-type definition)
(not type-inference)))
definition)
((and (initform definition)
(not (definition-type definition))
type-inference)
(append definition
(let ((result-type (funcall type-inference definition)))
(when result-type
(list :type result-type)))))
((and (not (initform definition))
initform-inference)
(append definition
(list :initform (funcall initform-inference definition))))
((and (not (initform definition))
(not initform-inference))
definition)
(t (error "Condition fell through!"))))

(defmacro define-class (name supers &body (slots . options))
"Define class like `defclass*' but with extensions.

The default initforms can be automatically inferred by the function specified
in the `:initform-inference' option, which defaults to `*initform-inference*'.
The initform can still be specified manually with `:initform' or as second
argument, right after the slot name.

The same applies to the types with the `:type-inference' option, the
`*type-inference*' default and the `:type' argument respectively."
(let* ((initform-option (assoc :initform-inference options))
(initform-inference (or (when initform-option
(setf options (delete :initform-inference options :key #'car))
(eval (second initform-option)))
*initform-inference*))
(type-option (assoc :type-inference options))
(type-inference (or (when type-option
(setf options (delete :type-inference options :key #'car))
(eval (second type-option)))
*type-inference*)))
`(hu.dwim.defclass-star:defclass* ,name ,supers
,(mapcar (lambda (definition)
(process-slot-initform
definition
:initform-inference initform-inference
:type-inference type-inference))
slots)
,@options)))
17 changes: 17 additions & 0 deletions libraries/class-star/package.lisp
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
;;;; SPDX-FileCopyrightText: Atlas Engineer LLC
;;;; SPDX-License-Identifier: BSD-3-Clause

(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


(defpackage :class-star
(:nicknames class*)
(:use :common-lisp)
(:import-from :hu.dwim.defclass-star)
(:export #:define-class
#:name-identity
#:*initform-inference*
#:type-zero-initform-inference
#:no-unbound-initform-inference
#:nil-fallback-initform-inference
#:*type-inference*
#:basic-type-inference))
68 changes: 68 additions & 0 deletions libraries/class-star/readme.org
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
This library enhances the class definition idiom with saner defaults and more
slot and class options. It wraps around the =hu.dwim.defclass-star= macro and
adds a few more features:

- initform customization (such as zero values);
- type inference (optionally errors out when type cannot be inferred).

Metaclasses would not be very useful here since most of our features need to be
enacted at compile-time, while metaclasses are mostly useful on classe
/instances/.

=defclass/std= is another popular library with a similar goal, but with more
insistance on conciseness, maybe at the expanse of readability. In particular,
it implements a way to specify slots by properties which may seem unnatural (we
read slots by their name, not by their properties).

* Features

- Implicit, customizable initarg and accessor like =defclass*=.
- Implicit, customizable initform. The default =*type-zero-function*= derives
the initform from the zero-value of basic types (e.g. 0 for integers, "" for
strings) or raise a macro-expansion-time error for unsupported types.
Other functions with different behaviours are provided, or you can provide your own.
- Implicit, customizable type inference from the initform.
- The initform can be specified as usual with =:initform= or directly in second
position, right after the slot name.
- Class option to toggle the default slot exportation.
- The `:export' slot option allows to specify the exportation of individual slots.

* Examples

#+begin_src lisp
(define-class foo ()
((slot1 :type integer) ; defaults to 0
(slot2 "hello!" :type string)
(unexported-slot :export nil))
(:export-class-name-p t)
(:export-accessor-names-p t)
(:accessor-name-transformer #'class*:name-identity))

(make-instance 'foo :my-slot1 17)
#+end_src

You can customize how to infer the default value. For instance, to fall back to
nil when the type is not one of the known basic types (see =basic-type-zero-values=):

#+begin_src lisp
(define-class bar ()
((name :type class))
(:type-zero-function #'nil-fallback-type-zero-function))
#+end_src

See the =*type-zero-function*= for the default inference function.

You can also set this option to nil, in which case you get the default behaviour
of no inference at all.

* References

- defclass/std:
https://github.com/EuAndreh/defclass-std
http://quickdocs.org/defclass-std/
- hu.dwim.defclass-star / defclass*:
http://quickdocs.org/hu.dwim.defclass-star/api
https://common-lisp.net/project/defclass-star/configuration.lisp.html
- Serapeum:
https://github.com/ruricolist/serapeum/
https://github.com/ruricolist/serapeum/issues/38
108 changes: 108 additions & 0 deletions libraries/class-star/tests/tests.lisp
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
;;;; SPDX-FileCopyrightText: Atlas Engineer LLC
;;;; SPDX-License-Identifier: BSD-3-Clause

(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


(prove:plan nil)

(prove:subtest "Simple class"
(prove:is (progn
(class*:define-class foo ()
((name "fooname")))
(let ((foo (make-instance 'foo)))
(name-of foo)))
"fooname"))

(prove:subtest "Simple class with custom accessors"
(class*:define-class bar ()
((name "fooname")
(age :accessor this-age)
(address :accessor nil))
(:accessor-name-transformer (lambda (name def) (declare (ignore def)) name)))
(make-instance 'bar)
(prove:ok (fboundp 'name))
(prove:ok (fboundp 'this-age))
(prove:is (fboundp 'address) nil))

(prove:subtest "Simple class default value"
(prove:is (progn
(class*:define-class foo-default ()
((name :type string)
(age :type number)))
(let ((foo (make-instance 'foo-default)))
(name-of foo)))
""))

;; TODO: Fix following test and try to make it portable.
#+nil
(prove:subtest "No initarg"
(prove:is-error (let ((hu.dwim.defclass-star:*automatic-initargs-p* nil))
(class*:define-class foo-no-initarg ()
((name :type string)))
(make-instance 'foo-no-initarg :name "bar"))
'sb-pcl::initarg-error))

(prove:subtest "No accessor"
(prove:is (progn
(class*:define-class foo-no-accessors ()
((name-no-acc :type string))
(:automatic-accessors-p nil))
(make-instance 'foo-no-accessors)
(fboundp 'name-no-acc-of))
nil))

(prove:subtest "Initform inference"
(class*:define-class foo-initform-infer ()
((name :type string)))
(prove:is (name-of (make-instance 'foo-initform-infer))
"")
(class*:define-class foo-initform-infer-no-unbound ()
((name :type function))
(:initform-inference 'class*:no-unbound-initform-inference))
(prove:is-error (make-instance 'foo-initform-infer-no-unbound)
'simple-error)
(class*:define-class foo-initform-infer-nil-fallback ()
((name :type (or function null)))
(:initform-inference 'class*:nil-fallback-initform-inference))
(prove:is (name-of (make-instance 'foo-initform-infer-nil-fallback))
nil))

(defvar street-name "bar")
(prove:subtest "Type inference"
(class*:define-class foo-type-infer ()
((name "foo")
(nickname street-name)
(age 1)
(height 2.0)
(width 2 :type number)
(lisper nil)
(empty-list '())
(nonempty-list '(1 2 3))
(mark :foo)
(sym 'sims)
(fun #'list)
(composite (error "Should not eval, type should not be infered"))))
(prove:is (getf (mopu:slot-properties 'foo-type-infer 'name) :type)
'string)
(prove:is (getf (mopu:slot-properties 'foo-type-infer 'nickname) :type)
'string)
(prove:is (getf (mopu:slot-properties 'foo-type-infer 'age) :type)
'integer)
(prove:is (getf (mopu:slot-properties 'foo-type-infer 'height) :type)
'number)
(prove:is (getf (mopu:slot-properties 'foo-type-infer 'width) :type)
'number)
(prove:is (getf (mopu:slot-properties 'foo-type-infer 'lisper) :type)
'boolean)
(prove:is (getf (mopu:slot-properties 'foo-type-infer 'empty-list) :type)
'list)
(prove:is (getf (mopu:slot-properties 'foo-type-infer 'nonempty-list) :type)
'list)
(prove:is (getf (mopu:slot-properties 'foo-type-infer 'sym) :type)
'symbol)
(prove:is (getf (mopu:slot-properties 'foo-type-infer 'fun) :type)
'function)
(prove:is (getf (mopu:slot-properties 'foo-type-infer 'composite) :type)
nil))

(prove:finalize)