Maybe this question is too general, nevertheless i'll try: Is there any comprehensive guide on types in common lisp?
I'm kind of confused about this subject:
Why are non-primitive types declared in make-array
's :element-type
are promoted to t
? Is there any possibility for compile-time or runtime checks of the real declared type?
Why are CLOS slot defined types don't work as constraints, allowing to put value of any type into the slot? Again, what about the checks?
The same for the functions' types declarations with declare
.. Are they just the optimization hints to the compiler?
Also, can I use custom type specifiers, including satisfies
in forementioned places for some robust checks, or they could only be used for explicit checks with typep
e.t.c?
As you can see, i've got some mess in my head, so would really appreciate any neat guide (or a set of guides).
I'm on SBCL, but would also be glad to know about differences between implementations.
You need to tell the compiler to optimize for safety if you want it to actually enforce the types:
CL-USER> (declaim (optimize (safety 3)))
NIL
CL-USER> (defclass foobar () ())
#<STANDARD-CLASS COMMON-LISP-USER::FOOBAR>
CL-USER> (defun foo (a)
(make-array 1 :element-type 'foobar
:initial-contents (list a)))
FOO
CL-USER> (foo (make-instance 'foobar))
#(#<FOOBAR {1005696CE3}>)
CL-USER> (foo 12)
;=> ERROR
CL-USER> (declaim (ftype (function (integer integer) integer) quux))
(QUUX)
CL-USER> (defun quux (a b)
(+ a b))
QUUX
CL-USER> (quux 12 12)
24 (5 bits, #x18, #o30, #b11000)
CL-USER> (quux 12 "asd")
;=> ERROR
Checking the types at run-time adds some overhead (especially if it happens in a loop), and may be done multiple times for a single value, so it's not done by default.
(declaim (optimize (safety 3)))
(defun some-predicate-p (a)
(format t "~&Checking type...")
(integerp a))
(deftype foo () `(satisfies some-predicate-p))
(defclass bar ()
((foo :type foo :initarg :foo)))
(declaim (ftype (function (foo) list) qwerty))
(defun qwerty (foo)
(loop repeat 10 collecting (make-instance 'bar :foo foo)))
(qwerty 12)
; Checking type...
; Checking type...
; Checking type...
; Checking type...
; Checking type...
; Checking type...
; Checking type...
; Checking type...
; Checking type...
; Checking type...
; Checking type...
;=> (#<BAR {1003BCA213}> #<BAR {1003BCA263}> #<BAR {1003BCA2B3}>
; #<BAR {1003BCA303}> #<BAR {1003BCA353}> #<BAR {1003BCA3A3}>
; #<BAR {1003BCA3F3}> #<BAR {1003BCA443}> #<BAR {1003BCA493}>
; #<BAR {1003BCA4E3}>)
If you want a function to always check the type of a place, regardless of the optimization settings, you should use CHECK-TYPE
manually.
Why are non-primitive types declared in make-array's :element-type are promoted to t? Is there any possibility for compile-time or runtime checks of the real declared type?
The :element-type
parameter is there that an implementation can choose an optimized memory layout for an array - mostly for saving memory space. This is typically useful with primitive types. For other types most Common Lisp runtimes will have no optimized storage implementation and thus the declaration will have no useful effect.
Why are CLOS slot defined types don't work as constraints, allowing to put value of any type into the slot? Again, what about the checks?
An implementation may do that.
Clozure CL:
? (defclass foo () ((bar :type integer :initform 0 :initarg :bar)))
#<STANDARD-CLASS FOO>
? (make-instance 'foo :bar "baz")
> Error: The value "baz", derived from the initarg :BAR,
can not be used to set the value of the slot BAR in
#<FOO #x302000D3EC3D>, because it is not of type INTEGER.
The same for the functions' types declarations with declare.. Are they just the optimization hints to the compiler?
The type declarations with declare can be ignored - for example in Symbolics Genera most declarations will be ignored. Implementations are not required to process them. Most implementations will at least interpret them as assurance that some object will be of that type and create optimized code for that - possibly without runtime checks and/or specialized code for that type. But it's usually necessary to set corresponding optimization levels (speed, safety, debug, ...)
Additionally compilers derived from CMUCL's compiler (SBCL, ...) may use them for some compile time checks.
But none of the effects is specified in the ANSI CL standard. The standard provides the declarations and leaves the interpretation to the implementations.
How types are handled during compilation is defined by implementations. In the case of SBCL, types are generally treated as assertions, but the actual behavior depends on optimisation levels.
Types as assertions means that if a function takes a number n
and produces a string s
, you generally don't assume that n
is a number. Instead, what you have is a guarantee that if the function returns, then n
effectively was a number and s
is now a string. But if you reuse s
, your compiler has an opportunity to skip a check for s
being a string. This is generally what you want because your functions are available globally and can thus be called from anywhere. Since functions are responsible for checking their inputs, it is normal that you always check for n
being a number first.
Yet, type declarations for functions can help you in case you call a function in a context where it can be proved that types will certainly mismatch at runtime (the intersection of types is empty). In order to trust type assertions blindly, you have to lower the safety levels.
Note: I originally posted it at a comment, but in order to avoid it being deleted, here is a link to a nice graphics representing relationships between types in CL:
http://sellout.github.io/2012/03/03/common-lisp-type-hierarchy
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With