common-lisp

How do I define my type based on class so I can use that type in typecase and related expressions?


Consider this:

(defclass my-string ()
  ((data :initarg :data :type simple-string)
   (properties :initarg :properties :type interval-tree))
  (:documentation
   "Represents a string with properties."))

I would like to use my-string as a type in a typecase expression:

(defun upcase (obj)
  (typecase obj
    (my-string (string-upcase (slot-value obj 'data)))
    (string (string-upcase obj))
    (integer (char-upcase (code-char obj)))
    (character (char-upcase obj))
    (otherwise (error "Wrong type argument ~S" obj))))

I thought that classes are types, but obviously not, since the above is a compiler error. So I declared a type:

(deftype my-string (s) (typep s 'my-string))

which it seems I have to use like this (otherwise I get compilation error that typedef expects an argument):

(defun upcase (obj)
  (typecase obj
    ((my-string obj) (string-upcase (slot-value obj 'data)))
    (string (string-upcase obj))
    (integer (char-upcase (code-char obj)))
    (character (char-upcase obj))
    (otherwise (error "Wrong type argument ~S" obj))))

However SBCL deletes my code as unreachable! :-)

How do I do this? How to properly declare a type for a class so I can use it in expressions where type declarations are usable?

I understand I can use struct instead of class, in which case the compiler generates code for the type, and the initial typecase "just works":

(defstruct my-string
  data properties)

Macroexpanding and looking at the generated code didn't really make more enlightened. I see this:

(SB-C:XDEFUN MY-STRING-P
     :PREDICATE
     NIL
     (SB-KERNEL::OBJECT)
   (TYPEP SB-KERNEL::OBJECT 'MY-STRING))

which does exactly what I did, but I don't think that function is in the play, or am I wrong? Either they somehow associate structs with types internally, or I have missed some relevant part of the generated code? I am not very familiar with all this, so it is very likely as well.

A related question: what is the proper way to model a custom class that extends a built-in type like string in CommonLisp? Is it a better way to do the composition, as done in my-string, or is it perhaps better to inherit from some built-in CL string class? Based on this SX question and answers, it seems to me that inheritance is not the best way to model types, which is probably for the better, since we don't like taxonomies as known from Java or old C++.

Finally, I wouldn't even need typecase in this case if I made upcase a generic method that specialize on those types, the entire problem goes away, which I am fully aware of. However, I would like to learn more about types and classes, and how to model something like the above in CL.

Sorry if it is a bit long, and too many questions baked into one, I am just learning this. It feels like every question I have and experimenting for an answer opens 10 more questions :).


Solution

  • You are using deftype in a completely wrong way, so even though there is a better answer I'd like to focus on the part that was errorneous.

    When you write (deftype u (s) (typep s 'u)), the first time you have no warning, and later you have an warning during compilation if you try again (with SBCL), because u in (typep s 'u) doesn't have the expected number of arguments, defined by (deftype u (s) ...). This can be explained once we understand what deftype does.

    TYPEP

    First, in order for the (typep ...) call to make sense, you would need to write something like (typep v '(u s)) where v is a value, and (u s) is a type, parameterized by argument s. For example, (integer -2 -2) is such a type, it is a subset of integer with inclusive bounds:

    (loop for i from -5 to 5 collect (list i (typep i '(integer -2 2))))
    => ((-5 NIL) (-4 NIL) (-3 NIL)
        (-2 T) (-1 T) (0 T) (1 T) (2 T) 
        (3 NIL) (4 NIL) (5 NIL))
    

    The values with a T are the one when typep succeeds.

    Type specifiers

    Notice that the type above, the second argument, is a symbolic term. It's a an expression in a mini-language of types. In this language you have standard atomic type specifiers, like string, class, number, and then you can compose them as compound type specifiers, like the standard ones (and ...), (or ...), etc. Finally, you can define your own type specifiers using deftype, and then the defined type can be used as a second argument to typep.

    Note also that this notion of typing is quite different from Hindley–Milner type systems that are popular in other functional programing languages: you cannot define recursive types (like lists of integers), the type (or (or a b) c) is equivalent to (or a b c), etc. This is just not the same approach, that's something to remember when dealing with types.

    DEFTYPE

    Typically deftype is useful in your program when you want to give names to types you will use a lot, and also maybe to do a bit of abstraction over types. For example, writing octet is shorter than always using (unsigned-byte 8), so here we can define the type as follows:

    (deftype octet () '(unsigned-byte 8))
    

    Another example is when you want to constrain the underlying type, for example here a square matrix necessarily has two dimensions with the same size; the type here is a type specifier so that you can talk about square matrices of floats or integers, etc.:

    (deftype square-matrix (size type)
      `(array ,type (,size ,size)))
    

    With the above definition, here is how you would check if an array is actually a square matrix of integers:

    (typep #2A((1 1) (2 2)) '(square-matrix 2 integer))
    

    In conclusion, square-matrix is a function producing a type specifier, and that's what deftype is about.

    Your my-string functions returns a generalized boolean (the result of typep), and the argument you wanted to give was a value (is s of type my-string), whereas s in a deftype argument is supposed to be used to express a type parameter, because you may have different variations of my-string (e.g. an explicit size parameter for your strings).

    DEFCLASS

    Going back to your original problem, the following works in a fresh environment:

    (defclass foo () ())
    
    (defun bar (v)
      (typecase v
        (string "a string")
        (foo "a foo")))