型システムは以下の型を定義します:
- 原始型:
Int
,Number
,String
,Char
,Boolean
- 配列
- レコード
- 代数的データ型(タグ付き共用体)
- ユーザ定義型
- 関数
- 多相型
- 制約のある型
- 型シノニム(型同義語)
- 列
String
、Number
、Boolean
の各原始型は、JavaScriptのそれと対応しています。
Int
型は整数値を表現します。ランタイムではJavaScriptの普通の数字と同じですが、Int
値を受け取る演算子(+)
などは、常にInt
値を返すように、異なる定義になっています。
PureScriptの配列はランタイムでJavaScriptの配列と対応していますが、全要素が同じ型でなければなりません。Array
型は、引数を1つ取ることで、自分自身の型が何であるかを特定します。例えば、整数の配列は型Array Int
を持ち、文字列の配列は型Array String
を持ちます。
PureScriptのレコードはJavaScriptのオブジェクトに対応しています。これは0個以上の名前付きフィールドを持ち、各フィールドが型を持っています。例えば:{ name :: String, greet :: String -> String }
はJavaScriptオブジェクトの、String
型のname
と、String
型を取りString
型を返す関数greet
の2フィールドに正確に対応します。
代数的データ型は1つ以上のコンストラクタから構成されます。各コンストラクタは0個以上の引数を取ります。
代数的データ型はコンストラクタからのみ作成され、パターンマッチによって解体されます(さらに徹底的なパターンマッチの方法については後に提示します)。
例をお見せします:
data Foo = Foo | Bar String
runFoo :: Foo -> String
runFoo Foo = "It's a Foo"
runFoo (Bar s) = "It's a Bar. The string is " <> s
main = do
log (runFoo Foo)
log (runFoo (Bar "Test"))
上記の例で、Fooは2つのコンストラクタを持つ代数的データ型です。1つ目のコンストラクタFoo
は引数を取らず、2つ目のコンストラクタBar
はString型の引数を1つ取ります。
runFoo
は代数的データ型のコンストラクタを発見する例になっています。また、最後の2行でFoo
型の値を作成しています。
ユーザ定義型はdata
キーワードによって表現されるデータ型と似ていますが、1つの引数を取るコンストラクタを1つ持つように制限されています。ユーザ定義型はnewtype
キーワードによって表現されます。
newtype Percentage = Percentage Number
ユーザ定義型は、ランタイムではその基となったデータ型と同じです。例えば、Percentage
型の値は、ランタイムではただのJavaScriptの数値です。
ユーザ定義型は、型検査によって、その基となった型とは異なるものとして扱われます。例えば、Number
型を受け取る関数にPercentage
型を渡すことは、型検査によって拒絶されます。
ユーザ定義型には、自身にクラスのインスタンスを割り当てることができます。例えば、Percentage
はShow
インスタンスを持つことができます:
instance showPercentage :: Show Percentage where
show (Percentage n) = show n <> "%"
PureScriptの関数はJavaScriptのそれとほぼ対応しますが、確実に1つの引数を持つようになっています。
式は多相型を持つことができます:
identity x = x
identity
は多相型のforall t0. t0 -> t0
を持つ、と推論されます。これは、identity
が何の型にでもなれるt0
型の値を受け取り、同じ型の値を返すことを意味します。
以下のように型注釈をつけることもできます:
identity :: forall a. a -> a
identity x = x
多相性は型の抽象化に制限がなく、値は種に関して多相になるかもしれません。例えば、列やモナドなどです(種システムを参照してください。)。
例えば、以下の関数はレコードの2パラメータにアクセスします。
addProps o = o.foo + o.bar + 1
addProps
から推論された型は:
forall r. { foo :: Int, bar :: Int | r } -> Int
ここで、型変数r
は種# Type
を持ちます。この種は型の列を表現しています。これはどんな型の指定でもインスタンス化することができます。
別の言い方をすると、addProps
はfoo
とbar
という2つのプロパティに加え、どんなプロパティを持つレコードでもいいということです。
したがって、以下のアプリケーションがコンパイルできます:
addProps { foo: 1, bar: 2, baz: 3 }
addProps
はプロパティbaz
について言及していないにも関わらず、です。しかし、以下のようにするとコンパイルできません:
addProps { foo: 1 }
プロパティbar
が欠落しているからです。
forall
量化子を関数アローの左側や、レコード型のフィールド、データコンストラクタ、型シノニムに書くことも可能です。
ほとんどの場合、この機能を使うときには型注釈が必要になります。
私達が多相関数を他の関数の引数として渡すことができる例をお見せします:
poly :: (forall a. a -> a) -> Boolean
poly f = (f 0 < 1) == f true
多相関数の型引数がNumber
とBoolean
の両方にインスタンス化されることに注意してください。
poly
の引数は多相でなければなりません。例えば、以下のようにすると失敗します:
test = poly (\n -> n + 1)
スコーレム化された変数a
は必ずしもInt
になるわけではないからです。
型の列は、名前付きの型から成る重複可能で順不同な集合です。重複するラベルは順番に型がまとめられ、NonEmptyList
のようになります。これは概念的には、列は型レベルのMap Label (NonEmptyList Type)
であると考えられるということです。
列はType
種ではなく、k
種に対しての# k
種を持つので、値として存在することはできません。むしろ、列はレコード型やラベル付けされた別の型を定義するために型シグネチャとして使用されます。非順序型は便利です。
閉じた列を表現するためには、フィールドをカンマで区切ります。また、フィールドと型はコロン2つで区切ります:
( name :: String, age :: Number )
開いた列(新しいフィールドを追加するために他の行と統合する可能性がある)を表現するためには、特定の文字と行変数をパイプで区切ります:
( name :: String, age :: Number | r )
利便性のため、type
キーワードを用いて型の同義語を宣言することができます。型シノニムは型引数を取ることができますが、部分適用は許されていません。また、型シノニムは他の型の定義のためにも用いることができますが、再帰は許されていません。
例えば:
-- 2フィールドを持つレコードの別名を作成
type Foo = { foo :: Number, bar :: Number }
-- 2フィールドは数値なので、加算する
addFoo :: Foo -> Number
addFoo o = o.foo + o.bar
-- 同じ形を持つ多相レコードの別名を作成
type Bar a = { foo :: a, bar :: a }
-- FooはBar Numberと等しい
-- 任意のBarのフィールドを関数に適用
combineBar :: forall a b. (a -> a -> b) -> Bar a -> b
combineBar f o = f o.foo o.bar
-- 複雑な関数型の別名を作成
type Baz = Number -> Number -> Bar Number
-- この関数は引数を2つ取り、2倍した値をレコードに入れて返す
mkDoubledFoo :: Baz
mkDoubledFoo foo bar = { foo: 2.0*foo, bar: 2.0*bar }
-- 任意のFoo内の値を2倍するように前の関数を実装する
-- (Bar NumberがFooと同じであることを思い出してください)
doubleFoo :: Foo -> Foo
doubleFoo = combineBar mkDoubledFoo
-- 複雑な副作用の列の記述を楽にする型シノニムを定義
-- これにより、副作用を列に追加することを受け入れます
type RandomConsoleEffects eff = ( random :: RANDOM, console :: CONSOLE | eff )
-- これは副作用をRANDOMとCONSOLEに制限します
type RandomConsoleEffect = RandomConsoleEffects ()
ユーザ定義型とは異なり、型シノニムは単に型の別名であり、基の型と区別されません。このため、型クラスのインスタンスを宣言するために使うことはできません。
多相型は1つ以上の制約
の上に成り立っています。詳しくは型クラスの章を見てください。
高階多相型と制約のある型を除いたほとんどの型は推論されますが、コロンを2つ用いた型注釈を任意で提供することができ、宣言として書くことも、式の後に書くこともできます。
-- Data.Semiringで定義する
one :: forall a. (Semiring a) => a
-- Intはone = 1であるSemiringのインスタンスなので、oneはIntになれます
int1 :: Int
int1 = one -- int1 = 1と同じ
-- もしくはNumberであっても、one = 1.0であるSemiringのインスタンスとして提供されます
number1 = one :: Number -- number1 = 1.0と同じ
-- また、この多相性を保つことができるので、任意のSemiringで動作します
-- (型注釈がなければ、これはデフォルトです)
semiring1 :: forall a. Semiring a => a
semiring1 = one
-- 他の型クラスの制約を設けることもできます
equal1 = one :: forall a. Semiring a => Eq a => a
種システムは以下の種を定義します:
- 型の種
Type
- 高階種
k1 -> k2
- 列種
# k
- ユーザ定義種
Control.Monad.Eff.Effect
など。この例は作用の種です。
列の種# k
はラベル付きの分類に使われ、種k
の型の非順序集合です。
例えば、# Type
は型の列の種で、レコードの定義に使われます。そして、# Control.Monad.Eff.Effect
は作用の列の種で、これは拡張可能の作用モナドControl.Monad.Eff.Eff
を定義するために使われます。
型変数は型や列だけでなく型コンストラクタや行コンストラクタなどを表すこともでき、そのような種を持つ型変数はforall
量化子によって束縛されます。