Common Lisp offers such type testing at run time. It has an elaborate type system, but it's not used as you might be accustomed to in a statically-typed language. The macro check-type
accepts a typespec, which can be a built-in specification or one defined by the macro deftype
. The constraints expressible with typespecs are those of a predicate function written in the host language, which is to say that anything you can inspect a run time can be the criteria for what constitutes your new type.
Consider this example:
(defun is-nothing (val)
(when (stringp val)
(string= val "nothing")))
(deftype strange-range ()
"A number between 0 and 100 inclusive, or the string \"nothing\"."
'(or (integer 0 100)
(satisfies is-nothing)))
That defines a type called "strange-range". Now test a few values against it:
CL-USER> (let ((n 0))
(check-type n strange-range))
NIL
CL-USER> (let ((n 100))
(check-type n strange-range))
NIL
CL-USER> (let ((n "nothing"))
(check-type n strange-range))
NIL
CL-USER> (let ((n 101))
(check-type n strange-range))
The last one triggers the debugger with the following message:
The value of N should be of type STRANGE-RANGE.
The value is: 101
[Condition of type SIMPLE-TYPE-ERROR]
This one provokes the same outcome:
CL-USER> (let ((n "something"))
(check-type n strange-range))
The constraints one can impose this way are expressive, but they don't serve the same purpose that the elaborate type systems of languages like Haskell or Scala do. While type definitions can coax the Common Lisp compiler to emit code more tailored to and efficient for the types of the operands, the examples above are more of a concise way to write run-time type checks.