Let's take an example :
(defun hello (a b)
(declare (ftype (function (integer list) t) hello))
(format t "Hello World"))
I would like to have a SIMPLE-TYPE-ERROR raised when I call it like this :
> (hello 'a 12)
Hello World
But it gives me no error.
I've read that one should use CHECK-TYPE to check types at runtime, like this :
(defun hello (a b)
(declare (ftype (function (integer list) t) hello))
(check-type a integer)
(check-type b list)
(format t "Hello World"))
> (hello 'a 12)
The value of A is A, which is not of type INTEGER.
[Condition of type SIMPLE-TYPE-ERROR]
But what's the point to (declare (ftype...) if it's not enforced at runtime and that I must add all those CHECK-TYPE forms ?
What's the best and idiomatic way of type checking parameters passed to a function and also type checking the returned value ?
There are two parts to this answer.
Firstly the ftype
declaration you have does not do what you think, unfortunately. From 3.3.4:
A free declaration in a form F1 that applies to a binding for a name N established by some form F2 of which F1 is a subform affects only references to N within F1; it does not to apply to other references to N outside of F1, nor does it affect the manner in which the binding of N by F2 is established.
In other words in a form like
(defun foo (...)
(declare (ftype foo ...))
...
The declaration applies only to references to foo
within the body of foo
: it does not apply elsewhere. So it tells (or may tell) the compiler things about possible recursive calls to foo
, only.
So if you want to make a global proclamation about a function you need to say
(declaim (ftype foo ...))
...
(defun foo (...)
...)
Secondly how declarations are interpreted is notoriously compiler-specific, in particular whether they are interpreted as assertions about the program which must be checked, or as declarations that something is the case which the compiler may assume. CMUCL and implementations derived from it (notably SBCL today) take the former interpretation (at least with high safety settings, I think), all other implementations I know of take the latter.
As an example of this if I write
(declaim (ftype (function (integer integer)
(values integer integer))
foo))
(defun foo (a b)
(values a b))
Then SBCL will say:
> (describe 'foo)
common-lisp-user::foo
[symbol]
foo names a compiled function:
Lambda-list: (a b)
Declared type: (function (integer integer)
(values integer integer &rest t))
Derived type: (function (integer integer)
(values integer integer &optional))
and (foo 'a 'b)
will signal an error.
However there's just no promise that that will happen in portable code at all.
Probably a better approach for portable code is
defun/checked
macro which turns those declarations into explicit checks in some cases.There is still a slight problem in that systems which take the traditional 'declarations are things the compiler can trust' approach may take code which, after macroexpansion, ends up looking like
(defun foo (a)
(declare (type fixnum a))
(check-type a fixnum)
...)
and simply eliding the type check, since they are trusting what you told the compiler. So you may need to have some settings for defun/checked
where it simply elides the type declarations altogether so that the checks do what they are intended to.
I don't think this is the right place to put such a defun/checked
macro but it's fairly easy to write one.