Haskell
purescript

【翻訳】PureScriptとHaskellの違い +α

PureScriptの公式ドキュメントより、『Haskellとの違い』のざっくり翻訳+αです。

(翻訳ここから)




評価戦略

Haskellとは異なり、PureScriptは正格評価です。

JavaScriptの評価戦略と一致するので、既存のコードとの相互運用はとても簡単です。PureScriptモジュールからエクスポートされた関数は、「通常の」JavaScript関数とまったく同じように振る舞いますし、それに対応して、FFIを通じてのJavaScriptの呼び出しもシンプルになっています。

正格評価を維持するということは、実行時システムや非常に複雑なJavaScript出力も必要ないということを意味します。やむをえないオーバーヘッドもついてきますが、JavaScriptの上に遅延性を導入することで、必要ならより効率のいいコードを書くことも可能です。


Prelude/base

PureScriptに暗黙のPreludeインポートはありません。Preludeモジュールは他のモジュールとまったく同じものです。また、コンパイラと一緒に配布されるライブラリもありません。

一般に広く使われている『標準の』Preludepurescript-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型の変数xyzがあるとしたら、PureScriptの(x + y) * z((x + y)|0 * z)|0へとコンパイルされます。


Unit

PureScriptはUnit型を持っており、これはHaskellの()に相当します。Preludeモジュールでは、このUnit型の値としてunitという値を提供しています。


[a]

PureScriptではリスト型に対して構文糖は提供していません。Data.ListListを使ってリストを構築してください。

組み込みの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のようにxyアクセサ関数が導入されるのではなく、JavaScriptのプロパティと同じようにxyを読むことができます。

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

この方法での導出が可能な型クラスの例としては、EqFunctorOrdがあります。その他の型クラスの一覧については、こちらを参照してください。

ジェネリクスを使うと、BoundedMonoidShowなどの型クラスについてのジェネリックな実装を使うことが可能になります。ジェネリックな実装を持つ他の型クラスについてや、独自の型クラスに対してジェネリックな実装を書く方法についての説明は、the generics-rep libraryを参照してください。


はぐれたインスタンス

Haskellとは異なり、PureScriptでははぐれたインスタンス(Orphan instances)は完全に禁止されます。これは、はぐれたインスタンスの定義はコンパイルエラーを引き起こすことがあるからです。

同じモジュール内にインスタンスが定義できないときは、newtypeで包むことで回避する方法があります。


デフォルトのメンバ

今のところ、型クラスにデフォルトのメンバを定義することはできません。これは将来的には変更される可能性があります。


型クラス階層

多くの型クラス階層はHaskellよりも段階的になっています。たとえば次のようになっています。



  • Categoryはスーパークラスとして(<<<)を提供する型クラスであるSemigroupoidを持ちます。Semigroupoidは単位元を要求しません。(※訳注:HaskellではCategory型クラスが射の合成(.)と単位元idの両方を持ちますが、PureScriptではこれらのメンバは射の合成(<<<)を提供する型クラスSemigroupoidと単位元identityを提供する型クラスCategoryに分かれており、SemigroupoidCategoryのスーパークラスになっています。Haskellではひとつの型クラスであったものが、PureScriptでは親子関係のあるふたつの型クラスに分割されているということです。)


  • Monoidはスーパークラスとして(<>)を提供するSemigroupを持ちます。Semigroupは単位元を要求しません。


  • Applicativeはスーパークラスとして(<*>)を提供するApplyを持ちます。Applypureの実装は要求しません。


タプル

レコードを使えば同じような役割を果たす任意個のタプルをもっと意味のある型とアクセサを持って実現できるので、PureScriptではタプルに特別な構文は持っていません。

2要素のタプルについては、purescript-tuples ライブラリを通じて使うことができます。Tupleは他の型やデータコンストラクタとまったく同じように扱われます。


合成演算子

PureScriptでは右から左に合成する関数として、.ではなく<<<を使います。これは.のプロパティアクセスや名前の修飾の構文とのあいまいさを避けるためです。また、左から右への合成については、対応する>>>演算子があります。

実際にはこの<<<演算子は、semigroupoids とCategoryに適用されるより抽象的な「射の合成演算子」です。Preludeモジュールは->型についてのSemigroupoidインスタンスを提供しており、これが関数合成になっています。


return

過去にはPureScriptでもreturnを使っていたときがありました。しかし、現在はこれは削除され、pureに置き換えられています。returnpureの単なる別名でしたので、この変更というのは単に別名が削除されたというだけです。


配列内包表記

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とは異なり、ユーザは種を自分で定義することができますが、それらは昇格されません。つまり、これらのコンストラクタは型でのみ使うことができ、値では使うことができません。種のシステムについての詳しい情報は、以下を参照してください。

https://github.com/purescript/documentation/blob/master/language/Types.md#kind-system


errorundefined

errorについては、purescript-exceptionsパッケージのEffect.Exception.Unsafe.unsafeThrowを使うことができます。

undefinedpurescript-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では異なる名前を持っているものがあります。



  • (>>)(*>)に変わりました。ApplyMonadのスーパークラスなので、Monad専用のバージョンは必要ないからです。

  • 0.9.1以降、Preludeライブラリはappendつまり(<>) (Haskellではmappend)のふたつめの別名である(++)をもはや含んでいません。


  • mapMtraverseです。traverseは、リストに限らず、どんなtraversableな構造に対しても適用するより一般的な形式であるからです。また、MonadではなくApplicative であることだけを要求します。同じように、liftMmapになっています。

  • HaskellのData.Listの一部の関数は、 Data.FoldableData.Traversableでもっと一般的な形式で提供されています。


  • somemanyが リストのような型(Data.ArrayData.List)にそれぞれ 定義されています。

  • typed holesには_fooではなく?fooを使ってください。holeには名前をつけなくてはならず、単なる?は禁止です。

  • 範囲は[1..2]ではなく1..2のように書きます。


(翻訳ここまで)


+α

そのほかの違いで、私が思いついたものや、指摘を受けて気付いたものをいくつか付け加えておきます。


型クラス制約

Haskellでは(TypeClassA a, TypeClassB b) => Type a bのようにして、いちどの=>で複数の型クラス制約を書くことができましたが、PureScriptではTypeClassA a => TypeClassB b => Type a bのようにして、=>につきひとつづつの型クラス制約を書きます。これはPureScriptにおける型クラス制約は、暗黙の引数に他ならないためです。たとえば、fortraverseの引数の順序を交換したものに過ぎませんが、次のように定義されています。

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] がありますが、これはFunctorfmap :: Functor f => (a -> b) -> f a -> f bによって一般化され、mapは不要になりました。これを踏まえてPureScriptには最初からリスト専用の関数であるmapはありませんので、Functorのほうの一般化された関数のほうにmapの名前を使うことができました。


IOEffect

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 -> aData.Array.Partialという別のモジュールで提供されており、必要であれば安全性と引き換えにしてこちらを使うこともできます。


ライブラリの高い一貫性

Preludeのところでも簡単に触れましたが、Haskellでは多くのライブラリが言語やコンパイラ組み込みになっている一方、PureScriptでは言語に組み込まれたライブラリはPrimモジュールに含まれた幾つかの基礎的な型のみです。特殊な要素を極限まで廃することで、言語自体をシンプルに保つと同時に、例外的に扱われる型が極めて少ない、一貫性のあるライブラリを提供できています。


さいごに

Haskellはある種の伝説というか、関数型プログラミング言語のなかでも特にラディカルな位置にあって、Haskellを『美しい言語』だと評する人もいるようです。しかし、Haskellもまた長い歴史を持ち、歴史的な理由で多くの瑕疵を抱えてきた言語です。この記事にあるように、多くの関数が重複していたりしますし、部分関数も多く提供されており実行時エラーを引き起こしやすく、素のHaskellは機能不足で使いづらく多くのGHC拡張を導入してようやく快適に書ける言語だと言えます。そんなわけで私は決して『美しい』とまでは思いませんが、Haskellはそれを正しく理解し、なるべく互換性も保ちつつも改良を加え続け、泥臭くも着実に正しい方向に進んでいると私は思います。

そうはいっても、これだけ長い歴史のある言語を互換性を維持しながら改良を続けるのは容易ではなく、その点PureScriptはHaskellの精神を受け継ぎつつも反省を踏まえて新たに作られている言語で、Haskellに残された不満が大きく解消しています。Haskellに手を出すような人はかなり身軽に言語を乗り換えるタイプの人だと思いますし、PureScriptにも気軽に興味を示すHaskellユーザがもっと増えてくれればいいと私は思います。