PureScriptの公式ドキュメントより、『Haskellとの違い』のざっくり翻訳+αです。原文のライセンスはCreative Commons Legal Code Attribution-NonCommercial-ShareAlike 3.0 Unported です。
(翻訳ここから)
評価戦略
Haskellとは異なり、PureScriptは正格評価です。
JavaScriptの評価戦略と一致するので、既存のコードとの相互運用はとても簡単です。PureScriptモジュールからエクスポートされた関数は、「通常の」JavaScript関数とまったく同じように振る舞いますし、それに対応して、FFIを通じてのJavaScriptの呼び出しもシンプルになっています。
正格評価を維持するということは、実行時システムや非常に複雑なJavaScript出力も必要ないということを意味します。やむをえないオーバーヘッドもついてきますが、JavaScriptの上に遅延性を導入することで、必要ならより効率のいいコードを書くことも可能です。
Prelude/base
PureScriptに暗黙のPrelude
インポートはありません。Prelude
モジュールは他のモジュールとまったく同じものです。また、コンパイラと一緒に配布されるライブラリもありません。
一般に広く使われている『標準の』Prelude
はpurescript-prelude
です。
モジュールのインポートとエクスポート
モジュールに定義された型クラスは、class
キーワードを使って特別にインポートします。
module B where
import A (class Fab)
PureScriptにqualified
キーワードはありません。import Data.List as List
というように書くと、Haskellでimport qualified Data.List as List
と書いたのと同じ効果になります。
モジュールのインポートとエクスポートについては、Modulesのページで詳しく説明されています。
型
明示的な forall
PureScriptにおける多相的な関数は、明示的なforall
を使って事前に型変数を定義する必要があります。たとえば、Haskellではリストのlength
関数は次のように定義することができます。
length :: [a] -> Int
PureScriptでは、この定義はType variable a is undefined
というエラーになります。PureScriptでこれと同じような定義をするには、次のように書きます。
length :: forall a. Array a -> Int
forall
では一度に複数個の型変数を定義することもできます。また、型クラス制約の手前に書かなくてはなりません。
ap :: forall m a b. (Monad m) => m (a -> b) -> m a -> m b
数
JavaScriptの標準のIEEE754浮動小数点数を表す、組み込みのNumber
型が存在し、Int
型ではそれが32ビット整数の値域に制限されます。Int
の値と操作には|0
が後ろに付加されてJavaScriptが生成されることでこれが実現されています。たとえば、もしInt
型の変数x
とy
とz
があるとしたら、PureScriptの(x + y) * z
は((x + y)|0 * z)|0
へとコンパイルされます。
Unit
PureScriptはUnit
型を持っており、これはHaskellの()
に相当します。Prelude
モジュールでは、このUnit
型の値としてunit
という値を提供しています。
[a]
PureScriptではリスト型に対して構文糖は提供していません。Data.List
のList
を使ってリストを構築してください。
組み込みのJavaScript配列に対応するものとしてはArray
型がありますが、これはList
と同じようなパフォーマンス特性があるわけではありません。Array
の値は[x, y, z]
というようなリテラルで構築することができますが、型についてはArray a
というように型注釈をすることが必要です。
レコード
PureScriptではJavaScript形式のオブジェクトを行型(Row types)を使って直接エンコードすることができます。Haskell形式のレコード定義は、PureScriptのものとはまったく異なる意味を持っています。
data Point = Point { x :: Number, y :: Number }
Haskellでは、上の定義は現在の環境へ次のようないくつかのものを導入します。
Point :: Number -> Number -> Point
x :: Point -> Number
y :: Point -> Number
それに対して、PureScriptではオブジェクト型ひとつを保持するPoint
構築子だけを導入します。実のところ、オブジェクト型を使うときは、データコンストラクタをまったく必要としないことが大半です。
type PointRec = { x :: Number, y :: Number }
オブジェクトはJavaScriptと同じような構文で構築されます(型の定義も同様です)。
origin :: PointRec
origin = { x: 0, y: 0 }
そしてPureScriptでは、Haskellのようにx
やy
アクセサ関数が導入されるのではなく、JavaScriptのプロパティと同じようにx
やy
を読むことができます。
originX :: Number
originX = origin.x
PureScriptではHaskellと同じようなレコード更新構文も提供します。
setX :: Number -> PointRec -> PointRec
setX val point = point { x = val }
気をつけたほうがいいよくある間違いとして、先程の本来のPoint
と同じようにデータ型を受け入れる関数を書いているときに、このオブジェクトはPoint
の中に包まれたままだというものがあります。そのため、次のようなコードはうまくいきません。
showPoint :: Point -> String
showPoint p = show p.x <> ", " <> show p.y
そうではなく、Point
を分解してオブジェクトからオブジェクトを取り出す必要があります。
showPoint :: Point -> String
showPoint (Point obj) = show obj.x <> ", " <> show obj.y
型クラス
矢印の方向
スーパークラスをもつ型クラスを定義するとき、PureScriptとHaskellでは矢印は逆向きになります。たとえば、PureScriptでは次のように書きます。
class (Eq a) <= Ord a where
...
この=>
は論理包含と同じように読むことができます。この場合、Ord a
インスタンスが存在する『ならば』Eq a
インスタンスが存在します。
名前付きのインスタンス
PureScriptでは、インスタンスには次のように名前が付きます。
instance arbitraryUnit :: Arbitrary Unit where
...
Haskellと同じように重複インスタンスは禁止されます。インスタンスの名前はコンパイル後のJavaScriptの可読性のために使われます。
インスタンスの導出
Haskellとは異なり、PureScriptにはデータ型の定義の構文自体にはインスタンスの自動導出の機能がありません。例えば、次のようなコードはPureScriptでは動きません。
data Foo = Foo Int String deriving (Eq, Ord)
しかしそのかわり、PureScriptには型のStandaloneDeriving
という機能があります。
data Foo = Foo Int String
derive instance eqFoo :: Eq Foo
derive instance ordFoo :: Ord Foo
この方法での導出が可能な型クラスの例としては、Eq
やFunctor
、Ord
があります。その他の型クラスの一覧については、こちらを参照してください。
ジェネリクスを使うと、Bounded
やMonoid
、Show
などの型クラスについてのジェネリックな実装を使うことが可能になります。ジェネリックな実装を持つ他の型クラスについてや、独自の型クラスに対してジェネリックな実装を書く方法についての説明は、the generics-rep libraryを参照してください。
はぐれたインスタンス
Haskellとは異なり、PureScriptでははぐれたインスタンス(Orphan instances)は完全に禁止されます。これは、はぐれたインスタンスの定義はコンパイルエラーを引き起こすことがあるからです。
同じモジュール内にインスタンスが定義できないときは、newtypeで包むことで回避する方法があります。
デフォルトのメンバ
今のところ、型クラスにデフォルトのメンバを定義することはできません。これは将来的には変更される可能性があります。
型クラス階層
多くの型クラス階層はHaskellよりも段階的になっています。たとえば次のようになっています。
-
Category
はスーパークラスとして(<<<)
を提供する型クラスであるSemigroupoid
を持ちます。Semigroupoid
は単位元を要求しません。(※訳注:HaskellではCategory
型クラスが射の合成(.)
と単位元id
の両方を持ちますが、PureScriptではこれらのメンバは射の合成(<<<)
を提供する型クラスSemigroupoid
と単位元identity
を提供する型クラスCategory
に分かれており、Semigroupoid
がCategory
のスーパークラスになっています。Haskellではひとつの型クラスであったものが、PureScriptでは親子関係のあるふたつの型クラスに分割されているということです。) -
Monoid
はスーパークラスとして(<>)
を提供するSemigroup
を持ちます。Semigroup
は単位元を要求しません。 -
Applicative
はスーパークラスとして(<*>)
を提供するApply
を持ちます。Apply
はpure
の実装は要求しません。
タプル
レコードを使えば同じような役割を果たす任意個のタプルをもっと意味のある型とアクセサを持って実現できるので、PureScriptではタプルに特別な構文は持っていません。
2要素のタプルについては、purescript-tuples ライブラリを通じて使うことができます。Tuple
は他の型やデータコンストラクタとまったく同じように扱われます。
合成演算子
PureScriptでは右から左に合成する関数として、.
ではなく<<<
を使います。これは.
のプロパティアクセスや名前の修飾の構文とのあいまいさを避けるためです。また、左から右への合成については、対応する>>>
演算子があります。
実際にはこの<<<
演算子は、semigroupoids とCategoryに適用されるより抽象的な「射の合成演算子」です。Prelude
モジュールは->
型についてのSemigroupoid
インスタンスを提供しており、これが関数合成になっています。
return
過去にはPureScriptでもreturn
を使っていたときがありました。しかし、現在はこれは削除され、pure
に置き換えられています。return
はpure
の単なる別名でしたので、この変更というのは単に別名が削除されたというだけです。
配列内包表記
PureScriptでは配列内包表記に特別な構文は提供していません。そのかわり、do
記法を使ってください。purescript-control
パッケージのControl.MonadPlus
モジュールが提供しているguard
関数は、結果をフィルターするのに使うことができます。
import Prelude (($), (*), (==), bind, pure)
import Data.Array ((..))
import Data.Tuple (Tuple(..))
import Control.MonadZero (guard)
factors :: Int -> Array (Tuple Int Int)
factors n = do
a <- 1 .. n
b <- 1 .. a
guard $ a * b == n
pure $ Tuple a b
$
を特別扱いしない
GHCは$
演算子に特別な型付けルールを適用しますが、そのためランク2のrunST
関数への$
の適用が正しく型付けされます。
runST $ do
...
PureScriptではこのようなルールは提供していませんので、次のどちらかに従います。
-
runST do ...
のようにして演算子をとる - または、
runST (do ...)
のようにして括弧で囲む
演算子の定義
Haskellでは、次のような自然な構文で演算子を定義することができます。
f $ x = f x
PureScriptでは、演算子は名前付き関数の別名として提供することになります。演算子を使った関数の定義は、バージョン0.9で廃止されました。
apply f x = f x
infixr 0 apply as $
演算子のセクション
Haskellでは、中置記法の演算子の部分適用には構文糖があります。
(2 ^) -- desugars to `(^) 2`, or `\x -> 2 ^ x`
(^ 2) -- desugars to `flip (^) 2`, or `\x -> x ^ 2`
PureScriptでは、演算子セクションはちょっと異なる見た目になります。
(2 ^ _)
(_ ^ 2)
拡張
PureScriptコンパイラはGHCのような言語拡張を提供しません。しかし、GHC拡張のいくつかと同等の機能(もしくは少なくとも似たような機能)が、『組み込みの』言語機能として存在しています。
- DataKinds (see note below)
- EmptyDataDecls
- ExplicitForAll
- FlexibleContexts
- FlexibleInstances
- FunctionalDependencies
- KindSignatures
- MultiParamTypeClasses
- PartialTypeSignatures
- RankNTypes
- RebindableSyntax
- ScopedTypeVariables
DataKinds
については注意してください。Haskellとは異なり、ユーザは種を自分で定義することができますが、それらは昇格されません。つまり、これらのコンストラクタは型でのみ使うことができ、値では使うことができません。種のシステムについての詳しい情報は、以下を参照してください。
error
とundefined
error
については、purescript-exceptions
パッケージのEffect.Exception.Unsafe.unsafeThrow
を使うことができます。
undefined
はpurescript-unsafe-coerce
パッケージにある Unsafe.Coerce.unsafeCoerce unit :: forall a. a
で模倣することができます。 https://github.com/purescript/purescript-prelude/issues/44 を参照してみてください。
ただし、PureScriptの正格性が原因で、これらはHaskell版とは異なる振る舞いを持つことがあるのに注意してください。
ドキュメントコメント
ドキュメントを書くときは、ドキュメントコメントの最初の一行だけではなく、ドキュメントコメントのすべての行の先頭にパイプ文字|
を書かなければなりません。詳しくはthe documentation for doc-commentsを参照してください。
Haskellの◯◯はどこにあるの?
PureScriptはHaskellのコードをそのまま引き継いでいるわけではないので、Haskellの演算子や関数のなかには、PureScriptでは異なる名前を持っているものがあります。
-
(>>)
は(*>)
に変わりました。Apply
はMonad
のスーパークラスなので、Monad
専用のバージョンは必要ないからです。 - 0.9.1以降、
Prelude
ライブラリはappend
つまり(<>)
(Haskellではmappend
)のふたつめの別名である(++)
をもはや含んでいません。 -
mapM
はtraverse
です。traverse
は、リストに限らず、どんなtraversableな構造に対しても適用するより一般的な形式であるからです。また、Monad
ではなくApplicative
であることだけを要求します。同じように、liftM
もmap
になっています。 - Haskellの
Data.List
の一部の関数は、Data.Foldable
やData.Traversable
でもっと一般的な形式で提供されています。 -
some
やmany
が リストのような型(Data.Array
やData.List
)にそれぞれ 定義されています。 - typed holesには
_foo
ではなく?foo
を使ってください。holeには名前をつけなくてはならず、単なる?
は禁止です。 - 範囲は
[1..2]
ではなく1..2
のように書きます。
(翻訳ここまで)
+α
そのほかの違いで、私が思いついたものや、指摘を受けて気付いたものをいくつか付け加えておきます。
文字列の型
Haskellでは文字列の型String
は文字 Char
のリスト[Char]
の別名となっています。これは確かに直感的で、リストの関数がすべて String
に対しても使えるというわかりやすさはあったものの、パフォーマンスが劣悪で結局は Haskell 最大の失敗のひとつとなりました。
OverloadedStrings拡張などで凌いではいるものの、文字列という基本的な型がString
、Text
、ByteString
のように複数入り混じっているのは、わかりやすいとはとても言えません。PureScriptではJavaScriptのString
と対応するString
がひとつあるだけで、それとは別にChar
があります。
Lazyな入出力
評価戦略とも関係するのですが、Haskellのでは古くから遅延評価の仕組みに基づく入出力の関数がいくつか提供されています。しかしこれは実用上はまったく使い物になりませんでした。PureScriptは正格評価なので、このような落とし穴はまったくありません。
型クラス制約
Haskellでは(TypeClassA a, TypeClassB b) => Type a b
のようにして、いちどの=>
で複数の型クラス制約を書くことができましたが、PureScriptではTypeClassA a => TypeClassB b => Type a b
のようにして、=>
につきひとつづつの型クラス制約を書きます。これはPureScriptにおける型クラス制約は、暗黙の引数に他ならないためです。たとえば、for
はtraverse
の引数の順序を交換したものに過ぎませんが、次のように定義されています。
for
:: forall a b m t
. Applicative m
=> Traversable t
=> t a
-> (a -> m b)
-> m (t b)
for x f = traverse f x
これをコンパイルすると、次のようなJavaScriptへと変換されます。
var $$for = function (dictApplicative) {
return function (dictTraversable) {
return function (x) {
return function (f) {
return Data_Traversable.traverse(dictTraversable)(dictApplicative)(f)(x);
};
};
};
};
型クラス制約が特殊な引数でしかないということを考えると、カリー化された関数のように=>
を繰り返し使う構文に一貫性があることがわかると思います。
Eq型クラスのメンバ
HaskellではEq
型クラスには(==)
と(/=)
のふたつが定義されますが、PureScriptのEq
型クラスでは(==)
の本来の名前であるeq
だけが定義されており、notEq
およびその別名である(/=)
はただの独立した関数になっています。
Haskellで(==)
と(/=)
の両方が実装可能になっているのは最適化の都合だというような話だったかと思うのですが、(/=)
が実装可能であることで実用上大きく性能が改善するというようなケースがあるのかは疑わしいです。PureScriptのシンプルな定義のほうが良さそうです。
for_
Haskellでは同じ機能であるfor :: (Traversable t, Applicative f) => t a -> (a -> f b) -> f (t b)
とforM :: (Traversable t, Monad m) => t a -> (a -> m b) -> m (t b)
のふたつが効率上の理由で別々に提供されていますが、PureScriptではfor
だけが提供されています。
関数をつくるアンダーバー_
演算子のセクションの話題と重複しますが、PureScriptでは式の一部がアンダーバー_
になっていると、「その部分を引数として受け取るような関数」を表す式になることがあります。演算子では(_ + 42)
は\x -> x + 42
と同じ意味になりました。それと同様に、次のようなアンダーバーを条件部分においたcase式
case _ of
Nothing -> "Nothing"
Just x -> "Just" <> show x
は、次のような関数と同じ意味になります。
\v -> case v of
Nothing -> "Nothing"
Just x -> "Just" <> show x
これと同じようなアンダーバーを、if _ then x else y
のようにif式でも使えますし、_.foo
のようにプロパティの読み取りに使ったり、_ { x = 42 }
や_ { x = _ }
のようにしてプロパティの更新をする関数を書くこともできます。「アンダーバーに置き換えた部分を引数にとるような関数にする」という振る舞いにおいて構文上の一貫性が取れています。
map
の名前
Haskellにはmap :: (a -> b) -> [a] -> [b]
がありますが、これはFunctor
のfmap :: Functor f => (a -> b) -> f a -> f b
によって一般化され、map
は不要になりました。これを踏まえてPureScriptには最初からリスト専用の関数であるmap
はありませんので、Functor
のほうの一般化された関数のほうにmap
の名前を使うことができました。
IO
とEffect
Haskellでは組み込みの作用の型はIO
ですが、PureScriptではEffect
です。使い勝手のうえではほとんど差はないのですが、これらが入出力以外も含む作用一般のための型であることを踏まえると、強いて言えばEffect
のほうが適切な名前なのではないかと思います。もちろん、IO
のほうが短くて書くのが楽だというのはあります。
部分関数の排除
Haskellのhead :: [a] -> a
は空リストに適用すると実行時エラーになります。このようないわゆる部分関数がHaskellには少なからず存在しており、Haskellはその厳格なイメージに反して実行時エラーに対して脆弱な部分を数多く残しています。PureScriptではそれに相当する関数はhead :: forall a. Array a -> Maybe a
となっており、結果をMaybe
で返すようになっているので実行時エラーになることがありません。このように、PureScriptのあらゆるAPIや構文は基本的に実行時エラーを起こさないような設計になっており、実行時エラーが起きることは基本的にありません。このような言語デザインは、同じくHaskell派生の言語であるElmでも同様になっています。
一方で、効率のためや記述性のために、部分関数であるhead :: forall a. Partial => Array a -> a
もData.Array.Partial
という別のモジュールで提供されており、必要であれば安全性と引き換えにしてこちらを使うこともできます。
ライブラリの高い一貫性
Preludeのところでも簡単に触れましたが、Haskellでは多くのライブラリが言語やコンパイラ組み込みになっている一方、PureScriptでは言語に組み込まれたライブラリはPrim
モジュールに含まれた幾つかの基礎的な型のみです。特殊な要素を極限まで廃することで、言語自体をシンプルに保つと同時に、例外的に扱われる型が極めて少ない、一貫性のあるライブラリを提供できています。
さいごに
Haskellはある種の伝説というか、関数型プログラミング言語のなかでも特にラディカルな位置にあって、Haskellを『美しい言語』だと評する人もいるようです。しかし、Haskellもまた長い歴史を持ち、歴史的な理由で多くの瑕疵を抱えてきた言語です。この記事にあるように、多くの関数が重複していたりしますし、部分関数も多く提供されており実行時エラーを引き起こしやすく、素のHaskellは機能不足で使いづらく多くのGHC拡張を導入してようやく快適に書ける言語だと言えます。そんなわけで私は決して『美しい』とまでは思いませんが、Haskellはそれを正しく理解し、なるべく互換性も保ちつつも改良を加え続け、泥臭くも着実に正しい方向に進んでいると私は思います。
そうはいっても、これだけ長い歴史のある言語を互換性を維持しながら改良を続けるのは容易ではなく、その点PureScriptはHaskellの精神を受け継ぎつつも反省を踏まえて新たに作られている言語で、Haskellに残された不満が大きく解消しています。Haskellに手を出すような人はかなり身軽に言語を乗り換えるタイプの人だと思いますし、PureScriptにも気軽に興味を示すHaskellユーザがもっと増えてくれればいいと私は思います。