cv.cm

コーディング · プログラミング言語

Haskell入門:初心者向けチュートリアル

約11分で読めます

結論

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
型例補足
Int42固定サイズの整数(一般的なプラットフォームのGHCでは64ビット)
Integer2 ^ 100任意精度の整数
Double3.1464ビットの浮動小数点数
BoolTrue, 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は選別されたパッケージのスナップショットを追加します。初心者はどちらでも問題ありません。

コーディング講座の一覧