コーディング · プログラミング言語
Haskell入門:初心者向けチュートリアル
結論
Haskellは、型推論と遅延評価を備えた、純粋関数型の静的型付け言語です。プログラムは純粋関数、代数的データ型、パターンマッチで組み立てられ、副作用はIOなどの型で追跡されます。このページでは、GHCのインストール、型、関数とガード、リストと高階関数、データ型、型クラス、MaybeとEither、そして完成したサンプルを扱います。
Haskellとは何か、何に使うのか
Haskellは、強力な静的型システム、型推論、遅延評価を備えた純粋関数型プログラミング言語です。研究者の委員会によって設計され、1990年に最初の仕様が定義されました。現在の標準はHaskell 2010で、主要な実装はGHC(Glasgow Haskell Compiler)です。Haskellでは、型で示されない限り関数に副作用はないため、プログラムの挙動を推論しやすく、テストもしやすくなります。
Haskellは、金融やフィンテック、コンパイラやプログラミング言語のツール(PandocやリンターのShellCheckはHaskellで書かれています)、ブロックチェーンのプロジェクト、Webバックエンド、研究などで使われています。代数的データ型、型クラス、純粋関数といった考え方が、Rust、Swift、Scala、Kotlin、TypeScriptに影響を与えたことから、学ぶ人も多くいます。
GHCのインストールとHello, worldの実行
おすすめのインストーラーはGHCupです。GHC、ビルドツールのCabalとStack、エディター向けのHaskell Language Serverをインストールします。すべてのプログラムには、IO ()型のmainアクションがあります。
-- hello.hs
main :: IO ()
main = putStrLn "Hello, world!"runghc hello.hs # run without compiling
# Hello, world!
ghc hello.hs && ./hello # compile to a native executable
ghci # interactive REPL; load a file with :load hello.hsコメントは--で始まります。GHCiでは:type expression(または:t)で任意の式の型が表示され、これは学習にとても役立ちます。
値・型・型推論
名前は一度束縛されると変わりません。変更可能な変数はありません。型シグネチャは::で書きます。コンパイラが型を推論するので省略できますが、トップレベルの定義には書くのが良い習慣です。
name :: String
name = "Haskell"
year :: Int
year = 1990
ratio :: Double
ratio = fromIntegral year / 1000 -- convert Int to Double explicitly
main :: IO ()
main = do
putStrLn (name ++ " " ++ show year) -- Haskell 1990
print ratio -- 1.99
print (7 `div` 2, 7 `mod` 2) -- (3,1)
print (2 ^ 64 :: Integer) -- 18446744073709551616| 型 | 例 | 補足 |
|---|---|---|
Int | 42 | 固定サイズの整数(一般的なプラットフォームのGHCでは64ビット) |
Integer | 2 ^ 100 | 任意精度の整数 |
Double | 3.14 | 64ビットの浮動小数点数 |
Bool | True, False | コンストラクターは大文字で始まります |
Char | 'h' | Unicodeの1文字 |
String | "text" | Charのリスト[Char]。Data.Textのほうが高速です |
showは値をStringに変換し、printはputStrLn . showです。数値が暗黙に変換されることはありません。fromIntegral、round、truncateを使ってください。
関数・ガード・パターンマッチ
関数の適用は、かっこではなく空白で書きます:add 2 3。どの関数も引数を1つ受け取り、残りを受け取る別の関数を返します。これをカリー化といい、add 2自体が関数です。パターンマッチとガードを使うと、ifの連鎖の大半を置き換えられます。
add :: Int -> Int -> Int
add x y = x + y
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)
grade :: Int -> Char
grade score
| score >= 90 = 'A'
| score >= 70 = 'B'
| otherwise = 'C'
describe :: [a] -> String
describe [] = "empty"
describe [_] = "one item"
describe _ = "many items"
main :: IO ()
main = do
print (add 2 3) -- 5
print (map (add 10) [1, 2, 3]) -- [11,12,13] (partial application)
print (factorial 20) -- 2432902008176640000
print (grade 75) -- 'B'
putStrLn (describe "hi") -- many items
print (if even 4 then "even" else "odd") -- "even"ifは式で、必ずelseが必要です。ループはありません。繰り返しは再帰か、map、filter、foldrといった高階関数で行います。
リスト・タプル・高階関数
リストは1種類の型の値を保持し、最もよく使うコレクションです。タプルは、異なる型でもよい固定個数の値をまとめます。リスト内包表記、範囲、関数合成演算子.を使うと、データ処理を簡潔に書けます。
import Data.List (sortOn)
main :: IO ()
main = do
let nums = [5, 3, 8, 1] :: [Int]
print (head nums, length nums, sum nums) -- (5,4,17)
print (0 : nums) -- [0,5,3,8,1] (prepend)
print (filter even nums) -- [8]
print (foldr (+) 0 [1 .. 10]) -- 55
print [x * x | x <- [1 .. 5], odd x] -- [1,9,25]
print (take 5 [1, 3 ..]) -- [1,3,5,7,9] (lazy infinite list)
let people = [("Ann", 31), ("Bo", 25)] :: [(String, Int)]
print (map fst (sortOn snd people)) -- ["Bo","Ann"]
print ((length . filter (> 2)) nums) -- 3
print (zip [1 :: Int ..] "abc") -- [(1,'a'),(2,'b'),(3,'c')]Haskellは遅延評価なので、値は必要になったときに初めて計算されます。そのためtake 5 [1, 3 ..]は無限リストでも動きます。一方で、遅延によって未評価の大きな式がたまることもあります。長いリストの合計には、foldlよりもData.Listのfoldl'を使いましょう。
代数的データ型と型クラス
独自の型はdataで定義します。型は複数のコンストラクターを持て(直和型)、それぞれがフィールドを持てます(直積型)。Show、Eq、Ord、Functorといった型クラスは、インターフェースに似た共通の振る舞いを表し、derivingでよく使うインスタンスを自動生成できます。
data Shape = Circle Double | Rect Double Double
deriving (Show, Eq)
area :: Shape -> Double
area (Circle r) = pi * r * r
area (Rect w h) = w * h
data User = User { userName :: String, userAge :: Int }
deriving Show
class Describable a where
describe :: a -> String
instance Describable Shape where
describe (Circle _) = "a circle"
describe (Rect _ _) = "a rectangle"
main :: IO ()
main = do
print (map area [Circle 1, Rect 2 3]) -- [3.141592653589793,6.0]
putStrLn (describe (Rect 1 1)) -- a rectangle
let u = User { userName = "Ann", userAge = 31 }
print (u { userAge = 32 }) -- User {userName = "Ann", userAge = 32}Maybe・Either・IO
Haskellにはnullがなく、純粋なコードには検査なしの例外もありません。値がないかもしれない場合はMaybe a型(Just xまたはNothing)、理由付きで失敗するかもしれない計算はEither e a型(Left errまたはRight x)です。入力の読み取りなどの副作用はIO型に置かれ、do記法で順番につなげます。
import Text.Read (readMaybe)
safeDiv :: Int -> Int -> Either String Int
safeDiv _ 0 = Left "division by zero"
safeDiv x y = Right (x `div` y)
parseAge :: String -> Maybe Int
parseAge s = case readMaybe s of
Just n | n >= 0 -> Just n
_ -> Nothing
main :: IO ()
main = do
print (safeDiv 10 2) -- Right 5
print (safeDiv 1 0) -- Left "division by zero"
print (parseAge "42") -- Just 42
print (parseAge "abc") -- Nothing
print (fmap (+ 1) (Just 41)) -- Just 42
putStrLn "Your name?"
n <- getLine -- an IO action that returns a String
putStrLn ("Hi, " ++ n)小さな完成プログラム:単語の出現頻度
このプログラムは、標準入力からテキストを読み、Mapで単語の出現頻度を数えて、よく出る上位3語を表示します。関数合成、GHCに同梱されているcontainersパッケージのData.Map、並べ替えを使っています。
import Data.Char (isAlpha, toLower)
import Data.List (sortBy)
import Data.Ord (comparing, Down (..))
import qualified Data.Map.Strict as Map
wordFreq :: String -> [(String, Int)]
wordFreq =
sortBy (comparing (Down . snd) <> comparing fst)
. Map.toList
. Map.fromListWith (+)
. map (\w -> (w, 1))
. words
. map (\c -> if isAlpha c then toLower c else ' ')
main :: IO ()
main = do
text <- getContents
mapM_ (\(w, n) -> putStrLn (w ++ ": " ++ show n)) (take 3 (wordFreq text))echo "The cat and the dog. The dog sat, and the cat ran." | runghc wordfreq.hs
# the: 4
# and: 2
# cat: 2次に学ぶこと
- haskell.orgのドキュメントページ。チュートリアル、書籍、GHC User's Guideへのリンクがあります。
- パッケージリポジトリのHackageと、名前や型で関数を探せる検索エンジンのHoogle。
- 『Learn You a Haskell for Great Good!』と『Real World Haskell』。どちらも無料で読めます。
- 言語の正確な定義であるHaskell 2010 Language Report。
よくある質問
Haskellは習得が難しいですか?
変更可能な変数もループもなく、表現力の高い型システムを使うため、最初は違和感があります。再帰、パターンマッチ、型を一歩ずつ学べば、中核となる言語は小さく一貫していると感じる人が多いです。
純粋関数型とはどういう意味ですか?
関数は同じ入力に対して常に同じ結果を返し、状態を変えたり入出力を行ったりしません。副作用を扱えないわけではなく、IOなどの型で表現されます。
Haskellのモナドとは何ですか?
モナドは、Maybe、Either、IOなどの型に対する型クラスで、>>=による計算の連鎖をサポートします。do記法は、その連鎖のための構文です。
遅延評価とは何ですか?
式は、その値が必要になったときに初めて評価されます。無限リストを扱え、無駄な計算も避けられますが、メモリ使用量の予測が難しくなることがあります。
StackとCabalのどちらを使うべきですか?
どちらもHaskellプロジェクトをビルドでき、GHCupでインストールできます。Cabalは標準のビルドツールで、Stackは選別されたパッケージのスナップショットを追加します。初心者はどちらでも問題ありません。