Skip to content

第 11 章: 不変データとコレクション処理

11.1 はじめに

前章ではパイプライン演算子と関数合成を学びました。この章では F# の 不変データ の考え方と、List モジュール / Seq モジュール によるコレクション処理を深掘りします。

11.2 F# の不変性

F# では変数はデフォルトで 不変(immutable) です。

let x = 5        // 不変(デフォルト)
let mutable y = 10  // 可変(明示的)

Rust と同様に、不変がデフォルトの設計です。Rust では let が不変、let mut が可変ですが、F# では let が不変、let mutable が可変です。

レコード型の不変性

FizzBuzzValue レコード型はデフォルトで不変です。

let value = createValue 3 "Fizz"
// value.Value <- "Buzz"  // コンパイルエラー: レコードフィールドは変更できない

// 新しい値を作成する場合は with 式を使う
let newValue = { value with Value = "Buzz" }
Assert.Equal("Fizz", value.Value)    // 元の値は変わらない
Assert.Equal("Buzz", newValue.Value) // 新しい値が作成される

FizzBuzzList の不変設計

Add メソッドは元のリストを変更せず、新しいリストを返します。

[<Fact>]
let ``値を追加できる`` () =
    let list = emptyList
    let newList = list.Add(createValue 1 "1")
    Assert.Equal(1, newList.Count)
    Assert.Equal(0, list.Count)  // 元のリストは不変

11.3 List モジュール

F# の List モジュールはイミュータブルなリンクドリストを操作する関数群を提供します。

List.map — 要素の変換

// 各 FizzBuzzValue の Value フィールドを取得
let stringValues =
    fizzBuzzList.Values
    |> List.map (fun v -> v.Value)
// ["1"; "2"; "Fizz"; "4"; "Buzz"; ...]

List.filter — 要素の選別

// Fizz の値だけを抽出
let fizzValues =
    fizzBuzzList.Values
    |> List.filter (fun v -> v.Value = "Fizz")

List.fold — 集約

// すべての Value を改行区切りで連結
let joined =
    fizzBuzzList.Values
    |> List.map (fun v -> v.Value)
    |> List.fold (fun acc v ->
        if acc = "" then v
        else acc + "\n" + v) ""

List.fold は Rust の .fold() や Java の Stream.reduce() に相当します。初期値と累積関数を受け取り、リストの各要素を順に処理します。

List.groupBy — グルーピング

// Value でグルーピングしてカウント
let counts =
    fizzBuzzList.Values
    |> List.groupBy (fun v -> v.Value)
    |> List.map (fun (key, group) -> (key, List.length group))
    |> Map.ofList

List.tryFind — 最初の一致要素

let firstFizz =
    fizzBuzzList.Values
    |> List.tryFind (fun v -> v.Value = "Fizz")

match firstFizz with
| Some value -> printfn "Found: %s" (value.ToString())
| None -> printfn "Not found"

List.tryFindOption<'T> を返します。Rust の .find()Option<T> を返すのと同様です。

11.4 Seq モジュール

Seq モジュールは 遅延評価 されるシーケンスを操作します。

// Seq は遅延評価(必要な分だけ計算)
let firstFiveFizzBuzz =
    Seq.initInfinite (fun i -> generate Standard (i + 1))
    |> Seq.map (fun v -> v.Value)
    |> Seq.take 5
    |> Seq.toList
// ["1"; "2"; "Fizz"; "4"; "Buzz"]

List と Seq の使い分け

特徴 List Seq
評価方式 即時評価 遅延評価
メモリ使用 全要素をメモリに保持 必要な分だけ計算
無限列 不可 可能(Seq.initInfinite)
パフォーマンス 小〜中サイズに最適 大量データに最適
用途 一般的なコレクション操作 ストリーム処理、大量データ

Rust のイテレータも遅延評価ですが、F# では List(即時)と Seq(遅延)を明示的に使い分けます。

11.5 FizzBuzzList のイミュータブル設計

パイプラインによるコレクション操作

FizzBuzzList にコレクション操作メソッドを追加します。

member this.Filter(predicate: FizzBuzzValue -> bool) =
    { Values = this.Values |> List.filter predicate }

member this.FindFirst(predicate: FizzBuzzValue -> bool) =
    this.Values |> List.tryFind predicate

member this.ToStringValues() =
    this.Values |> List.map (fun v -> v.Value)

member this.Add(value: FizzBuzzValue) =
    { Values = this.Values @ [ value ] }

member this.AddRange(values: FizzBuzzValue list) =
    { Values = this.Values @ values }

すべてのメソッドが 新しい値を返す 設計です。元のリストは変更されません。

テストで確認

[<Fact>]
let ``文字列リストに変換できる`` () =
    let list =
        createList
            [ createValue 1 "1"
              createValue 3 "Fizz"
              createValue 5 "Buzz" ]
    let strings = list.ToStringValues()
    Assert.Equal<string list>([ "1"; "Fizz"; "Buzz" ], strings)

[<Fact>]
let ``最初の一致する値を取得できる`` () =
    let list =
        createList
            [ createValue 1 "1"
              createValue 3 "Fizz"
              createValue 6 "Fizz" ]
    let found = list.FindFirst(fun v -> v.Value = "Fizz")
    Assert.True(found.IsSome)
    Assert.Equal(3, found.Value.Number)

[<Fact>]
let ``一致する値がない場合はNoneを返す`` () =
    let list =
        createList [ createValue 1 "1"; createValue 2 "2" ]
    let found = list.FindFirst(fun v -> v.Value = "Fizz")
    Assert.True(found.IsNone)

11.6 他言語との比較

概念 F# Rust Java
不変デフォルト let x = 5 let x = 5 final で明示
パイプライン \|> List.filter f \|> List.map g .iter().filter(f).map(g) .stream().filter(f).map(g)
fold List.fold f init list .fold(init, f) .reduce(init, f)
遅延評価 Seq モジュール イテレータ(デフォルト遅延) Stream(デフォルト遅延)
Option Some v / None Some(v) / None Optional.of(v) / empty()

11.7 まとめ

この章では以下を学びました。

概念 F# の実現方法
不変性 let バインディング、レコード型
コピー with 式によるレコードの部分更新
List モジュール map, filter, fold, groupBy, tryFind
Seq モジュール 遅延評価によるストリーム処理
イミュータブル設計 操作は常に新しい値を返す

次章では、Option 型Result 型 を使ったエラーハンドリングと型安全性を学びます。