Search code examples
lispcommon-lisp

How to check if two types are the same in Common Lisp?


I'm learning Common Lisp and is now trying to replicate C++'s (boost's) lexical_cast<bool>(string). But can't find a way to compare types in CL.

The C++ version looks like this

template <typename ret_type>
ret_type lexical_cast(const std::string& val)
{
    if constexpr (std::is_same_v<ret_type, bool>)
        return val == "true";
    // Otherwise don't care
}

So far I came up with the following Lisp code using equal to compare types. I assume this would work as Lisp is indifferent of data and code.

(defun lexical_cast(ret_type val)
    (if (equal ret_type #'boolean) (equalp val "true"))
    ; Otherwise don't case
)

But SBCL generates error:

; in: DEFUN LEXICAL_CAST
;     #'BOOLEAN
; 
; caught WARNING:
;   The function BOOLEAN is undefined, and its name is reserved by ANSI CL so that
;   even if it were defined later, the code doing so would not be portable.
; 
; compilation unit finished
;   Undefined function:
;     BOOLEAN
;   caught 1 WARNING condition

Nor typep helps as it compares the type of a object against another type. Not directly comparing them. I've search through Lisp tutorials and manuals without success. How should I do this?


Solution

  • Common Lisp defines SUBTYPEP to compare types. Using that, you can define a type equality comparison function:

    (defun type-equal (t1 t2)
      (and (subtypep t1 t2)
           (subtypep t2 t1)))
    

    For example, there are different way to encode the boolean type, but they cover the same set of values so they are equal:

    (type-equal '(member t nil) '(or (eql t) (eql nil)))
    => T, T
    

    There is also a COERCE functions that in a limited way allows to convert a value to another type. This is however not extensible.

    Simple cast

    Let's define first a new condition for cases where we don't know how to cast a value to a target type. To keep things simple, this error has no associated data:

    (define-condition cast-error (error) ())
    

    Then, a rigid but simple definition for lexical-cast can be:

    (defun lexical-cast (return-type value)
      (coerce (cond
                ((type-equal return-type 'boolean)
                 (typecase value
                   (string (string= value "true"))
                   (real (/= value 0))
                   (t (error 'cast-error))))
                ((subtypep return-type 'real)
                 (typecase value
                   (string (read-from-string value))
                   (boolean (if value 1 0))
                   (t (error 'cast-error))))
                (t
                 (error 'cast-error)))
              return-type))
    

    I wrap the cond form in a coerce form to check the inner result is of the appropriate return-type. This helps detect implementation errors.

    The inner form performs multiple-dispatch on the return type (cond) and on the value's type (typecases). You can for example convert from a string to a boolean:

    (lexical-cast '(member t nil) "true")
    T
    

    Assuming we follow the C convention of 0 being false, here is a cast from a number (notice that the boolean type is expressed differently):

    (lexical-cast '(or (eql t) (eql nil)) 0)
    NIL
    

    Note also that Common Lisp allows you to define ranges. Here we have an error because reading from the string gives a value outside of the excepted range (careful: reading from a string can be unsafe for some inputs, but this is out of scope here).

    (lexical-cast '(integer 0 10) "50")
    ;; error from COERCE
    
    (lexical-cast '(integer 0 10) "5")
    5
    

    Here is another example, casting to a subtype of reals from a boolean:

    (lexical-cast 'bit t)
    1
    

    Extensible cast

    In order to have a generic extensible lexical cast, like PPRINT-DISPATCH, you would need to maintain of table of conversion functions for different types, while ensuring you always select the closest type matching the return-type.

    A very simplified way to do that would be to define individual generic conversion functions for some dedicated return types:

    ;; can be extended for other classes of values
    (defgeneric cast-to-boolean (value)
      (:method ((v symbol))  v)
      (:method ((v integer)) (eql v 1))
      (:method ((v string))  (string= v "true")))
    

    Then, you maintain a mapping from types to conversion functions:

    ;; usually this is hidden behind user-friendly macros
    
    (defvar *type-cast* nil)  
    
    ;; this could be written instead (set-cast 'boolean #'cast-to-boolean)
    ;; for example, but here we just set the list directly.
    
    (setf *type-cast*
          (acons 'boolean #'cast-to-boolean *type-cast*))
    

    Then, you have a single cast function that looks up the first type that is equal to the return-type, and call the associated function:

    (defun gen-type-cast (return-type value)
      (let ((f (assoc return-type *type-cast* :test #'type-equal)))
        (if f (funcall (cdr f) value) (error 'cast-error))))
    

    For example:

    (gen-type-cast '(member t nil) "true")
    T
    

    See also

    The lisp-types system can be helpful to analyze and simplify type expressions.