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.
• Prescriptive and descriptive types: | ||
• Generic type predicates: | ||
• Predefined classes: | ||
• Type expressions and type constructors: | ||
• Native 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.
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.
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.
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
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?
.
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?
.
Both arguments must be classes. Returns #t
iff sub is
a subclass of super. A class is regarded as a subclass of
itself.
We’ll introduce classes for each built-in type as we go through this chapter. Here are a few basic classes to start with:
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
.
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.
This class represents a supertype of all user-defined classes.
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).
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 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 … 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 … 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 … ¶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 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
.
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.
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
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>)