← melang

Haskell

melangの各noteで他言語と比較した、Haskellの設計をまとめたページ。ここに書かれている内容は、各noteの比較表から自動で集めたものです。

型

強力な型推論があり、型注釈はほぼ省略できる。

age :: Int
age = 20
age :: Int
age = 20

name = "Alice" -- 型注釈がなくても型推論で String と判定される

Hindley-Milner系の強力な型推論を持ち、型注釈はほとんどの場面で省略できる。代数的データ型複数の型を組み合わせて新しい型を作る手法。「AまたはB」のように取りうる形が閉じた集合になっているものをSum Type(直和型)と呼び、Rustの`enum`やHaskellの`data`が代表例。各variant(構成要素)が異なるフィールドを持てる点で、単純な整数の列挙とは異なる。(ADTAlgebraic Data Type(代数的データ型)の略。複数の型を組み合わせて新しい型を作る手法で、「AまたはB」のように取りうる形が閉じた集合になっているものをSum Type(直和型)と呼ぶ。Rustの`enum`やHaskellの`data`が代表例で、各variant(構成要素)が異なるフィールドを持てる点で、単純な整数の列挙とは異なる。)とパターンマッチが標準機能として組み込まれており、`Maybe`型でNullの有無を型として表現する。

型注釈の位置

型注釈は本体とは別の行に書く「シグネチャ方式」。引数・戻り値を区別せず矢印でつなぐ。

age :: Int
age = 20
age :: Int
age = 20

greet :: String -> String -- 引数と戻り値を矢印でつなげた1つの型
greet name = "Hello, " ++ name

値そのものの定義行とは別に`name :: Type`という型シグネチャを独立した行として書く。関数はカリー化された1引数関数の連鎖として扱われるため、引数と戻り値を構文上区別せず`A -> B`のように矢印でつなぐ。型注釈はHindley-Milner推論によりほぼ省略できるが、可読性のためシグネチャを明示するのが慣習になっている。

型変換

変換関数で明示し、失敗し得る変換はMaybeで返す。

y = fromIntegral x :: Int64
x :: Int32
x = 10

y :: Int64
y = fromIntegral x        -- 数値型間の変換

z :: Int32
z = fromIntegral (3000000000 :: Int64) -- 黙って溢れる

toIntegralSized (3000000000 :: Int64) :: Maybe Int32 -- Nothing

readMaybe "42" :: Maybe Int -- Just 42

数値型間の変換は`fromIntegral`が担い、縮小変換でも黙って値が溢れる。範囲外を検出したい場合は`toIntegralSized`が`Maybe`を返す。文字列のパースは、例外を投げる`read`より失敗を`Maybe`で表す`readMaybe`が推奨されている。

暗黙の型変換

数値型間の暗黙変換はなく、変換関数で明示する。

x :: Int
x = 10
x :: Int
x = 10

y :: Integer
y = x             -- 型エラー
y = fromIntegral x -- OK

数値型間の暗黙変換はなく、`Int`から`Integer`、`Int`から`Double`のような変換にも`fromIntegral`や`realToFrac`のような明示的な変換関数を要求する。数値リテラル自体は`Num`型クラスにより多相的に扱われるが、これは既存の値を変換する話とは別の仕組み。

Enum / Sum Type

dataによる代数的データ型複数の型を組み合わせて新しい型を作る手法。「AまたはB」のように取りうる形が閉じた集合になっているものをSum Type(直和型)と呼び、Rustの`enum`やHaskellの`data`が代表例。各variant(構成要素)が異なるフィールドを持てる点で、単純な整数の列挙とは異なる。が中心。

data Result = Success User | Failure String
data Result = Success User | Failure String

describe :: Result -> String
describe (Success user) = "ok: " ++ show user
describe (Failure message) = "error: " ++ message
-- パターン漏れは -Wincomplete-patterns で警告される

`data`によるADTAlgebraic Data Type(代数的データ型)の略。複数の型を組み合わせて新しい型を作る手法で、「AまたはB」のように取りうる形が閉じた集合になっているものをSum Type(直和型)と呼ぶ。Rustの`enum`やHaskellの`data`が代表例で、各variant(構成要素)が異なるフィールドを持てる点で、単純な整数の列挙とは異なる。が中心で、各コンストラクタが異なるフィールドを持てる。網羅性はコンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。の警告(`-Wincomplete-patterns`)として検出でき、警告をエラー扱いにするビルド設定も広く使われる。

値の不変性

束縛も値も不変。状態の変更は、別の仕組みとして分けて扱う。

let x = ... / data User = User { name :: String }
data User = User { name :: String, tags :: [String] }

let a = User { name = "alice", tags = ["a"] }
let b = a { name = "bob" }        -- 新しい値。aは変わらない
let c = a { tags = tags a ++ ["admin"] }

-- 変更が必要な場合は、型に現れる別の仕組みを使う
import Data.IORef
main = do
  r <- newIORef (0 :: Int)
  modifyIORef r (+ 1)

束縛は再代入されず、データも不変である。更新は、レコード更新構文や関数で新しい値を作って表す。変更可能な状態は`IORef`・`ST`・`MVar`などとして型に現れ、純粋な処理と区別される。melangの方針に最も近いが、状態変更を型で区別する点は、melangでは決めていない。

名前付き引数

名前付き引数はなく、意味を明示したい場合はレコード構文を使う。

foo :: Int -> Int -> Int
foo :: Int -> Int -> Int
foo width height = width + height
foo 10 20 -- 引数名は呼び出し側に現れない

data FooArgs = FooArgs { width :: Int, height :: Int }
foo2 :: FooArgs -> Int
foo2 args = width args + height args
foo2 FooArgs { width = 10, height = 20 } -- フィールド名で意味を明示

関数呼び出しに名前付き引数はない。意味を明示したい場合は、フィールド名を持つレコードを渡すことが一般的な代替手段になる。

関数の引数

値がすべて不変なので、渡し方はプログラマが選ばない。

f :: Int -> Int
f :: [Int] -> [Int]
f xs = 1 : xs   -- 元のxsは変わらない。共有するかコピーするかは処理系が決める

g :: Int -> Int
g !x = x + 1    -- BangPatterns。引数を即座に評価する

値がすべて不変なので、コピーか参照かはプログラムの結果に影響せず、処理系が決める。melangの「変更可能なデータが存在しない」前提に最も近い言語。melangはこの前提に、Java・Kotlinと同じ「常に共有して渡す」仕様を組み合わせ、コストを予測しやすくする。

if 文/式

`if`は式で、`else`は常に必須。`else`なしで副作用を実行するには`when`を使う。

if cond then a else b
x = if cond then 1 else 2

-- 副作用を条件付きで実行する場合はwhenを使う
import Control.Monad (when)
main = when cond process

`if`は式で、`else`を省略できない。条件付きの副作用は`Control.Monad`の`when`・`unless`で表すため、「手続きの`if`」と「値の`if`」が構文ではなくライブラリ関数で分かれている。

I/O の明示

`IO`型が、I/Oを行う関数の戻り値型として必須になる。

findUser :: UserId -> IO User
findUser :: UserId -> IO User
findUser id = ...

getUser :: UserId -> User
getUser id = findUser id -- 型エラー: IO User と User は別の型

getUser :: UserId -> IO User
getUser id = findUser id -- OK

I/Oを行う関数の戻り値は必ず`IO a`型になり、`IO a`から`a`を一般的な方法で取り出すことはできない(`do`記法でIOアクションとして合成するのみ)。そのため純粋な関数からI/O関数を呼ぶことは型エラーになり、コンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。が強制する。`unsafePerformIO`という明示的な抜け道はあるが、名前の通り安全性の放棄として扱われる。標準の`IO`は単一のeffectで、ファイル・ネットワーク・乱数などを型レベルで区別しないが、`mtl`や`effectful`などのライブラリでより細かいeffectを型として分離できる。

言語名

人名(数学者Haskell Curry)に由来する固有名詞。

論理学者Haskell Curryの名(ファーストネーム)に由来する。人名由来の命名は一般語との衝突を避けやすいが、綴りの読み方が英語話者にも自明でない(「ハスケル」と読む)という副作用がある。