Open18

[Book]すごいHaskellたのしく学ぼう (純粋関数型言語)

AirichanAirichan

その前に補足:そもそも関数型言語とは & 命令型言語との違いは

関数型言語の特徴

  • "強い制約を課して、本来すべきことに集中"
  • 主に「不変性」や「副作用の排除」を重視する
    • 副作用:関数や処理が外部の状態を変更したり、外部から影響を受けたりすること
      ⭐️ 副作用の排除 = 状態と振る舞いを切り離す
項目 関数型 命令型
設計思想 「何をすべきか」を記述 「どうやるか」を指示
状態管理 不変(immutable) 可変(mutable)
副作用 基本的にない(管理する必要がある) 必要に応じて自由に行う
関数の定義 数学における計算を行うもの
足りない部分はアクション・モナドとして用意
手続きを1まとめにしたもの
出力、計算、接続...基本的になんでもできる
制御構造 関数の組み合わせ ループや条件分岐
Haskell, Scala, F# C, Java, Python (一部)

※ ここから詳しくは読みながら詳しく書いていく

関数型言語の分類

静的 動的
純粋関数型言語 Haskell、Miranda -
非純粋関数型言語 Scala、OCaml、F#、ML Lisp、Scheme、Erlang
AirichanAirichan

イントロダクション

Haskellって何なの

  • 純粋関数型言語
    • 静的型付け言語であり型推論を持っている。

    • "何であるか"のみ伝える

    • 変数はどんな時でも不変 (変数の緊縛(binding)という)

    • 副作用は持たない

      • 関数にできることは"何かを計算して返すこと"だけ。

      = "参照透明性" という。

    • 遅延評価を行う

      • 簡単にいうならば結果が必要になるまで関数は実行しない。
AirichanAirichan

環境準備

brewでインストールできる

brew install ghc
AirichanAirichan

初めの第1歩

Haskellのコンパイラ: GHC (Glasgow Haskell Compiler)

GHCとは?GHCの構成

資料
▶︎wiki
▶︎GHC公式

ghci

  • 実際にGHCをダウンロードして使ってみよう。
    • Homebrewでもできる!

ghciと打つと以下のように出る。

ダウンロードすると最新版が入ってるはず。今はversion 9.12.1

1-1. 関数呼び出し

Haskell の関数(基本の関数)について。

  • 関数には前置関数中置関数がある。(Haskellの関数は通常前置記法で使用される。)

    • 中置関数: 2つの引数の間に関数名を置く形式
      • ex) 算術演算子(+, -, *, /など)や、リスト連結演算子 (++) も中置関数の例!
        8 * 5 (結果は40)
    • 前置関数: 関数名が引数の前に来る形式。
         前置関数の呼び出しは、関数名の後にスペースで区切って引数を並べるだけ
      • ex) succ 8 (結果は9)
      • ex) max 8 9 (結果は9)
  • 関数適用の優先順位について:
    関数適用が他のすべての演算(この場合は + 演算)よりも高い優先順位を持つということ。

ghci> succ 9 + max 5 4 + 1
16
ghci> ( succ 9 ) + ( max 5 4 ) + 1
16

上記2つの式は等価である。
上記の上の例で説明すると、「関数が優先」= succ関数とmax関数が優先して計算されるため、
succ 9 = 10, max 5 4 = 5,が先に計算されるため、答えが16になる。
要するに上記例の下の式と同じなのだ。

  • 関数を中置関数として扱いたいときはバッククォート``を使用する
AirichanAirichan

1-2. 赤ちゃんの最初の関数

▶︎ 関数を作って読み込み実行する

  • .hsフィルを作成、関数を書いてみる
(ex)haskell-study.hs
doubleMe x = x + x
  • そのファイルを作成したディレクトリでghciを起動
  • 作成ファイルを読み込むには以下のコマンドを実施
ghci> :l ファイル名
<!-- もしくは以下のように -->
ghci> :load ファイル名
  • 実行してみる
ghci> doubleMe 9
18
ghci> doubleMe 8.3 
16.6

▶︎ 編集し再度読み込む

今度はさっき作ったファイルに他の関数も作成する

haskell-study.hs
<!-- さっき書いた関数 -->
doubleMe x = x + x

<!-- 二つの引数をそれぞれ2倍してから足し合わせる関数 -->
doubleUs x y = x * 2 + y * 2
ghci> doubleUs 4 9
26
ghci> doubleUs 28 88 + doubleMe 123
478

▶︎ 組み合わせる

さっきの関数を少々変えてみる。
doubleUs x y = x * 2 + y * 2の x * 2 は、先に定義したdoubleMeのことだ。よって以下のようにできる

haskell-study.hs
doubleMe x = x + x
doubleUs x y = doubleMe x + y * 2
AirichanAirichan

▶︎ Haskellのif式:式としてのif | 命令型言語との違い

if式を書いてみる

haskell-study.hs
doubleSmallNumber x = if x > 100
                    then x
                    else x * 2 

result x = if x > 0 then "Positive" else "Non-positive"

Haskellは関数型言語だ。ifは式として作成するため
必ず返り値を定義する必要がありelse節が必須だ。

ポイントは、if式全体が関数内で値を返す1つの式として評価されることだ。

→ 命令型言語であればelse節を省略してif文として書くことができ、
ifは以下のように書くことができ評価しないということもあり得るが、
if (x > 100) { println("x is large");}
関数型言語ではそれはあり得ない。実際省略するとコンパイルエラーが起きるよ

補足:式と文

以下に、式と文の主な違いをコピーできる形式でMarkdownテーブルにまとめました:

特徴 式(Expression) 文(Statement)
値の生成 常に値を返す 必ずしも値を返さない
組み合わせ 他の式の一部として使用可能 通常、他の文に直接埋め込めない

shellで直打ちするなら以下のように囲う必要があるよ

ghci> doubleSmallNumber' x = (if x > 100 then x else x * 2)
AirichanAirichan

🌱 Haskell における '(アポストロフィ) について

  • Haskellでは関数名にアポストロフィ( ')を使うことができ
    慣習的に正格(遅延じゃない)版の関数を表したり、
    少し変更した場合に元の関数名に似た名前にするために使割れることが多い

特別な構文上の意味は持たない

⚠️ Haskellでの関数の注意点

  • 関数名を大文字で始めないこと。

    • Haskell では、関数名や変数名は小文字で始める 必要があり,
      大文字で始まるのは 型 や データコンストラクタです。
  • 関数は少なくとも1つの引数を受け取ること

    • 関数は 引数なしでは定義できない。(少なくとも () を返す必要がある)
    • 引数を受け取らずに固定の値を持つものは 関数ではなく"定数"もしくは"名前" だ。
AirichanAirichan

📖 Haskell における '(シングルクォート)と "(ダブルクォート)

🌱 'A'(シングルクォート)は Char 型

  • 'A' のようにシングルクォートで囲まれたものは、1つの文字 (Char 型) を表す。
  • 1文字のみ を扱う場合に使用。

🌱 "A"(ダブルクォート)は String 型

  • "A" のようにダブルクォートで囲まれたものは、文字列 (String 型) を表す。
  • Haskell において String は [Char](Char のリスト) として実装されている。
  • 複数の文字を扱う場合 に使用。
AirichanAirichan

1-3. HaskellのList入門

Listについて書く前に....

  • GHCIの中で名前定義はletを使用すること
  • 最初に書いたように定義した変数は不変だよ。
  • Listは一様なデータ構造であること(= 要素が同一データ型であること)
ghci> let lostNumber = [4,8,15,16,23,42]
ghci> lostNumber
[4,8,15,16,23,42]

いろんなリスト操作

🌱 連結 : ++演算子

ghci> [1,2,3,4] ++ [5,6,7,8]
[1,2,3,4,5,6,7,8]

⚠️長いリストに対しては、最初のリストへ++をずっとするので注意が必要。

🌱 リストの先頭に単一要素を追加する: 演算子(cons演算子)

  • 引数を一つ受け取る
  • 第一引数(以下でいう'A')は追加しようとしてるリストの要素の型と同じ型の単一要素
ghci> 'A':" SMALL CAT"
"A SMALL CAT"
ghci> 5:[1,2,3,4,5]
[5,1,2,3,4,5]

※Haskell における '(シングルクォート)と "(ダブルクォート)

🌱 リスト要素へのアクセス

  • 先頭から取得したい: !!演算子

要素数を超えたらエラーになるよ

ghci> "Steve Buscemi" !! 6
'B'
ghci> [9.4,33.2,96.2,11.2,23.25] !! 1
33.2
  • リスト中のリスト
ghci> let b = [[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]
ghci> b
[[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]
ghci> b ++ [[1,1,1,1]]
[[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3],[1,1,1,1]]
ghci> [6,6,6]:b
[[6,6,6],[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]
ghci> b !! 2
[1,2,2,3,4] 
AirichanAirichan
  • リスト中の比較について

中の要素が比較可能であればリストも比較可能なのだが、
比較の仕方は、以下のように行われる。
①まず最初の要素同士を比較
②それらが等しいなら2番目の要素を比較
③それらが等しいなら3番目の要素を比較

(繰り返し)

ghci> [3,2,1] > [2,1,0]
True
ghci> [3,2,1] > [2,10,100]
True
ghci> [3,4,2] > [3,4]
True
ghci> [3,4,2] > [2,4]
True
ghci> [3,4,2] == [3,4,2]
True
  • 等値比較 (== と /=)

リストが 全く同じ要素を持っている なら == で True になります。異なる場合は False

AirichanAirichan

🌱 Haskellのリスト操作関数

⚠️ 重要な注意点

Haskellの多くのリスト操作関数は空リスト(カラリスト)を渡すと実行時エラーになります。これらの関数を使用する前に、リストが空でないか確認するか、Maybe型やパターンマッチングを使うなどの対策が必要です。

🍃 基本的なリスト操作関数

関数 説明 空リスト時の挙動
head リストの最初の要素を返す head [1,2,3]1 エラー *** Exception: Prelude.head: empty list
tail 最初の要素を除いたリストを返す tail [1,2,3][2,3] エラー *** Exception: Prelude.tail: empty list
last リストの最後の要素を返す last [1,2,3]3 エラー *** Exception: Prelude.last: empty list
init 最後の要素を除いたリストを返す init [1,2,3][1,2] エラー *** Exception: Prelude.init: empty list
length リストの長さを返す length [1,2,3]3 安全:length []0
null リストが空かどうかを判定 null [1,2,3]False 安全:null []True
reverse リストを逆順にする reverse [1,2,3][3,2,1] 安全:reverse [][]
take 先頭からn個の要素を取る take 2 [1,2,3][1,2] 安全:take n [][]
drop 先頭からn個の要素を除いたリストを返す drop 2 [1,2,3][3] 安全:drop n [][]
maximum リスト内の最大値を返す maximum [1,5,3]5 エラー *** Exception: Prelude.maximum: empty list
sum リスト内の要素の合計を返す sum [1,2,3]6 安全:sum []0
elem 要素がリストに含まれるか判定 elem 2 [1,2,3]True 安全:elem x []False

💡 安全なリスト操作のパターン

空リストによるエラーを避けるために、以下のパターンを使用することができます。

  1. パターンマッチングを使用する
safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:_) = Just x

safeTail :: [a] -> [a]
safeTail [] = []
safeTail (_:xs) = xs
  1. nullでリストが空かどうかを事前に確認する
getFirstElement xs = if null xs then error "Empty list" else head xs
  1. Data.Maybeモジュールの関数を利用する
import Data.Maybe (listToMaybe)

-- listToMaybeは安全にheadの代わりになる
safeHead' = listToMaybe  -- safeHead' [1,2,3] → Just 1, safeHead' [] → Nothing

📚 実践的な例

-- リストが空でないことを確認してから操作
processNonEmptyList :: [Int] -> Int
processNonEmptyList xs
  | null xs = 0  -- 空リストの場合は0を返す
  | otherwise = head xs + last xs  -- 最初と最後の要素の合計

-- Maybeを使った安全な操作
safeLast :: [a] -> Maybe a
safeLast [] = Nothing
safeLast xs = Just (last xs)

-- 複数の関数を組み合わせる
takeReverse :: Int -> [a] -> [a]
takeReverse n = reverse . take n  -- n個取って逆順にする

🔍 注意すべきケース

  1. head, tail, last, init, maximum, minimumは空リストで必ずエラーになります
  2. リスト内包表記やmap, filterなどの高階関数を使うと、多くの場合より安全にリスト操作ができます
  3. 再帰関数を書く際は、必ず空リストのケースを考慮してください

これらの関数を使う前に、リストが空かどうかをnull関数で確認するか、Maybe型を利用したパターンマッチングを行うことで、実行時エラーを防ぐことができます。

AirichanAirichan

1-4. レンジでチン!

リスト範囲表記(Range)

Haskellでは、連続した数値やその他の列挙可能な値のリストを簡単に生成するための
範囲表記(Range)という機能がある。

🌱基本的な範囲表記

[start..end]という構文で連続した値のリストを生成可能。

-- [1,2,3,4,5]  開始値から終了値までの連続した値
ghci> [1..5]
[1,2,3,4,5]

-- "abcde"      文字も範囲表記が使える
ghci> ['a'..'e']
"abcde"
  • ⚠️ 減少列を作りたい時は、注意!
-- 不正(空リスト!自動的に降順にはならない)
ghci> [5..1]
[]

-- 正しい。
ghci> [5,4..1]
[5,4,3,2,1]

🌱ステップ(増分)の指定

最初の2つの要素をカンマで区切って記述し、その後に上限値を指定

[1,3..10]     -- [1,3,5,7,9]    2つごとに増加(増分は2)
[2,4..10]     -- [2,4,6,8,10]   2つごとに増加
[5,10..25]    -- [5,10,15,20,25] 5つごとに増加
[10,9..1]     -- [10,9,8,7,6,5,4,3,2,1] 減少する範囲

🌱無限リスト

上限を省略すると無限リストになります

[1..]       -- 1から始まる無限リスト [1,2,3,...]
[2,4..]     -- 2から始まる偶数の無限リスト [2,4,6,...]

Haskellは遅延評価(lazy evaluation)を採用しているため、
無限リストを扱うことができる。
無限リストは必要な部分だけが評価されるため、take!!などの関数と組み合わせて使用します。

(Ex. )

-- 最初の24個の13の倍数を取得する
take 24 [13,26..]  -- 13から始まる13の倍数の無限リストから24個取得
-- 以下も同じ意味
[13,26..24*13]     -- 上限を計算して範囲指定

他、便利な無限リスト生成関数

  1. cycle - リストを無限に繰り返す

    take 10 (cycle [1,2,3])   -- [1,2,3,1,2,3,1,2,3,1]
    take 12 (cycle "LOL ")    -- "LOL LOL LOL "
    
  2. repeat - 要素を無限に繰り返す

    take 10 (repeat 5)        -- [5,5,5,5,5,5,5,5,5,5]
    
  3. replicate - 要素を指定回数繰り返す(repeattakeの組み合わせと同等)

    replicate 10 5           -- [5,5,5,5,5,5,5,5,5,5]
    replicate 3 "hello"      -- ["hello","hello","hello"]
    

関連する便利な関数

範囲表記は内部的に以下の関数に変換されます:

  • enumFrom x[x..]と同等

    enumFrom 1        -- [1,2,3,4,5...](無限リスト)
    enumFrom 'a'      -- "abcdef..."(無限リスト)
    
  • enumFromTo x y[x..y]と同等

    enumFromTo 1 10   -- [1,2,3,4,5,6,7,8,9,10]
    enumFromTo 'a' 'f' -- "abcdef"
    
  • enumFromThen x y[x,y..]と同等(xとyの差がステップになる)

    enumFromThen 1 3    -- [1,3,5,7,9...](無限リスト、ステップ2)
    enumFromThen 5 2    -- [5,2,-1,-4...](無限リスト、ステップ-3)
    
  • enumFromThenTo x y z[x,y..z]と同等

    enumFromThenTo 1 3 10    -- [1,3,5,7,9]
    enumFromThenTo 10 8 1    -- [10,8,6,4,2]
    

これらの関数はEnum型クラスのメソッドだから、
IntCharFloatなどEnumのインスタンスとなる型で使用可能。

🌱 浮動小数点数との使用

⚠️ 丸め誤差に注意が必要

[0.1, 0.3 .. 1.0]  -- [0.1,0.3,0.5,0.7,0.9]
AirichanAirichan

1-5. リスト内包表記

数学の集合表記(集合の内包的記法)に似た記法で、
リストのフィルタリング、変換、組み合わせを行う方法。

🌱基本構文

「|」(縦線)の
左側に出力する式を、
右側に条件(ジェネレーター(要素 <- リスト))を記述する。

[| 要素 <- リスト, 条件1, 条件2, ...]

👀 Ex.

-- 1から10までの数の2倍のリスト
ghci> [x * 2 | x <- [1..10]]
[2,4,6,8,10,12,14,16,18,20]


-- 1から10までの偶数のみの2倍のリスト
ghci> [x * 2 | x <- [1..10], even x]
[4,8,12,16,20]

-- 条件を複数指定(2と3の両方で割り切れる数)
ghci> [x | x <- [1..20], x `mod` 2 == 0, x `mod` 3 == 0]
[6,12,18]

AirichanAirichan

1-6.タプル

  • 異なる型の要素を固定長のグループとして扱うためのデータ構造
  • リストとは異なり、異なる型の値を格納でき(=ヘテロ)、長さは不変
  • 空のタプル () は"ユニット"と呼ばれ、値を持たないことを示す(他の言語の void に相当)

🌱 基本構文

-- 2要素のタプル(ペア)
pair :: (Int, String)
pair = (1, "apple")

-- 3要素のタプル(トリプル)
triple :: (Int, String, Bool)
triple = (2, "banana", True)

🌱タプルへのアクセス方法

  1. パターンマッチング
-- タプルの分解
(a, b) = (1, "apple")  -- a = 1, b = "apple"

-- 関数の引数でのパターンマッチング
sumPair :: (Int, Int) -> Int
sumPair (x, y) = x + y
  1. アクセス関数(ペア専用)
fst (1, "apple")   -- 1 (1番目の要素を取得)
snd (1, "apple")   -- "apple" (2番目の要素を取得)
AirichanAirichan

2章 型を信じろ!

  • Haskellは静的型付け(=すべての式の型がコンパイル時に決定される)
  • Haskellは型推論をサポートしている
    • Java や Pascal と異なり、明示的に型を宣言する必要がない
    • コンパイラが式から自動的に型を推測できる

2-1. 明示的な型宣言

  • ::は「〜型を持つ」と読む

  • 明示的な型は常に大文字で始まる

  • GHCiでの型の確認
    :tコマンドを使用すると、式の型を確認できる

ghci> :t 'a'
'a' :: Char

ghci> :t True
True :: Bool

ghci> :t "HELLO!"
"HELLO!" :: [Char]  -- 文字列は文字のリスト

ghci> :t (True, 'a')
(True, 'a') :: (Bool, Char)  -- タプルは各要素の型で決まる

ghci> :t 4 == 5
4 == 5 :: Bool  -- 比較式はブール型を返す

🌱関数の型宣言

-- 文字列から大文字のみを残す関数
removeNonUppercase :: [Char] -> [Char]  -- または String -> String
removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]

-- 3つの整数を加算する関数
addThree :: Int -> Int -> Int -> Int
addThree x y z = x + y + z
AirichanAirichan

2-2. 一般的なHaskellの型

🌱 Int

  • 固定幅の整数型(通常32ビットシステムでは -2147483648 から 2147483647 まで)
     =有界
haskellCopyminInt :: Int
minInt = -2147483648

🌱 Integer

  • 任意精度の整数型(制限なし、非常に大きな数値を扱える)
haskellCopyfactorial :: Integer -> Integer
factorial n = product [1..n]

-- factorial 50 は非常に大きな数値になる
ghci> factorial 50
30414093201713378043612608166064768844377641568960512000000000000

🌱Float

  • 単精度浮動小数点数
haskellCopycircumference :: Float -> Float
circumference r = 2 * pi * r

ghci> circumference 4.0
25.132742

🌱Double

  • 倍精度浮動小数点数
haskellCopycircumference' :: Double -> Double
circumference' r = 2 * pi * r

🌱Bool

  • 論理値型(TrueまたはFalse)

🌱Char: 文字型

  • 単一の文字、シングルクォートで表記

🌱Tuples: タプル型

  • 長さと要素の型に依存
  • 空のタプル () は「ユニット型」