Haskell
melangの各noteで他言語と比較した、Haskellの設計をまとめたページ。ここに書かれている内容は、各noteの比較表から自動で集めたものです。
型
強力な型推論があり、型注釈はほぼ省略できる。
age :: Int
age = 20age :: 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 = 20age :: Int
age = 20
greet :: String -> String -- 引数と戻り値を矢印でつなげた1つの型
greet name = "Hello, " ++ name値そのものの定義行とは別に`name :: Type`という型シグネチャを独立した行として書く。関数はカリー化された1引数関数の連鎖として扱われるため、引数と戻り値を構文上区別せず`A -> B`のように矢印でつなぐ。型注釈はHindley-Milner推論によりほぼ省略できるが、可読性のためシグネチャを明示するのが慣習になっている。
型変換
変換関数で明示し、失敗し得る変換はMaybeで返す。
y = fromIntegral x :: Int64x :: 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 = 10x :: 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 Stringdata 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 -> Intfoo :: 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 -> Intf :: [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 bx = 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 UserfindUser :: UserId -> IO User
findUser id = ...
getUser :: UserId -> User
getUser id = findUser id -- 型エラー: IO User と User は別の型
getUser :: UserId -> IO User
getUser id = findUser id -- OKI/Oを行う関数の戻り値は必ず`IO a`型になり、`IO a`から`a`を一般的な方法で取り出すことはできない(`do`記法でIOアクションとして合成するのみ)。そのため純粋な関数からI/O関数を呼ぶことは型エラーになり、コンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。が強制する。`unsafePerformIO`という明示的な抜け道はあるが、名前の通り安全性の放棄として扱われる。標準の`IO`は単一のeffectで、ファイル・ネットワーク・乱数などを型レベルで区別しないが、`mtl`や`effectful`などのライブラリでより細かいeffectを型として分離できる。
言語名
人名(数学者Haskell Curry)に由来する固有名詞。
論理学者Haskell Curryの名(ファーストネーム)に由来する。人名由来の命名は一般語との衝突を避けやすいが、綴りの読み方が英語話者にも自明でない(「ハスケル」と読む)という副作用がある。