値・式・不変性
基礎Haskell に変数はなく、すべては「定義」。一度バインドされた値は変わらない。let ... in と where でローカル定義。
x :: Int
x = 42
-- ローカル定義
result :: Double
result = let r = 5.0
pi = 3.14159
in pi * r * r
-- where 節
circleArea r = pi * r * r
where pi = 3.14159Int(固定長整数)・Integer(任意精度整数)・Double・Bool・Char・String(= [Char])。型注釈は :: で記述。
x :: Int
x = 42
y :: Integer
y = 2 ^ 100 -- 任意精度
z :: Double
z = 3.14
b :: Bool
b = True
c :: Char
c = 'A'
s :: String
s = "hello" -- [Char] と同じHaskell はデフォルトで遅延評価(非正格)。式は必要になるまで評価されない(thunk)。無限リストも定義できる。
nats :: [Int]
nats = [1..] -- 無限リスト(遅延)
take 5 nats -- [1,2,3,4,5]
ones :: [Int]
ones = 1 : ones -- 無限の 1 のリスト演算子は関数。バッククォートで関数を中置演算子として使える(x div y)。infixl・infixr で優先順位を定義可能。
div 10 3 -- 3(前置)
10 `div` 3 -- 3(中置)
(+) 1 2 -- 3(演算子を前置)
1 + 2 * 3 -- 7(* が高優先)
$ -- 最低優先度の関数適用
print $ 1 + 2 -- print (1+2)制御フロー・パターンマッチ
基礎Haskell のパターンマッチは関数定義に直接書ける(複数節)。case ... of で式としても使える。
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)
describe :: Int -> String
describe x = case x of
0 -> "zero"
1 -> "one"
_ -> "other"| でガード節を書く。otherwise は常に真の条件(True と同じ)。パターンマッチとガードを組み合わせられる。
bmi :: Double -> String
bmi b
| b <= 18.5 = "Underweight"
| b <= 25.0 = "Normal"
| b <= 30.0 = "Overweight"
| otherwise = "Obese"Haskell の if は式であり else 節は必須(両分岐の型が一致する必要がある)。
abs' :: Int -> Int
abs' n = if n < 0 then -n else n
-- 多段は guard の方が読みやすい
max' a b = if a > b then a else bwhere は定義の後に書くローカル定義節(関数全体のスコープ)。let...in は式の中で使うローカル定義。
hypotenuse :: Double -> Double -> Double
hypotenuse a b = sqrt (sq a + sq b)
where sq x = x * x
-- let in
compute :: Int -> Int
compute n = let x = n * 2
y = x + 1
in x * y関数・高階関数
基礎Haskell のすべての関数は自動でカリー化。f a b は (f a) b。引数を一部渡して部分適用した関数を作れる。
add :: Int -> Int -> Int
add x y = x + y
add5 :: Int -> Int
add5 = add 5 -- 部分適用
map (* 2) [1,2,3] -- [2,4,6]
filter (> 3) [1..5] -- [4,5]\x -> expr でラムダ式(無名関数)を定義。バックスラッシュが λ の代替。
double :: [Int] -> [Int]
double = map (\ x -> x * 2)
-- セクション(演算子の部分適用)
map (* 2) [1,2,3] -- [2,4,6]
map (subtract 1) [1,2,3] -- [0,1,2](1を引く).(ドット)で関数を合成。(f . g) x = f (g x)。$ は最低優先度の関数適用で括弧を省略できる。
import Data.Char (toUpper)
import Data.List (intercalate)
titleCase :: String -> String
titleCase = intercalate " "
. map (\(h:t) -> toUpper h : t)
. words
-- $の使用
print $ map (* 2) [1..5] -- print (map (* 2) [1..5])Haskell の中核的な高階関数。foldr は右結合で遅延評価と相性が良い。foldl'(正格版)が大きなリストに推奨。
map (+ 1) [1,2,3] -- [2,3,4]
filter even [1..10] -- [2,4,6,8,10]
foldl (+) 0 [1..5] -- 15
foldr (:) [] [1,2,3] -- [1,2,3](リストの再構成)
scanl (+) 0 [1..5] -- [0,1,3,6,10,15]リスト・タプル
基礎連結リスト。[] が空リスト、: が先頭追加(cons)。パターンマッチで (x:xs) と分割できる。
[1, 2, 3] -- リストリテラル
1 : [2, 3] -- 先頭追加 → [1,2,3]
[1,2] ++ [3,4] -- 結合 → [1,2,3,4]
head [1,2,3] -- 1
tail [1,2,3] -- [2,3]
null [] -- True(空判定)
length [1,2,3] -- 3[ expr | x <- list, condition ] の形式でリストを生成。SQL の SELECT に相当。
[ x^2 | x <- [1..10] ]
-- [1,4,9,16,25,36,49,64,81,100]
[ (x,y) | x <- [1..3], y <- [1..3], x /= y ]
-- [(1,2),(1,3),(2,1),(2,3),(3,1),(3,2)]
evens = [ x | x <- [1..20], even x ]固定長・異種型のデータ構造。fst・snd でペアの要素を取得。パターンマッチで分割。
(1, "hello", True) :: (Int, String, Bool)
fst (1, 2) -- 1
snd (1, 2) -- 2
let (x, y) = (3, 4) -- パターン分割
zip [1,2,3] "abc" -- [(1,'a'),(2,'b'),(3,'c')]String = [Char] は便利だが遅い。本番では Data.Text(テキスト)・Data.ByteString(バイナリ)を使う。
words "hello world" -- ["hello","world"]
unwords ["hello","world"] -- "hello world"
lines "a\nb\nc" -- ["a","b","c"]
unlines ["a","b"] -- "a\nb\n"
import Data.Char (toUpper)
map toUpper "hello" -- "HELLO"型定義
基礎data で代数的データ型を定義。直和型(sum type)と直積型(product type)の組み合わせ。
-- 直和型(判別共用体)
data Color = Red | Green | Blue
-- 直積型(レコード)
data Point = Point { x :: Double, y :: Double }
-- 組み合わせ
data Shape
= Circle { radius :: Double }
| Rect { width :: Double, height :: Double }
area :: Shape -> Double
area (Circle r) = pi * r * r
area (Rect w h) = w * hnewtype は単一フィールドを持つラッパー型。実行時コストはゼロ(コンパイル後は内部型と同一)。型安全な区別が目的。
newtype Name = Name String
newtype Email = Email String
createUser :: Name -> Email -> String
createUser (Name n) (Email e) = n ++ " <" ++ e ++ ">"
-- createUser (Email "x@y") (Name "Bob") -- 型エラーtype は型の別名。String = [Char] が代表例。新しい型ではなく、同じ型を別の名前で呼ぶだけ。
type Name = String
type Age = Int
type Person = (Name, Age)
greet :: Person -> String
greet (n, a) = n ++ " is " ++ show a ++ " years old"Maybe a は Just a または Nothing(null の型安全な代替)。Either e a は Left e(エラー)または Right a(成功)。
safeDiv :: Int -> Int -> Maybe Int
safeDiv _ 0 = Nothing
safeDiv a b = Just (a `div` b)
safeDiv 10 2 -- Just 5
safeDiv 10 0 -- Nothing
-- Either でエラー情報を持つ
safeSqrt :: Double -> Either String Double
safeSqrt x
| x < 0 = Left "Negative input"
| otherwise = Right (sqrt x)例外・IO
基礎副作用は IO a 型で管理する。do 記法で手続き的に書ける。<- で IO から値を取り出す。return は値を IO にラップ(Haskell の return は C の return とは違う)。
main :: IO ()
main = do
putStr "Name: "
name <- getLine
putStrLn ("Hello, " ++ name ++ "!")
let greeting = "Welcome, " ++ name
putStrLn greetingControl.Exception で例外を扱う。catch・try・throwIO。IOException はファイル I/O エラーに使う。
import Control.Exception
main :: IO ()
main = do
result <- try (readFile "missing.txt") :: IO (Either IOException String)
case result of
Right content -> putStrLn content
Left e -> putStrLn ("Error: " ++ show e)interact は stdin 全体を受け取って変換する簡便関数。getContents で遅延読み込み可能。
-- 行を逆順にするプログラム
main :: IO ()
main = interact (unlines . reverse . lines)型クラス
基礎型クラスは「ある型が持つべきインターフェース」を定義する。class で定義し、instance で実装。
class Describable a where
describe :: a -> String
data Color = Red | Green | Blue
instance Describable Color where
describe Red = "red color"
describe Green = "green color"
describe Blue = "blue color"
describe Red -- "red color"Eq(等値)・Ord(順序)・Show(文字列化)・Read(解析)・Num(数値演算)・Functor(fmap)など。deriving で自動導出。
data Point = Point Double Double
deriving (Show, Eq, Ord)
p = Point 1.0 2.0
show p -- "Point 1.0 2.0"
p == Point 1.0 2.0 -- True
-- Functor の instance
instance Functor ((,) a) where
fmap f (x, y) = (x, f y)Functor はコンテキストの中の値を変換する型クラス。fmap が核心メソッド。<$> は中置版の fmap。
fmap (+1) (Just 5) -- Just 6
fmap (+1) Nothing -- Nothing
fmap (*2) [1,2,3] -- [2,4,6](map と同じ)
fmap length (Just "hi") -- Just 2
(+1) <$> Just 5 -- Just 6(中置)deriving で Show・Eq・Ord・Generic を自動導出。GHC.Generics と組み合わせてシリアライズ(aeson など)を自動化できる。
{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics (Generic)
import Data.Aeson (ToJSON, FromJSON)
data User = User { name :: String, age :: Int }
deriving (Show, Eq, Generic)
instance ToJSON User
instance FromJSON UserApplicative・Monad
応用Applicative は文脈の中の関数を文脈の中の値に適用する。<*> と pure(値をコンテキストに入れる)が核心。
pure (+3) <*> Just 5 -- Just 8
Just (+3) <*> Just 5 -- Just 8
Nothing <*> Just 5 -- Nothing
[(+1),(* 2)] <*> [10,20] -- [11,21,20,40]Monad はコンテキストの計算をチェーンする。>>=(bind)は左辺のコンテキストから値を取り出して関数に渡す。do 記法はシンタックスシュガー。
-- Maybe モナド
safeDiv :: Int -> Int -> Maybe Int
safeDiv _ 0 = Nothing
safeDiv a b = Just (a `div` b)
compute :: Maybe Int
compute = do
x <- safeDiv 10 2 -- Just 5
y <- safeDiv x 0 -- Nothing
return (x + y) -- Nothing(途中で失敗)リストはモナドとして非決定性計算を表す。>>= が concatMap に相当。guard で条件フィルタリング。
import Control.Monad (guard)
pythagorean :: Int -> [(Int,Int,Int)]
pythagorean n = do
a <- [1..n]
b <- [a..n]
c <- [b..n]
guard (a^2 + b^2 == c^2)
return (a, b, c)
pythagorean 20 -- [(3,4,5),(5,12,13),(6,8,10),(8,15,17)]State s a は状態を引き回す計算をカプセル化。get・put・modify で状態を操作。純粋関数型で可変状態をシミュレート。
import Control.Monad.State
counter :: State Int Int
counter = do
n <- get
put (n + 1)
return n
runState (do { counter; counter; counter }) 0
-- (2, 3) ← 最後の戻り値=2, 最終状態=3ジェネリクス・型レベルプログラミング
応用型変数(a・b)で任意の型に対して動作するジェネリック関数を定義。
id :: a -> a
id x = x
const :: a -> b -> a
const x _ = x
flip :: (a -> b -> c) -> b -> a -> c
flip f y x = f x y型の型を「kind」と呼ぶ。Int の kind は *(= Type)、Maybe の kind は * -> *(型を受け取って型を返す)。
-- :kind コマンドで確認(GHCi)
-- :k Int → Type
-- :k Maybe → Type -> Type
-- :k Either → Type -> Type -> Type
-- :k Functor → (Type -> Type) -> Constraint一般化代数データ型。コンストラクタごとに異なる型索引を付けられる。型安全な AST の表現などに使う。
{-# LANGUAGE GADTs #-}
data Expr a where
Lit :: Int -> Expr Int
Bool :: Bool -> Expr Bool
Add :: Expr Int -> Expr Int -> Expr Int
If :: Expr Bool -> Expr a -> Expr a -> Expr a
eval :: Expr a -> a
eval (Lit n) = n
eval (Bool b) = b
eval (Add x y) = eval x + eval y
eval (If p t f) = if eval p then eval t else eval fよく使うモナドとトランスフォーマー
応用Reader r a は読み取り専用の環境 r を引き回す計算。依存注入や設定の受け渡しに使う。
import Control.Monad.Reader
data Config = Config { host :: String, port :: Int }
getUrl :: Reader Config String
getUrl = do
h <- asks host
p <- asks port
return $ h ++ ":" ++ show p
runReader getUrl (Config "localhost" 8080)
-- "localhost:8080"Writer w a はログなどの出力を蓄積しながら計算を進める。tell でログを追記する。
import Control.Monad.Writer
loggedAdd :: Int -> Int -> Writer [String] Int
loggedAdd x y = do
tell ["Adding " ++ show x ++ " and " ++ show y]
return (x + y)
runWriter (loggedAdd 3 4)
-- (7, ["Adding 3 and 4"])StateT・ReaderT・ExceptT でモナドをスタックし、複数の効果を組み合わせる。lift で下位のモナドの操作を持ち上げる。
import Control.Monad.State
import Control.Monad.Except
type App a = ExceptT String (State Int) a
increment :: App ()
increment = do
n <- lift get
if n >= 10 then throwError "Limit reached"
else lift $ put (n + 1)
runState (runExceptT increment) 0
-- (Right (), 1)IO と副作用の管理
基礎IORef a は IO の中で使えるミュータブル参照。ST モナドは純粋計算内でのみ使えるミュータブル状態。
import Data.IORef
main :: IO ()
main = do
ref <- newIORef (0 :: Int)
modifyIORef ref (+1)
modifyIORef ref (+1)
val <- readIORef ref
print val -- 2STM(Software Transactional Memory)は TVar を使ったデッドロックフリーの並行状態管理。async ライブラリで並行タスクを管理。
import Control.Concurrent.STM
import Control.Concurrent.Async
main :: IO ()
main = do
counter <- newTVarIO (0 :: Int)
let increment = atomically $ modifyTVar' counter (+1)
(a, b) <- concurrently (mapM_ (\_ -> increment) [1..1000])
(mapM_ (\_ -> increment) [1..1000])
final <- readTVarIO counter
print final -- 2000unsafePerformIO は IO を純粋関数として実行する「脱出ハッチ」。型システムを破るため、誤用するとバグの原因になる。使用は極めて限定的にする。
import System.IO.Unsafe
-- 注意: これは例示。基本的に使うべきではない
globalRef :: IORef Int
globalRef = unsafePerformIO (newIORef 0)
{-# NOINLINE globalRef #-}unsafePerformIO は参照透過性を破壊する。FFI やグローバルな IORef の初期化など、安全性が保証できる場合のみ使用すること。ファイル・テキスト処理
基礎readFile・writeFile・appendFile で基本的なファイル操作。System.IO の Handle で詳細制御。
import System.IO
main :: IO ()
main = do
content <- readFile "input.txt"
let processed = map toUpper content
writeFile "output.txt" processed
appendFile "log.txt" "done\n"Data.Text は Unicode テキスト処理に最適化。Data.ByteString はバイナリデータ用。本番コードでは String より推奨。
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
main :: IO ()
main = do
content <- TIO.readFile "input.txt"
let upper = T.toUpper content
TIO.putStrLn upperaeson は Haskell の標準 JSON ライブラリ。Generic と組み合わせてボイラープレートなしで JSON 変換できる。
{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics
import Data.Aeson
data User = User { name :: String, age :: Int }
deriving (Show, Generic)
instance ToJSON User
instance FromJSON User
-- encode (User "Alice" 30) → "{"name":"Alice","age":30}"ビルドツール・エコシステム
基礎cabal(Haskell の標準ビルドツール)と stack(再現性を重視したビルドツール)の2つが主流。初学者には cabal が推奨(GHCup で GHC 管理)。
# cabal
cabal init --non-interactive
cabal build
cabal run
cabal test
# stack
stack new my-project
stack build
stack runHLS(Haskell Language Server)がほぼすべての IDE の LSP バックエンド。GHCi で対話的に試せる。:type・:kind・:info が特に便利。
-- GHCi コマンド
ghci
> :type map -- map :: (a -> b) -> [a] -> [b]
> :kind Maybe -- Maybe :: Type -> Type
> :info Functor -- 型クラスの定義と全 instance
> :set +s -- 実行時間とメモリを表示Hackage は Haskell のパッケージリポジトリ。Stackage は検証済みのスナップショット(LTS・Nightly)を提供し、依存関係の整合性を保証。
# cabal.project で依存追加
# build-depends: aeson >= 2.0, text, containers
# cabal で最新版を取得
cabal update
cabal install aesonテスト・QuickCheck
基礎HUnit は Haskell の xUnit スタイルテストフレームワーク。@?= で等値アサーション。
import Test.HUnit
testAdd :: Test
testAdd = TestCase $ do
assertEqual "1+1" 2 (1+1)
assertEqual "head" 1 (head [1,2,3])
main :: IO ()
main = runTestTT testAdd >>= printプロパティ(性質)を定義し、ランダムな入力で自動テスト。Haskell 発祥で他言語にも移植された。反例が見つかると最小化して報告する。
import Test.QuickCheck
prop_revRev :: [Int] -> Bool
prop_revRev xs = reverse (reverse xs) == xs
prop_sortOrdered :: [Int] -> Bool
prop_sortOrdered xs = isSorted (sort xs)
where isSorted ys = all (uncurry (<=)) (zip ys (tail ys))
main :: IO ()
main = do
quickCheck prop_revRev
quickCheck prop_sortOrderedHspec は BDD スタイルのテストフレームワーク。describe・it・shouldBe で読みやすいテスト記述ができる。QuickCheck と統合可能。
import Test.Hspec
spec :: Spec
spec = do
describe "reverse" $ do
it "reverses a list" $
reverse [1,2,3] `shouldBe` [3,2,1]
it "is involutory" $ property $
\xs -> reverse (reverse xs) == (xs :: [Int])高度な型システム機能
応用@Type で型変数に明示的に型を渡せる。型推論が曖昧な場合に型を明確に指定できる。
{-# LANGUAGE TypeApplications #-}
read @Int "42" -- 42 :: Int
show @Double 3.14 -- "3.14"
pure @Maybe 42 -- Just 42DataKinds で値コンストラクタを型レベルに昇格。型レベルの自然数(Nat)やリスト('[])が使える。型安全な長さ付きベクターなどを実装できる。
{-# LANGUAGE DataKinds, KindSignatures #-}
import GHC.TypeLits
data Vec (n :: Nat) a where
VNil :: Vec 0 a
VCons :: a -> Vec n a -> Vec (n + 1) a
-- vHead :: Vec (n+1) a -> a(空ベクターは型エラー)
vHead :: Vec (1 + n) a -> a
vHead (VCons x _) = xlens ライブラリでネストしたデータ構造の getter/setter を型安全に合成できる。view・over・set で操作。
import Control.Lens
data Address = Address { _city :: String }
data Person = Person { _name :: String, _address :: Address }
makeLenses ''Address
makeLenses ''Person
alice = Person "Alice" (Address "Tokyo")
view (address . city) alice -- "Tokyo"
over (address . city) (++ "!") alice -- Address "Tokyo!"パフォーマンス・実践
応用遅延評価はサンクの蓄積でメモリを消費することがある。!(BangPatterns)・seq・deepseq・foldl' で正格評価を強制できる。
{-# LANGUAGE BangPatterns #-}
import Control.DeepSeq
-- 正格な foldl'(サンク蓄積を防ぐ)
import Data.List (foldl')
sum' :: [Int] -> Int
sum' = foldl' (+) 0
-- BangPatterns で引数を正格評価
loop :: !Int -> Int -> Int
loop acc 0 = acc
loop acc n = loop (acc + n) (n - 1)criterion は統計的に精密な Haskell のベンチマークライブラリ。WHNF(弱頭部正規形)と NF(正規形)の評価を区別して計測できる。
import Criterion.Main
main :: IO ()
main = defaultMain
[ bench "foldl'" $ whnf (foldl' (+) 0) [1..10000 :: Int]
, bench "sum" $ nf sum [1..10000 :: Int]
]GHC のプロファイリングフラグでメモリ・時間のコストを計測。+RTS -p -h でコスト集計とヒープグラフを生成。
# プロファイリングビルド
cabal build --enable-profiling
# 実行時にプロファイリング
./my-program +RTS -p -h -RTS
# 生成ファイル
# my-program.prof → コスト集計
# my-program.hp → ヒープグラフ(hp2ps で可視化)GHC 拡張で Haskell 標準の機能を拡張できる。OverloadedStrings・LambdaCase・RecordWildCards は特に使用頻度が高い。
{-# LANGUAGE OverloadedStrings #-} -- 文字列リテラルを多相的に
{-# LANGUAGE LambdaCase #-} -- \case で無名パターンマッチ
{-# LANGUAGE RecordWildCards #-} -- let {..} = record でフィールド展開
{-# LANGUAGE TupleSections #-} -- (,3) = \x -> (x,3)
{-# LANGUAGE ScopedTypeVariables #-} -- 型変数をスコープに持ち込む