For Development HEAD DRAFTSearch (procedure/syntax/module):

6.1 Types and classes

Scheme is a dynamically and strongly typed language. That is, every value knows its type at run-time, and the type determines what kind of operations can be applied on the value.

The Scheme standard is pretty simple on types; basically, an object being a type means the type predicate returns true on the object, and that’s all. Gauche adopts a bit more elaborated system—types are first-class objects and you can query various information.

In Gauche, types are conventionally named with brackets < and >, e.g. <string>. It’s nothing syntactically special with these brackets; they’re valid characters to consist of variable names.


6.1.1 Prescriptive and descriptive types

Types are used in two ways. A type can be seen as a template of the actual values (instances)—that is, a type prescribes the structure of, and possible operations on, its instances. In Gauche, such prescriptive types are represented by classes. Every value in Gauche belongs to a class, which can be queried with the class-of procedure. You can also define your own class with define-class or define-record-type (see Object system, and see gauche.record - Record types).

A type can also be seen as a constraint of a given expression—that is, a type describes what characteristics a certain expression must have. Gauche has entities to represent such descriptive types separate from classes. Descriptive types are created with type constructors (see Type expressions and type constructors). We also have a set of predefined descriptive types to communicate to C libraries (see Native types.

A value is an instance of a class. For example, 1 is an instance of <integer>, and "xyz" is an instance of <string>. This presctiptive type relationship is checked with is-a? procedure: (is-a? 1 <integer>) and (is-a? "xyz" <string>) both returns #t.

On the other hand, you may have a procedure that takes either a number or a string as an argument. “A number or a string” is a type constraint, and can be expressed as a descriptive type, (</> <number> <string>). Descriptive type relationship is checked with of-type? procedure: (of-type? 1 (</> <number> <string>)) and (of-type? "xyz" (</> <number> <string>)) both returns #t. More conveniently, (assume-type arg (</> <number> <string>)) would raise an error if arg doesn’t satisfy the given type constraint.


6.1.2 Generic type predicates

A “type predicate” is a predicate that tells if an object is of a specific type; e.g. number? tells you if the argument is a number. Since types are first-class in Gauche, we have predicates that can tell an object is of a given type, as well as predicates to ask the relationship between types.

Function: is-a? obj class

This is a prescriptive types predicate. Returns true iff obj is an instance of class or an instance of descendants of class.

(is-a? 3 <integer>)   ⇒ #t
(is-a? 3 <real>)      ⇒ #t
(is-a? 5+3i <real>)   ⇒ #f

Note: If obj’s class has been redefined, is-a? also triggers instance update. See Class redefinition for the details.

Function: of-type? obj type

This is a descriptie type predicate. Returns true iff obj satisfies the constraints described by type, which can be either a class (in that case, this is the same as is-a?), or a descriptive type (see Type expressions and type constructors below).

(of-type? 1   (</> <number> <string>)) ⇒ #t
(of-type? "a" (</> <number> <string>)) ⇒ #t
(of-type? 'a  (</> <number> <string>)) ⇒ #f
Function: subtype? sub super

Returns #t if a type sub is a subtype of a type super (includes the case that sub is super). Otherwise, returns #f.

In general, if sub is a subtype of super, an object that satisfies the constraints of sub also satisfies the constraints of super, so you can use the object where objects of type super are expected. In other words, sub is more restrictive than super. If both sub and super are classes, sub being a subtype of super means sub is a subclass of super.

(subtype? <integer> <real>) ⇒ #t
(subtype? <char> (</> <char> <string>)) ⇒ #t
(subtype? (</> <integer> <string>) (</> <number> <string>)) ⇒ #t

Note that we’re not rigorous on this “substitution principle”, for we don’t aim at guaranteeing type safety through static analysis. For example, “list of integers” can be used in place of a generic list most of the time, and (subtype? (<List> <integer>) <list>) is #t. However, if list is mutated, you can’t replace generic list with a list of integers—because mutators may try to set non-integer in the list. That kind of cases needs to be handled separately, not on relying solely on subtype?.

Function: type? obj

Returns #t if obj is a type (either a class or a descriptive type), #f otherwise.

You can pass objects that returns #t for this predicate to the second argument of of-type?, and the first or second argument of subtype?.

Function: subclass? sub super

Both arguments must be classes. Returns #t iff sub is a subclass of super. A class is regarded as a subclass of itself.


6.1.3 Predefined classes

We’ll introduce classes for each built-in type as we go through this chapter. Here are a few basic classes to start with:

Builtin Class: <top>

This class represents the supertype of all the types in Gauche. That is, for any class X, (subtype? X <top>) is #t, and for any object x, (is-a? x <top>) is #t.

Builtin Class: <bottom>

This class represents the subtype of all the types in Gauche. For any class X, (subtype? <bottom> X) is #t, and for any object x, (is-a? x <bottom>) is #f.

There’s no instance of <bottom>.

Note: Although <bottom> is subtype of other types, the class precedence list (CPL) of <bottom> only contains <bottom> and <top>. It’s because it isn’t always possible to calculate a linear list of all the types. Even if it is possible, it would be expensive to check and update the CPL of <bottom> every time a new class is defined or an existing class is redefined. Procedures subtype? and is-a? treat <bottom> specially.

One of use case of <bottom> is applicable? procedure. See Procedure class and applicability.

Builtin Class: <object>

This class represents a supertype of all user-defined classes.

Function: class-of obj

Returns a class metaobject of obj.

(class-of 3)         ⇒ #<class <integer>>
(class-of "foo")     ⇒ #<class <string>>
(class-of <integer>) ⇒ #<class <class>>

Note: In Gauche, you can redefine existing user-defined classes. If the new definition has different configuration of the instance, class-of on existing instance triggers instance updates; see Class redefinition for the details. Using current-class-of suppresses instance updates (see Accessing instance).


6.1.4 Type expressions and type constructors

Types are first-class objects manipulatable at runtime in Gauche, but they must be known at compile-time in order to do optimizations or static analysis. In certain places, Gauche requires the the value of type-yielding expressions to be statically computable. We call such expressions type expressions.

A type expression is either a global variable reference that are constantly bound to a type, or a call of type constructor:

<type-expression> : <global-variable-constantly-bound-to-a-type>
                  | <type-constructor-call>

<type-constructor-call> : (<type-constructor> <type-constructor-argument> ...)

<type-constructor-argument> : <type-expression>
                            | <integer-constant>
                            | ->
                            | *

All built-in classes, such as <integer>, are statically bound (in precise terms, they are “inlinable” binding). If you try to alter it, a warning is issued, and the further bahvior will be undefined. In future, we’ll make it an error to alter the binding of global variables bount to types. Classes defined with define-class and define-record-type are also bound as inlinable.

Type constructors are special classes whose instances are descriptive types. Type constructors can be invoked as if they are a procedure, but <type-constructor-call> above is recognized by the compiler and the derived type is computed at compile-time. So, at runtime you only see the resulting derived type instance.

For example, <?> is a type constructor that creates “maybe”-like type, e.g. “an integer or #f”. (Do not confuse this with Maybe type defined in SRFI-189, see srfi.189 - Maybe and Either: optional container types). A type expression (<?> <integer>) yields such type (printed as #<? <integer>>):

(<?> <integer>) ⇒ #<? <integer>>

It looks like a procedure call, but it s computed at compile time:

gosh> (disasm (^[] (<?> <integer>)))
CLOSURE #<closure (#f)>
=== main_code (name=#f, cc=0x7f5cd76ab540, codevec=...):
signatureInfo: ((#f))
     0 CONST-RET #<? <integer>>
Type Constructor: <?> type

Type must be a type expression. This creates a maybe-like type, that is, the object is either of type or #f. Usually, #f indicates that the value is invalid (e.g. the return value of assoc).

This type has ambiguity when #f can be a meaningful value, but traditionally been used a lot in Scheme, for it is lightweight. We also have a “proper” Maybe type in SRFI-189 (see srfi.189 - Maybe and Either: optional container types) but that involves extra allocation to wrap the value.

So, those who came from functional programming may find this isn’t a right abstraction, but it has its place in the Scheme world.

Type Constructor: </> type …

Type … must be type expressions. This creates a sum type of type …. For example, (</> <string> <symbol>) is a type that is either a string or a symbol.

Type Constructor: <Tuple> type …

Type … must be type expressions, except that the last argument that may be an identifier *. This creates a product type of type ….

Actually, this type is looser than the product types in typical statically-typed languages. Our tuple is a subtype of list, with types of each positional element are restricted with type …. For example, (<Tuple> <integer> <string>) is a list of two elements, the first one being an integer and the second being a string.

(of-type? '(3 "abc") (<Tuple> <integer> <string>)) ⇒ #t

If you need a more strict and disjoint product type, you can just create a class or a record.

If the last argument is *, the resulting type allows extra elements after the typed elements.

(of-type? '(3 "abc" 1 2) (<Tuple> <integer> <string> *)) ⇒ #t
Type Constructor: <^> type … -> type …

Each type must be type expressions, except that the one right before -> and the last one may be an identifier *.

This creates a procedure type, with the constraints in arguments and return types. The type … before -> are the argument types, and the ones after -> are the result types.

The * in the last of argument type list and/or result type list indicates extra elements are allowed.

In vanilla Scheme, all procedures belong to just one type, that responds true to procedure?. It is simple and flexible, but sometimes the resolution is too coarse to do reasoning on the program.

In Gauche, we can attach more detailed type information in procedures. In the current version, some built-in procedures already have such type information, that can be retrieved with procedure-type:

(procedure-type cons) ⇒ #<^ <top> <top> -> <pair>>

Not all procedures have such information, though. Do not expect the rigorousness of statically typed languages.

At this moment, Scheme-defined procedures treats all argument types as <top>. We’ll provide a way to attach type info in future.

Type Constructor: <List> type :optional min-length max-length
Type Constructor: <Vector> type :optional min-length max-length

Type must be a type expression.

These create a list or a vector type whose elements are of type, respectively. For example, (<List> <string>) is a list of strings.

The optional arguments must be a literal real numbers that limit the minimum and maximum (inclusive) length of the list or the vector. When omitted, min-length is zero and max-length is inf.0.

Note: subtype? uses covariant subtyping, that is, (subtype? (<List> <string>) <list>) is #t.

Type Constructor: <Assortment> constant …

Constants must be an expression that evaluates to a symbol, a boolean or a number, at the compile time. This type expression creates a type whose instances are limited to the given constant values.

(define (p obj)
  (of-type? obj (<Assortment> 'one 'two 'three)))

(p 'one)   ⇒ #t
(p 'three) ⇒ #t
(p 'four)  ⇒ #f

Mere membership test can be realized simply with a list or a set, but the construction of assortment type is done in compile-time, and potentially creates opportunities for optimizations or compile-time checks. For example, you can proclaim the argument is only allowed with certain set of symbols:

(define (p obj)
  (assume-type obj (<Assortment> 'one 'two 'three))
  ...)

Currently, it is only used by a runtime check. However, in futuer version of Gauche, the compiler knows the argument obj must be one of these symbols, and may use that information for static analysis.


6.1.5 Native types

Native types are a set of predefined descriptive types that bridge Scheme types and C types. They can be used to access binary blobs passed from a foreign functions, for example. They’ll also be used for FFI.

An example of native types is <int16>, which corresponds to C’s int16_t. You can use <int16> to check if a Scheme object can be treated as that type:

(of-type? 357 <int16>)   ⇒ #t
(of-type? 50000 <int16>) ⇒ #f

Or obtain size and alignment info on the running platform:

gosh> (describe <int16>)
#<native-type <int16>> is an instance of class <native-type>
slots:
  name      : <int16>
  super     : #<class <integer>>
  c-type-name: "int16_t"
  size      : 2
  alignment : 2
Native type: <fixnum>
Native type: <int>
Native type: <int8>
Native type: <int16>
Native type: <int32>
Native type: <int64>
Native type: <short>
Native type: <long>
Native type: <uint>
Native type: <uint8>
Native type: <uint16>
Native type: <uint32>
Native type: <uint64>
Native type: <ushort>
Native type: <ulong>
Native type: <float>
Native type: <double>
Native type: <size_t>
Native type: <ssize_t>
Native type: <ptrdiff_t>
Native type: <off_t>
Native type: <void>

Predefined native types. The correspondence of Scheme and C types are shown below:

Native type  Scheme       C            Notes
-----------------------------------------------------------------
<fixnum>     <integer>    ScmSmallInt  Integers within fixnum range

<int>        <integer>    int          Integers representable in C
<int8>       <integer>    int8_t
<int16>      <integer>    int16_t
<int32>      <integer>    int32_t
<int64>      <integer>    int64_t
<short>      <integer>    short
<long>       <integer>    long
<uint>       <integer>    uint         Integers representable in C
<uint8>      <integer>    uint8_t
<uint16>     <integer>    uint16_t
<uint32>     <integer>    uint32_t
<uint6r>     <integer>    uint64_t
<ushort>     <integer>    ushort
<ulong>      <integer>    ulong
<float>      <real>       float        Unboxed value casted to float
<double>     <real>       double

<size_t>     <integer>    size_t       System-dependent types
<ssize_t>    <integer>    ssize_t
<ptrdiff_t>  <integer>    ptrdiff_t
<off_t>      <integer>    off_t

<void>       -            void        (Used only as a return type.
                                        Scheme function returns #<undef>)


For Development HEAD DRAFTSearch (procedure/syntax/module):
DRAFT