Schemeは動的な強い型付けの言語です。つまり、全ての値は 実行時に自分の型を知っていて、その型が値に適用できる操作を決定します。
Scheme標準は、型についてはとても単純です。基本的に、オブジェクトがある型であるというのは、 そのオブジェクトに対して型述語が真を返す、それだけです。 Gaucheはもう少し込み入ったシステムを採用していて、型も第一級のオブジェクトになり 様々な情報を問い合わせることができます。
Gaucheでは慣習的に、型は<string>
のように
<
と>
で囲まれた名前を持ちます。
この<
や>
は文法的には何ら特別な意味を持ちません。
普通に変数名として使える文字です。
• 規範的な型と記述的な型: | ||
• 汎用型述語: | ||
• 定義済みクラス: | ||
• 型の式と型コンストラクタ: | ||
• ネイティブタイプ: |
型には2つの使われ方があります。まず、型は実際の値(インスタンス)のテンプレートと
見ることができます。つまり、型がインスタンスの構造と、それに適用できる操作を
あらかじめ決めている(prescribe)ということです。
Gaucheでは、このような規範的(prescriptive)な型は、クラスで表現されます。
Gauche内での値は全て何かのクラスに属していて、
class-of
手続きでそのクラスを取れます。また、ユーザは独自のクラスを
define-class
やdefine-record-type
で定義することもできます
(オブジェクトシステムおよびgauche.record
- レコード型参照)。
型はまた、与えられた式についての制約と見ることもできます。 つまり、型がその式が持つべき特定の性質を記述する(describe)ということです。 Gaucheでは、クラスとは別に、そういった記述的(descriptive)な型を表現するエンティティを 用意しています。記述的な型は型コンストラクタで作ることができます (型の式と型コンストラクタ参照)。 また、Cライブラリとやりとりするための記述的な型も用意されています (ネイティブタイプ参照)。
値は、特定のクラスのインスタンスです。
例えば1
は<integer>
のインスタンスであり、
"xyz"
は<string>
のインスタンスです。
このprescriptiveな型の関係は、手続きis-a?
で検査できます。
(is-a? 1 <integer>)
、(is-a? "xyz" <string>)
は
どちらも#t
を返します。
一方、例えば手続きが数値か文字列を引数に取る、といったケースもあります。
「数値か文字列」というのは型に関する制約であり、descriptiveな型
(</> <number> <string>)
として表現できます。
descriptiveな型の関係は手続きof-type?
で検査できます。
(of-type? 1 (</> <number> <string>))
、
(of-type? "xyz" (</> <number> <string>))
はどちらも#t
を返します。
また、(assume-type arg (</> <number> <string>))
は
argが与えられた型の制約を満たさなければエラーを投げます。
「型述語」は、与えられたオブジェクトが特定の型を持つかどうかを判定する述語です。
例えばnumber?
は引数が数値かどうかを返します。
Gaucheでは型がファーストクラスなので、もう一歩進めて、
オブジェクトが与えられた型を持つかどうかを判定する述語や、
型同士の関係を調べる述語が提供されています。
これは規範的な型に関する述語です。 objがclassのインスタンスであるか、classのサブクラスの インスタンスである場合に、真を返します。
(is-a? 3 <integer>) ⇒ #t (is-a? 3 <real>) ⇒ #t (is-a? 5+3i <real>) ⇒ #f
註:objのクラスが再定義されていた場合、is-a?
はインスタンスアップデートを
トリガします。クラスの再定義を参照してください。
これは記述的な型を調べる述語です。
objがtypeで記述される制約を満たすなら#t
を、そうでなければ#f
を
返します。typeはクラスか(その場合はis-a?
と同じです)、
記述型 (see 型の式と型コンストラクタ参照) です。
(of-type? 1 (</> <number> <string>)) ⇒ #t (of-type? "a" (</> <number> <string>)) ⇒ #t (of-type? 'a (</> <number> <string>)) ⇒ #f
型subが型superのサブタイプであれば(subとsuperが
同じである場合も含め)#t
を、そうでなければ#f
を返します。
一般的に、subがsuperのサブタイプであれば、 subの制約を満たすオブジェクトはsuperが期待されている箇所で使うことが できます。言い換えれば、subの制約の方がsuperよりきついということです。 subとsuperのどちらもがクラスの場合、subがsuperの サブタイプであるということは、subがsuperのサブクラスであるということです。
(subtype? <integer> <real>) ⇒ #t (subtype? <char> (</> <char> <string>)) ⇒ #t (subtype? (</> <integer> <string>) (</> <number> <string>)) ⇒ #t
この「置換原則」は常に厳密に成り立つわけではないことに注意してください。
例えば「整数のリスト」は大抵の場合、ジェネリックなリストが期待されているところに
使えて、従って(subtype? (<List> <integer>) <list>)
も#t
を
返します。しかし、渡されたリストが変更される場合は、ジェネリックなリストを
整数のリストで置き換え可能とは限りません。コードはリストに整数でないものをセットしようとする
かもしれないからです。こういったケースはsubtype?
に頼るのではなく
個別に対応する必要があります。
objが型(クラスまたは記述型)であれば#t
を、
そうでなければ#f
を返します。
この述語が真を返すオブジェクトは、of-type?
の第二引数、
もしくはsubtype?
の引数に渡せます。
引数はどちらもクラスでなければなりません。
subがsuperのサブクラスであれば#t
を、そうでなければ#f
を
返します。クラスはそれ自身のサブクラスとみなされます。
組み込みの型に対応するクラスについては、この章の中で順に紹介してゆきます。 まずは次のいくつかのクラスから始めましょう。
全ての型のスーパータイプを表現するクラスです。
つまり、どんなクラスX
に対しても(subtype? X <top>)
は#t
であり、
どんなオブジェクトx
に対しても(is-a? x <top>)
は#t
です。
全ての型のサブタイプを表現するクラスです。
どんなクラスX
に対しても(subtype? <bottom> X)
は#t
であり、
どんなオブジェクトx
に対しても(is-a? x <bottom>)
は#f
です。
<bottom>
型のインスタンスは存在しません。
註: <bottom>
は全ての型のサブタイプですが、そのクラス順位リスト
(class precedence list, CPL)には<bottom>
と<top>
以外のクラスは含まれていません。全ての型を線形に並べることは常に可能である
とは限らず、まだそうであったとしても新たなクラスの定義や既存のクラスの
再定義のたびに<bottom>
のCPLを検査してアップデートすることは
高くつくでしょう。subtype?
やis-a?
といった手続きは
<bottom>
を特別扱いしています。
<bottom>
の使いどころのひとつは、applicable?
手続きです。
procedureクラスと適用可能性を参照のこと。
このクラスは、ユーザ定義されたクラスのスーパータイプを表現するクラスです。
objのクラスを返します。
(class-of 3) ⇒ #<class <integer>> (class-of "foo") ⇒ #<class <string>> (class-of <integer>) ⇒ #<class <class>>
註: Gaucheでは、ユーザ定義クラスを再定義することができます。
新たな定義でインスタンスの構造が変更された場合、以前のクラスから作られた
インスタンスにclass-of
を適用すると、インスタンスが新しいクラスに
適合するようにアップデートされます。詳しくはクラスの再定義を参照して
ください。インスタンスアップデートを避けるにはcurrent-class-of
を
使います(インスタンスへのアクセス参照)。
型はGaucheでは実行時に扱える第一級のオブジェクトですが、 静的解析のためにはコンパイル時に型がわかる必要があります。 なので、いくつかの場所では、型を生成する式の値が静的に計算可能であることが要求されます。 そのような式を「型の式 (type expression)」と呼びます。
型の式は、型に静的に束縛されたグローバル変数か、型コンストラクタの呼び出しです:
<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> | -> | *
<integer>
のような全ての組み込みクラスは静的に束縛されています
(正確には、「インライン可能な」束縛になっています)。
そのような束縛を変更しようとすると警告が出され、それ以降の動作は保証されません。
将来的には、型に静的に束縛された変数を変更するのはエラーにするつもりです。
define-class
やdefine-record-type
で定義されるクラスも、
インライン可能な束縛になります。
型コンストラクタはそれ自身がクラスで、 そのインスタンスが記述的な型オブジェクトになるものです。 型コンストラクタは構文的には手続き呼び出しのように使えますが、 上の<type-constructor-call>はコンパイラによって認識され、 コンパイル時に計算されて型オブジェクトになります。 なので、ランタイムには結果の型オブジェクトしか見えません。
例えば、<?>
は”maybe”のようなオプショナルな型を作る型コンストラクタです。
(<?> <integer>)
は「<integer>
か#f
」を意味します
(SRFI-189 (srfi.189
- MaybeとEither、オプショナルなコンテナ型参照) の
Maybe型と混同しないようにしてください)。
この型オブジェクトは #<? <integer>>
と表示されます。
(<?> <integer>) ⇒ #<? <integer>>
型コンストラクタの呼び出しは手続き呼び出しのように見えますが、コンパイル時に計算されます。
gosh> (disasm (^[] (<?> <integer>))) CLOSURE #<closure (#f)> === main_code (name=#f, cc=0x7f5cd76ab540, codevec=...): signatureInfo: ((#f)) 0 CONST-RET #<? <integer>>
typeは型の式でなければなりません。
この型の式はmaybe型に似た型を作ります。オブジェクトは型typeを持つか#f
です。
#f
は通常、それが無効な値であることを示します
(例: assoc
の戻り値)。
この型は、#f
が意味を担う場合に曖昧性があるのですが、
軽量であるゆえにSchemeでは伝統的に多用されてきました。
「正しい」MaybeについてはSRFI-189 (srfi.189
- MaybeとEither、オプショナルなコンテナ型参照)
により提供されているのですが、値をラップするごとに新たなアロケーションを必要とします。
関数型言語から来た人には、これは正しい抽象化には見えないかもしれませんが、 Scheme界の中ではそれなりに実用的な型です。
type … はそれぞれ型の式でなければなりません。
この型コンストラクタは、type … の直和型を作ります。
例えば(</> <string> <symbol>)
は文字列かシンボルであるような型を表します。
type …はそれぞれ型の式でなければなりません。
ただ、最後の要素にはシンボル*
も許されます。
この型コンストラクタは、type … の直積型を作ります。
実のところ、これは静的型付け言語の直積型よりも緩い型です。
Gaucheではタプルはリストのサブタイプで、リストの各位置の要素が
対応するtypeで制約されているものです。
例えば(<Tuple> <integer> <string>)
は2要素のリストで、
最初が整数、次が文字列であるようなものを表します。
(of-type? '(3 "abc") (<Tuple> <integer> <string>)) ⇒ #t
より厳密な、他の型とは別であるような直積型が必要なら、クラスやレコードを作ってください。
最後の要素が*
である場合、結果の型は、
型が指定された要素のあとに任意個の要素を許します。
(of-type? '(3 "abc" 1 2) (<Tuple> <integer> <string> *)) ⇒ #t
->
type … ¶type … はそれぞれ型の式でなければなりません。ただし、
->
の直前、および最後のtypeは識別子*
も許されます。
この型コンストラクタは引数および戻り値の型を制約する手続き型を作ります。
->
の前のtype … は引数の型を、
->
の後のは戻り値の型を指定します。
引数の型リストの最後に*
がある場合は省略可能な追加の引数を、
戻り値の型リストの最後に*
がある場合は追加の戻り値があることを示します。
素のSchemeでは、全ての手続きは(procedure?
が真を返すという意味で)
ひとつの型に属しています。これは簡単で柔軟ですが、
プログラムの解析をしたい場合には粗すぎる分類です。
Gaucheでは手続きオブジェクトがより詳しい型情報を持てます。
現在のバージョンでは、組み込み手続きの一部に既に詳しい型情報が付加されていて、
procedure-type
によって取り出すことができます。
(procedure-type cons) ⇒ #<^ <top> <top> -> <pair>>
ただ、全ての手続きにこの情報が付加されているわけではありません。 静的型付け言語のような厳密さを期待しないでください。
今のところ、Schemeで定義された手続きは全ての引数や戻り値を<top>
型として
扱います。いずれより詳しい型情報を指定できるようにする予定です。
typeは型の式でなければなりません。
各要素がtypeであるようなリストもしくはベクタの型を作ります。
例えば(<List> <string>)
は文字列のリストです。
省略可能引数はどちらも実数でなければならず、 それぞれリストやベクタの長さの最小値と最大値(両端含む)を指定します。 省略された場合、min-lengthは0、max-lengthは無限大です。
註: subtype?
は共変関係を見ます。つまり、
(subtype? (<List> <string>) <list>)
は#t
です。
constantsはコンパイル時にシンボル、真偽値、あるいは数値に評価される式でなければなりません。 この型の式は、与えられた定数のみをインスタンスとみなす記述型を作ります。
(define (p obj) (of-type? obj (<Assortment> 'one 'two 'three))) (p 'one) ⇒ #t (p 'three) ⇒ #t (p 'four) ⇒ #f
単なるメンバシップの検査ならリストやセットでもできますが、 assortment型の生成はコンパイル時に行われ、 最適化やコンパイル時検査の機会を増やします。 例えば、引数が特定のシンボルのいずれかしか取らないことを、次のような形で 宣言しておくことができるでしょう。
(define (p obj) (assume-type obj (<Assortment> 'one 'two 'three)) ...)
現在のところこれは実行時チェックに使われるだけですが、 将来のGaucheではコンパイラが、引数はこれらのシンボルしか許されないという情報を 静的解析に利用するかもしれません。
ネイティブタイプは、Schemeの型とCの型の橋渡しをするためにあらかじめ定義されている型です。 外部の関数から渡されたバイナリデータの中身にアクセスする時などに使えます。 将来的にはFFIにも使う予定です。
ネイティブタイプの一例は<int16>
です。これはCのint16_t
に相当します。
例えばCのint16_t
としてSchemeオブジェクトを渡せるかどうかを
次のとおり検査できます。
(of-type? 357 <int16>) ⇒ #t (of-type? 50000 <int16>) ⇒ #f
また、実行中のプラットフォームにおけるその型のデータの大きさやアラインメントを知ることもできます。
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
定義済みのネイティブタイプです。 Cの型との関係は次の表のとおりです。
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>)