Using (declare (type ...)) but still have 'safe' functions
Asked Answered
I

2

6

Is it possible to use (declare (type ...)) declarations in functions but also perform type-checking on the function arguments, in order to produce faster but still safe code?

For instance,

(defun add (x y)
    (declare (type fixnum x y))
    (the fixnum x y))

when called as (add 1 "a") would result in undefined behaviour, so preferably I'd like to modify it as

(defun add (x y)
    (declare (type fixnum x y))
    (check-type x fixnum)
    (check-type y fixnum)
    (the fixnum x y))

but I worry that the compiler is allowed to assume that the check-type always passes and thus omit the check.

So my question is, is the above example wrong as I expect it, and secondly, is there any common idiom* in use to achieve type-safety with optimised code?

*) I can imagine, for instance, using an optimised lambda and calling that after doing the type-checking, but I wonder if that's the most elegant way.

Ina answered 31/8, 2015 at 22:48 Comment(0)
W
8

You can always check the types first and then enter optimized code:

(defun foo (x)
  (check-type x fixnum)
  (locally
    (declare (fixnum x)
             (optimize (safety 0)))
    x))

The LOCALLY is used for local declarations.

Waldenses answered 1/9, 2015 at 7:4 Comment(0)
D
0

Since you are asking:

Is it possible to use (declare (type ...)) declarations in functions but also perform type-checking on the function arguments, in order to produce faster but still safe code?

it seems to me that you are missing an important point about the type system of Common Lisp, that is that the language is not (and cannot-be) statically typed. Let's clarify this aspect.

Programming languages can be roughly classified in three broad categories:

  1. with static type-checking: every expression or statement is checked for type-correctness at compile time, so that type errors can be detected during program development, and the code is more efficient since no check for types must be done at run-time;
  2. with dynamic type checking: every operation is checked at run time for type correctness, so that no type-error can occur at run-time;
  3. without type checking: type-errors can occur at run-time so that the program can stop for error or have an undefined behaviour.

Edited

With the respect to the previous classification, the Common Lisp specification left to the implementations the burden of deciding if they want to follow the second or the third approach! Not only, but through the optimize declaration the specification lets the implementations free to change this dinamically, in the same program.

So, most implementations, at the initial optimization and safety levels, implements the second approach, enriched with the following two possibilities:

  1. one can request to the compiler to omit run time type-checking when compiling some piece of code, typically for efficiency reasons, so that, inside that particular piece of code, and depending on the optimization and safety settings, the language can behave like the languages of the third category: this could be supported by hints through type declarations, like (declare (type fixnum x)) for variables and (the fixnum (f x)) for values;

  2. one can insert into the code explicit type-checking tests to be performed at run-time, through check-type, so that an eventual difference in the type of the value checked will cause a “correctable error”.

Note, moreover, that different compilers can behave differently in checking types at compile times, but they can never reach the level of compilers for languages with static type checking because Common Lisp is a highly dynamical language. Consider for instance this simple case:

(defun plus1(x) (1+ x))

(defun read-and-apply-plus1()
    (plus1 (read)))

in which no static type-checking can be done for the call (plus1 (read)), since the type of (read) is not known at compile time.

Dunker answered 1/9, 2015 at 7:4 Comment(7)
I don't get the simple case you're bringing. How do statically-typed languages know the type of their (read) equivalent at compile-time?Lysimachus
Well, actually they can't. In this sense Common Lisp is “more dynamic” (someone would say “more powerful”). Some of them do not have any equivalent of read, while others can use a kind of dynamic type casting, canceling the benefits of static type checking.Dunker
I understand the type system of Common Lisp, but I worry that as soon as you start with compiler optimisations, then suddenly things might leave the serene realm of dynamic typing and enter the nitty-gritty of machine language. My concern is exactly when the behaviour goes to your point 3) via (declare (type ...)) that my example code may exhibit undefined behaviour.Ina
I think this is left to the compiler, and for my experience different compilers behave in different ways. You could write both functions and use (disassemble 'add) to see if you can discern any difference between them. I suppose that different compilers will behave differently, producing similar or different code for the two versions.Dunker
This post, although informative, is mostly irrelevant as an answer, and it's a bit wrong. The standard doesn't require a Common Lisp implementation to check for type errors except in very few cases, thus making it belong to the third category. For instance, the function + might signal type-error, but it doesn't have to and it may result in undefined behavior, as-is, without type declarations.Allonym
@PauloMadeira, thank you for point out this, I've updated the answer.Dunker
Nitpick: the type of (read) is T, and that is already an interesting information (the result is a valid object at runtime). Functions using the returned value cannot assume anything more specific, so they have to be generic and perform dynamic dispatch. You can still perform static analysis: (plus1 (read)) is necessarily a number, because we know the dynamic checks of the generic 1+ will filter bad inputs and signal errors (when safety checks are not disabled).Hua

© 2022 - 2024 — McMap. All rights reserved.