新しい言語を学ぶには、言語自体以上のものが必要です。 生産性を上げるには、標準ライブラリの大部分を暗記し、残りの部分についても概ね把握しておく必要があります。 たとえば、C#を知っているなら、Java言語自体はすぐに習得できますが、Java Class Libraryに慣れるまでは本当の意味で上達したとは言えません。

同様に、F#のコレクションを扱う関数すべてにある程度慣れるまでは、F#で本当に効率的に作業することはできません。

C#ではLINQメソッドを数個知っていれば十分です1SelectWhereなど)。 しかしF#では、現在Listモジュールに約100の関数があり(SeqモジュールやArrayモジュールでも同様です)。これはかなりの数です。

1 実際にはもっとありますが、少数で事足ります。F#ではすべてを知ることがより重要です。

C#からF#に移行する場合、大量のリスト関数に圧倒されることがあります。

そこで、求める関数を見つけるためのガイドとして、この投稿を書きました。 お遊びで、ゲームブック『きみならどうする?』のスタイルで作成しました。

どのコレクションを使うべき?

まず、異なる種類の標準コレクションに関する情報をまとめた表を示します。F#固有のものが5つあります。listseqarraymapsetです。 また、ResizeArrayIDictionaryもよく使用されます。

不変? 備考
list はい 長所:
  • パターンマッチングが可能。
  • 再帰を使った複雑な繰り返しが可能。
  • 前方への繰り返しが高速。先頭への追加が高速。
短所:
  • インデックスによるアクセスやその他のアクセス方法が遅い。
seq はい

IEnumerableの別名。

長所:
  • 遅延評価。
  • メモリ効率が良い(一度に1要素だけ読み込む)。
  • 無限シーケンスを表現できる。
  • IEnumerableを使用する.NETライブラリとの相互運用性がある。
短所:
  • パターンマッチングができない。
  • 前方のみの繰り返し。
  • インデックスによるアクセスやその他のアクセス方法が遅い。
array いいえ

BCLのArrayと同じ。

長所:
  • ランダムアクセスが高速。
  • メモリ効率が良く、特に構造体の場合にキャッシュ局所性がある。
  • Arrayを使用する.NETライブラリとの相互運用性がある。
  • 2次元、3次元、4次元配列をサポート。
短所:
  • パターンマッチングの制限がある。
  • 永続的ではない。
map はい 不変のディクショナリ。キーにIComparableの実装が必要。
set はい 不変のセット。要素にIComparableの実装が必要。
ResizeArray いいえ BCLのListの別名。長所と短所は配列と似ているが、サイズ変更が可能。
IDictionary はい

要素にIComparableの実装が不要な代替ディクショナリとして、 BCLのIDictionaryを使用できます。 F#でのコンストラクタはdictです。

Addなどのミューテーションメソッドは存在しますが、呼び出すと実行時エラーが発生します。

これらがF#で頻繁に使用される主なコレクション型で、一般的な場合には十分です。

ただし、他の種類のコレクションが必要な場合、多くの選択肢があります。

  • .NETのコレクションクラスを使用できます。従来の可変なものや、 System.Collections.Immutable名前空間にある新しいものがあります。
  • または、F#のコレクションライブラリの1つを使用することもできます。
    • FSharpx.Collections、FSharpxシリーズのプロジェクトの一部。
    • ExtCore。一部はFSharp.CoreのMapやSet型のほぼ直接的な代替として、特定のシナリオでパフォーマンスが向上します(例:HashMap)。他は特定のコーディングタスクに役立つユニークな機能を提供します(例:LazyListやLruCache)。
    • Imms:.NET用の高性能な不変データ構造。
    • Persistent:効率的な永続的(不変)データ構造。

ドキュメントについて

特に記載がない限り、F# v4ではすべての関数がlistseqarrayで利用可能です。MapモジュールとSetモジュールにもいくつかの関数がありますが、ここではmapsetについては触れません。

関数のシグネチャには、標準的なコレクション型としてlistを使用します。seqarrayのバージョンのシグネチャも同様です。

関数名をクリックするとリンクが開きます。 これらの関数の一部は最新のF#では非推奨です。それらについては、コメントが含まれているGitHub上のソースコードに直接リンクします。

利用可能性に関する注意

これらの関数の利用可能性は、使用するF#のバージョンによって異なる場合があります。

  • F#バージョン3(Visual Studio 2013)では、リスト、配列、シーケンス間に一定の不一致がありました。
  • F#バージョン4(Visual Studio 2015)では、この不一致が解消され、ほぼすべての関数が3つのコレクション型すべてで利用可能になりました。

F# v3とF# v4の変更点を知りたい場合は、このチャート出典元)を参照してください。 このチャートはF# v4の新しいAPI(緑)、既存のAPI(青)、意図的に残された空白部分(白)を示しています。

以下で説明する関数の中には、このチャートにないものもあります。これらはさらに新しいものです。 古いバージョンのF#を使用している場合は、GitHub上のコードを参考に自分で実装することができます。

この注意事項を踏まえた上で、冒険を始めましょう。


目次


1. どんなコレクションを持っていますか?

どのようなコレクションを持っていますか?

  • コレクションを持っておらず、作成したい場合は、セクション2に進んでください。
  • すでに操作したいコレクションを持っている場合は、セクション9に進んでください。
  • 操作したい2つのコレクションがある場合は、セクション23に進んでください。
  • 操作したい3つのコレクションがある場合は、セクション24に進んでください。
  • 操作したい3つ以上のコレクションがある場合は、セクション25に進んでください。
  • コレクションを結合または分割したい場合は、セクション26に進んでください。


2. 新しいコレクションの作成

新しいコレクションを作成したいのですね。どのように作成しますか?

  • 新しいコレクションが空または1つの要素を持つ場合は、セクション3に進んでください。
  • 新しいコレクションが既知のサイズの場合は、セクション4に進んでください。
  • 新しいコレクションが潜在的に無限の場合は、セクション7に進んでください。
  • コレクションのサイズが不明な場合は、セクション8に進んでください。


3. 空または1要素のコレクションの作成

空または1要素の新しいコレクションを作成したい場合は、以下の関数を使います。

コレクションのサイズが事前に分かっている場合、一般的には別の関数を使う方が効率的です。以下のセクション4を参照してください。

使用例

let list0 = List.empty
// list0 = []

let list1 = List.singleton "hello"
// list1 = ["hello"]


4. 既知のサイズの新しいコレクションの作成

  • コレクションのすべての要素が同じ値を持つ場合は、セクション5に進んでください。
  • コレクションの要素が異なる可能性がある場合は、セクション6に進んでください。


5. 既知のサイズで、各要素が同じ値を持つ新しいコレクションの作成

既知のサイズで、各要素が同じ値を持つ新しいコレクションを作成したい場合は、replicateを使います。

Array.createは基本的にreplicateと同じです(ただし実装が微妙に異なります)。replicateはF# v4で初めてArray用に実装されました。

使用例

let repl = List.replicate 3 "hello"
// val repl : string list = ["hello"; "hello"; "hello"]

let arrCreate = Array.create 3 "hello"
// val arrCreate : string [] = [|"hello"; "hello"; "hello"|]

let intArr0 : int[] = Array.zeroCreate 3
// val intArr0 : int [] = [|0; 0; 0|]

let stringArr0 : string[] = Array.zeroCreate 3
// val stringArr0 : string [] = [|null; null; null|]

zeroCreateでは、ターゲットの型をコンパイラが知っている必要があることに注意してください。


6. 既知のサイズで、各要素が異なる値を持つ新しいコレクションの作成

既知のサイズで、各要素が潜在的に異なる値を持つ新しいコレクションを作成したい場合、以下の3つの方法から選べます。

  • init : length:int -> initializer:(int -> 'T) -> 'T list 各インデックスに対して指定されたジェネレータを呼び出してコレクションを作成します。
  • リストと配列の場合、[1; 2; 3](リスト)や[|1; 2; 3|](配列)のようなリテラル構文も使えます。
  • リスト、配列、シーケンスの場合、for .. in .. do .. yieldというコンプリヘンション構文を使用できます。

使用例

// リスト初期化子を使用
let listInit1 = List.init 5 (fun i-> i*i)
// val listInit1 : int list = [0; 1; 4; 9; 16]

// リストコンプリヘンションを使用
let listInit2 = [for i in [1..5] do yield i*i]
// val listInit2 : int list = [1; 4; 9; 16; 25]

// リテラル
let listInit3 = [1; 4; 9; 16; 25]
// val listInit3 : int list = [1; 4; 9; 16; 25]

let arrayInit3 = [|1; 4; 9; 16; 25|]
// val arrayInit3 : int [] = [|1; 4; 9; 16; 25|]

リテラル構文では増分も指定できます。

// +2の増分を持つリテラル
let listOdd= [1..2..10]
// val listOdd : int list = [1; 3; 5; 7; 9]

コンプリヘンション構文はさらに柔軟で、複数回yieldできます。

// リストコンプリヘンションを使用
let listFunny = [
    for i in [2..3] do 
        yield i
        yield i*i
        yield i*i*i
        ]
// val listFunny : int list = [2; 4; 8; 3; 9; 27]

また、簡易的なインラインフィルタとしても使えます。

let primesUpTo n = 
   let rec sieve l  = 
      match l with 
      | [] -> []
      | p::xs -> 
            p :: sieve [for x in xs do if (x % p) > 0 then yield x]
   [2..n] |> sieve 

primesUpTo 20
// [2; 3; 5; 7; 11; 13; 17; 19]

他に2つのテクニックがあります。

  • yield!を使用してリストを返すことができます。
  • 再帰も使用できます。

以下は、両方のテクニックを使用して2ずつ10までカウントアップする例です。

let rec listCounter n = [
    if n <= 10 then
        yield n
        yield! listCounter (n+2)
    ]

listCounter 3
// val it : int list = [3; 5; 7; 9]
listCounter 4
// val it : int list = [4; 6; 8; 10]


7. 新しい無限コレクションの作成

無限リストが必要な場合、リストや配列ではなくシーケンスを使う必要があります。

  • initInfinite : initializer:(int -> 'T) -> seq<'T> 繰り返し処理時に、指定された関数を呼び出して連続した要素を返す新しいシーケンスを生成します。
  • 再帰ループを使用したシーケンスコンプリヘンションでも無限シーケンスを生成できます。

使用例

// ジェネレータバージョン
let seqOfSquares = Seq.initInfinite (fun i -> i*i)
let firstTenSquares = seqOfSquares |> Seq.take 10

firstTenSquares |> List.ofSeq // [0; 1; 4; 9; 16; 25; 36; 49; 64; 81]

// 再帰バージョン
let seqOfSquares_v2 = 
    let rec loop n = seq {
        yield n * n
        yield! loop (n+1)
        }
    loop 1
let firstTenSquares_v2 = seqOfSquares_v2 |> Seq.take 10


8. 不定サイズの新しいコレクションの作成

コレクションのサイズが事前に分からない場合があります。この場合、停止シグナルを受け取るまで要素を追加し続ける関数が必要です。 ここでunfoldが役立ちます。停止シグナルはNone(停止)またはSome(続行)を返すかどうかです。

使用例

この例では、空の行が入力されるまでコンソールから読み取りを繰り返します。

let getInputFromConsole lineNo =
    let text = System.Console.ReadLine()
    if System.String.IsNullOrEmpty(text) then
        None
    else
        // 値と新しいスレッド状態を返す
        // "text"が生成されるシーケンスに含まれる
        Some (text,lineNo+1)

let listUnfold = List.unfold getInputFromConsole 1

unfoldはジェネレータを通じて状態を受け渡す必要があります。これを無視することもできますし(上のReadLineの例のように)、 これまでの処理内容を記録するために使うこともできます。たとえば、unfoldを使ってフィボナッチ数列のジェネレータを作成できます。

let fibonacciUnfolder max (f1,f2)  =
    if f1 > max then
        None
    else
        // 値と新しいスレッド状態を返す
        let fNext = f1 + f2
        let newState = (f2,fNext)
        // f1が生成されるシーケンスに含まれる
        Some (f1,newState)

let fibonacci max = List.unfold (fibonacciUnfolder max) (1,1)
fibonacci 100
// int list = [1; 1; 2; 3; 5; 8; 13; 21; 34; 55; 89]


9. 1つのリストの操作

1つのリストを操作する場合で...

  • 既知の位置にある要素を取得したい場合は、セクション10に進んでください。
  • 検索によって1つの要素を取得したい場合は、セクション11に進んでください。
  • コレクションのサブセットを取得したい場合は、セクション12に進んでください。
  • コレクションを分割、チャンク化、またはグループ化して小さなコレクションにしたい場合は、セクション13に進んでください。
  • コレクションを単一の値に集計または要約したい場合は、セクション14に進んでください。
  • 要素の順序を変更したい場合は、セクション15に進んでください。
  • コレクション内の要素をテストしたい場合は、セクション16に進んでください。
  • 各要素を別のものに変換したい場合は、セクション17に進んでください。
  • 各要素に対して繰り返し処理を行いたい場合は、セクション18に進んでください。
  • 繰り返し処理を通じて状態を受け渡したい場合は、セクション19に進んでください。
  • 繰り返し処理やマッピング中に各要素のインデックスを知る必要がある場合は、セクション20に進んでください。
  • コレクション全体を異なるコレクション型に変換したい場合は、セクション21に進んでください。
  • コレクション全体の動作を変更したい場合は、セクション22に進んでください。
  • コレクションをその場で変更したい場合は、セクション27に進んでください。
  • IDisposableを使用した遅延コレクションを使いたい場合は、セクション28に進んでください。


10. 既知の位置にある要素の取得

以下の関数は、位置によってコレクション内の要素を取得します。

しかし、コレクションが空の場合はどうなるでしょうか?その場合、headlastは例外(ArgumentException)で失敗します。

また、インデックスがコレクション内に見つからない場合はどうでしょうか?その場合も再び例外が発生します(リストの場合はArgumentException、配列の場合はIndexOutOfRangeException)。

そのため、一般的にこれらの関数の使用は避け、以下のtryXXXの同等品を使用することをお勧めします。

使用例

let head = [1;2;3] |> List.head
// val head : int = 1

let badHead : int = [] |> List.head
// System.ArgumentException: リストが空です。

let goodHeadOpt = 
    [1;2;3] |> List.tryHead 
// val goodHeadOpt : int option = Some 1

let badHeadOpt : int option = 
    [] |> List.tryHead 
// val badHeadOpt : int option = None    

let goodItemOpt = 
    [1;2;3] |> List.tryItem 2
// val goodItemOpt : int option = Some 3

let badItemOpt = 
    [1;2;3] |> List.tryItem 99
// val badItemOpt : int option = None

前述のように、リストに対するitem関数の使用は避けるべきです。たとえば、リストの各項目を処理したい場合、命令型プログラミングの背景から 次のようなループを書きたくなるかもしれません。

// こうしないでください!
let helloBad = 
    let list = ["a";"b";"c"]
    let listSize = List.length list
    [ for i in [0..listSize-1] do
        let element = list |> List.item i
        yield "hello " + element 
    ]
// val helloBad : string list = ["hello a"; "hello b"; "hello c"]

こうしないでください!代わりにmapのようなものを使ってください。より簡潔で効率的です。

let helloGood = 
    let list = ["a";"b";"c"]
    list |> List.map (fun element -> "hello " + element)
// val helloGood : string list = ["hello a"; "hello b"; "hello c"]


11. 検索による要素の取得

findfindIndexを使用して、要素またはそのインデックスを検索できます。

また、逆方向にも検索できます。

しかし、項目が見つからない場合はどうなるでしょうか?その場合、これらの関数は例外(KeyNotFoundException)で失敗します。

そのため、headitemと同様に、一般的にこれらの関数の使用は避け、以下のtryXXXの同等品を使用することをお勧めします。

mapの後にfindを行う場合、多くの場合pick(または、より良い選択としてtryPick)を使用して2つのステップを1つに結合できます。使用例は以下を参照してください。

使用例

let listOfTuples = [ (1,"a"); (2,"b"); (3,"b"); (4,"a"); ]

listOfTuples |> List.find ( fun (x,y) -> y = "b")
// (2, "b")

listOfTuples |> List.findBack ( fun (x,y) -> y = "b")
// (3, "b")

listOfTuples |> List.findIndex ( fun (x,y) -> y = "b")
// 1

listOfTuples |> List.findIndexBack ( fun (x,y) -> y = "b")
// 2

listOfTuples |> List.find ( fun (x,y) -> y = "c")
// KeyNotFoundException

pickでは、boolを返す代わりにoptionを返します。

listOfTuples |> List.pick ( fun (x,y) -> if y = "b" then Some (x,y) else None)
// (2, "b")

PickとFindの比較

pick関数は不要に見えるかもしれませんが、optionを返す関数を扱う際に便利です。

たとえば、文字列を解析してint型のSomeを返し、有効な整数でない場合はNoneを返すtryInt関数があるとします。

// string -> int option
let tryInt str = 
    match System.Int32.TryParse(str) with
    | true, i -> Some i
    | false, _ -> None

そして、リスト内の最初の有効な整数を見つけたいとします。素朴な方法は以下のようになります。

  • tryIntを使用してリストをマップする
  • findを使用して最初のSomeを見つける
  • Option.getを使用してoptionの中身を取得する

コードは次のようになるでしょう。

let firstValidNumber = 
    ["a";"2";"three"]
    // 入力をマップする
    |> List.map tryInt 
    // 最初のSomeを見つける
    |> List.find (fun opt -> opt.IsSome)
    // optionからデータを取得する
    |> Option.get
// val firstValidNumber : int = 2

しかし、pickはこれらのステップをすべて一度に行います!そのため、コードがはるかに簡潔になります。

let firstValidNumber = 
    ["a";"2";"three"]
    |> List.pick tryInt

pickと同じ方法で多くの要素を返したい場合は、chooseの使用を検討してください(セクション12参照)。


12. コレクションから要素のサブセットを取得

前のセクションでは1つの要素を取得する方法を説明しました。では、複数の要素を取得するにはどうすればよいでしょうか?幸運なことに、選択肢がたくさんあります。

コレクションの前方から要素を抽出するには、以下のいずれかを使います。

コレクションの後方から要素を抽出するには、以下のいずれかを使います。

その他の要素のサブセットを抽出するには、以下のいずれかを使います。

リストを個別の要素に絞り込むには、以下のいずれかを使います。

使用例

前方から要素を取得:

[1..10] |> List.take 3    
// [1; 2; 3]

[1..10] |> List.takeWhile (fun i -> i < 3)    
// [1; 2]

[1..10] |> List.truncate 4
// [1; 2; 3; 4]

[1..2] |> List.take 3    
// System.InvalidOperationException: 入力シーケンスの要素数が不足しています。

[1..2] |> List.takeWhile (fun i -> i < 3)  
// [1; 2]

[1..2] |> List.truncate 4
// [1; 2]   // エラーなし!

後方から要素を取得:

[1..10] |> List.skip 3    
// [4; 5; 6; 7; 8; 9; 10]

[1..10] |> List.skipWhile (fun i -> i < 3)    
// [3; 4; 5; 6; 7; 8; 9; 10]

[1..10] |> List.tail
// [2; 3; 4; 5; 6; 7; 8; 9; 10]

[1..2] |> List.skip 3    
// System.ArgumentException: インデックスが正しい範囲外です。

[1..2] |> List.skipWhile (fun i -> i < 3)  
// []

[1] |> List.tail |> List.tail
// System.ArgumentException: 入力リストが空でした。

その他の要素のサブセットを抽出:

[1..10] |> List.filter (fun i -> i%2 = 0) // 偶数
// [2; 4; 6; 8; 10]

[1..10] |> List.where (fun i -> i%2 = 0) // 偶数
// [2; 4; 6; 8; 10]

[1..10] |> List.except [3;4;5]
// [1; 2; 6; 7; 8; 9; 10]

スライスを抽出:

Array.sub [|1..10|] 3 5
// [|4; 5; 6; 7; 8|]

[1..10].[3..5] 
// [4; 5; 6]

[1..10].[3..] 
// [4; 5; 6; 7; 8; 9; 10]

[1..10].[..5] 
// [1; 2; 3; 4; 5; 6]

リストのスライシングは遅い可能性があるので注意してください。ランダムアクセスではないためです。一方、配列のスライシングは高速です。

個別の要素を抽出:

[1;1;1;2;3;3] |> List.distinct
// [1; 2; 3]

[ (1,"a"); (1,"b"); (1,"c"); (2,"d")] |> List.distinctBy fst
// [(1, "a"); (2, "d")]

ChooseとFilterの比較

pickと同様に、choose関数は一見扱いにくく見えるかもしれませんが、オプションを返す関数を扱う場合に便利です。

実際、choosefilterに対して、pickfindに対するのと同じ関係にあります。ブールフィルタを使う代わりに、SomeNoneで信号を送ります。

前と同様に、文字列を解析して有効な整数の場合はSome intを返し、そうでない場合はNoneを返すtryInt関数があるとします。

// string -> int option
let tryInt str = 
    match System.Int32.TryParse(str) with
    | true, i -> Some i
    | false, _ -> None

ここで、リスト内のすべての有効な整数を見つけたいとします。素朴な方法は以下のようになります。

  • tryIntを使ってリストをマップする
  • Someのものだけを含むようにフィルタリングする
  • Option.getを使って各オプションから値を取り出す

コードは次のようになるでしょう。

let allValidNumbers = 
    ["a";"2";"three"; "4"]
    // 入力をマップ
    |> List.map tryInt 
    // "Some"のみを含める
    |> List.filter (fun opt -> opt.IsSome)
    // 各オプションからデータを取得
    |> List.map Option.get
// val allValidNumbers : int list = [2; 4]

しかし、chooseはこれらのステップをすべて一度に行います。そのため、コードがはるかに簡潔になります。

let allValidNumbers = 
    ["a";"2";"three"; "4"]
    |> List.choose tryInt

すでにオプションのリストがある場合、chooseidを渡すことで、フィルタリングと"Some"の返却を1ステップで行えます。

let reduceOptions = 
    [None; Some 1; None; Some 2]
    |> List.choose id
// val reduceOptions : int list = [1; 2]

chooseと同じ方法で最初の要素を返したい場合は、pickの使用を検討してください(セクション11参照)。

chooseと同様の操作を他のラッパー型(成功/失敗の結果など)で行いたい場合は、ここで議論されています


13. 分割、チャンク化、グループ化

コレクションを分割する方法はたくさんあります。使用例を見て、違いを確認してください。

使用例

[1..10] |> List.chunkBySize 3
// [[1; 2; 3]; [4; 5; 6]; [7; 8; 9]; [10]]  
// 最後のチャンクは1つの要素を持つことに注意

[1..10] |> List.splitInto 3
// [[1; 2; 3; 4]; [5; 6; 7]; [8; 9; 10]]
// 最初のチャンクが4つの要素を持つことに注意

['a'..'i'] |> List.splitAt 3
// (['a'; 'b'; 'c'], ['d'; 'e'; 'f'; 'g'; 'h'; 'i'])

['a'..'e'] |> List.pairwise
// [('a', 'b'); ('b', 'c'); ('c', 'd'); ('d', 'e')]

['a'..'e'] |> List.windowed 3
// [['a'; 'b'; 'c']; ['b'; 'c'; 'd']; ['c'; 'd'; 'e']]

let isEven i = (i%2 = 0)
[1..10] |> List.partition isEven 
// ([2; 4; 6; 8; 10], [1; 3; 5; 7; 9])

let firstLetter (str:string) = str.[0]
["apple"; "alice"; "bob"; "carrot"] |> List.groupBy firstLetter 
// [('a', ["apple"; "alice"]); ('b', ["bob"]); ('c', ["carrot"])]

splitAtpairwise以外のすべての関数は、エッジケースを適切に処理します。

[1] |> List.chunkBySize 3
// [[1]]

[1] |> List.splitInto 3
// [[1]]

['a'; 'b'] |> List.splitAt 3
// InvalidOperationException: 入力シーケンスの要素数が不足しています。

['a'] |> List.pairwise
// InvalidOperationException: 入力シーケンスの要素数が不足しています。

['a'] |> List.windowed 3
// []

[1] |> List.partition isEven 
// ([], [1])

[] |> List.groupBy firstLetter 
//  []


14. コレクションの集計または要約

コレクション内の要素を集計する最も一般的な方法はreduceを使うことです。

また、よく使われる集計にはreduceの特定バージョンがあります。

最後に、いくつかのカウント関数があります。

使用例

reduceは初期状態を持たないfoldの変種です - foldについてはセクション19を参照してください。 考え方の一つは、各要素間に演算子を挿入するだけというものです。

["a";"b";"c"] |> List.reduce (+)     
// "abc"

これは以下と同じです。

"a" + "b" + "c"

別の例を示します。

[2;3;4] |> List.reduce (*)     
// 以下と同じ
2 * 3 * 4
// 結果は24

要素の結合方法によっては結合の順序が重要になるため、「reduce」には2つの変種があります。

  • reduceはリストを前方に進みます。
  • reduceBackは、予想通り、リストを後方に進みます。

その違いを示します。まずreduceから:

[1;2;3;4] |> List.reduce (fun state x -> (state)*10 + x)

// 以下から構築              // 各ステップの状態
1                            // 1
(1)*10 + 2                   // 12 
((1)*10 + 2)*10 + 3          // 123 
(((1)*10 + 2)*10 + 3)*10 + 4 // 1234

// 最終結果は1234

同じ結合関数をreduceBackで使うと、異なる結果が得られます。以下のようになります。

[1;2;3;4] |> List.reduceBack (fun x state -> x + 10*(state))

// 以下から構築              // 各ステップの状態
4                            // 4
3 + 10*(4)                   // 43  
2 + 10*(3 + 10*(4))          // 432  
1 + 10*(2 + 10*(3 + 10*(4))) // 4321  

// 最終結果は4321

関連する関数foldfoldBackについての詳細な議論は、再度セクション19を参照してください。

その他の集計関数はもっと分かりやすいです。

type Suit = Club | Diamond | Spade | Heart 
type Rank = Two | Three | King | Ace
let cards = [ (Club,King); (Diamond,Ace); (Spade,Two); (Heart,Three); ]

cards |> List.max        // (Heart, Three)
cards |> List.maxBy snd  // (Diamond, Ace)
cards |> List.min        // (Club, King)
cards |> List.minBy snd  // (Spade, Two)

[1..10] |> List.sum
// 55

[ (1,"a"); (2,"b") ] |> List.sumBy fst
// 3

[1..10] |> List.average
// 型 'int' は演算子 'DivideByInt' をサポートしていません

[1..10] |> List.averageBy float
// 5.5

[ (1,"a"); (2,"b") ] |> List.averageBy (fst >> float)
// 1.5

[1..10] |> List.length
// 10

[ ("a","A"); ("b","B"); ("a","C") ]  |> List.countBy fst
// [("a", 2); ("b", 1)]

[ ("a","A"); ("b","B"); ("a","C") ]  |> List.countBy snd
// [("A", 1); ("B", 1); ("C", 1)]

ほとんどの集計関数は空のリストを好みません!安全を期すならfold関数の使用を検討してください - セクション19を参照。

let emptyListOfInts : int list = []

emptyListOfInts |> List.reduce (+)     
// ArgumentException: 入力リストが空でした。

emptyListOfInts |> List.max
// ArgumentException: 入力シーケンスが空でした。

emptyListOfInts |> List.min
// ArgumentException: 入力シーケンスが空でした。

emptyListOfInts |> List.sum      
// 0

emptyListOfInts |> List.averageBy float
// ArgumentException: 入力シーケンスが空でした。

let emptyListOfTuples : (int*int) list = []
emptyListOfTuples |> List.countBy fst
// (int * int) list = []


15. 要素の順序の変更

反転、ソート、順列を使用して要素の順序を変更できます。以下のすべては新しいコレクションを返します。

また、その場でソートする配列専用の関数もあります。

使用例

[1..5] |> List.rev
// [5; 4; 3; 2; 1]

[2;4;1;3;5] |> List.sort
// [1; 2; 3; 4; 5]

[2;4;1;3;5] |> List.sortDescending
// [5; 4; 3; 2; 1]

[ ("b","2"); ("a","3"); ("c","1") ]  |> List.sortBy fst
// [("a", "3"); ("b", "2"); ("c", "1")]

[ ("b","2"); ("a","3"); ("c","1") ]  |> List.sortBy snd
// [("c", "1"); ("b", "2"); ("a", "3")]

// 比較関数の例
let tupleComparer tuple1 tuple2  =
    if tuple1 < tuple2 then 
        -1 
    elif tuple1 > tuple2 then 
        1 
    else
        0

[ ("b","2"); ("a","3"); ("c","1") ]  |> List.sortWith tupleComparer
// [("a", "3"); ("b", "2"); ("c", "1")]

[1..10] |> List.permute (fun i -> (i + 3) % 10)
// [8; 9; 10; 1; 2; 3; 4; 5; 6; 7]

[1..10] |> List.permute (fun i -> 9 - i)
// [10; 9; 8; 7; 6; 5; 4; 3; 2; 1]


16. コレクションの要素のテスト

これらの関数はすべてtrueまたはfalseを返します。

使用例

[1..10] |> List.contains 5
// true

[1..10] |> List.contains 42
// false

[1..10] |> List.exists (fun i -> i > 3 && i < 5)
// true

[1..10] |> List.exists (fun i -> i > 5 && i < 3)
// false

[1..10] |> List.forall (fun i -> i > 0)
// true

[1..10] |> List.forall (fun i -> i > 5)
// false

[1..10] |> List.isEmpty
// false


17. 各要素を別のものに変換する

私は時々、関数型プログラミングを「変換指向プログラミング」と考えるのが好きです。map(LINQではSelect)は、このアプローチの最も基本的な要素の1つです。 実際、私はこのテーマについてここで連載を書いています。

時には、各要素がリストにマップされ、すべてのリストをフラット化したい場合があります。この場合は、collect(LINQではSelectMany)を使います。

その他の変換関数には以下があります。

使用例

以下は、従来の方法でmapを使用する例です。リストとマッピング関数を受け取り、新しく変換されたリストを返す関数として使います。

let add1 x = x + 1

// リスト変換としてのmap
[1..5] |> List.map add1
// [2; 3; 4; 5; 6]

// マップされるリストには何でも入れられます!
let times2 x = x * 2
[ add1; times2] |> List.map (fun f -> f 5)
// [6; 10]

map関数変換器として考えることもできます。要素から要素への関数をリストからリストへの関数に変換します。

let add1ToEachElement = List.map add1
// "add1ToEachElement"は、intからintへの変換ではなく、リストからリストへの変換を行います
// val add1ToEachElement : (int list -> int list)

// 使用例
[1..5] |> add1ToEachElement 
// [2; 3; 4; 5; 6]

collectはリストをフラット化するのに役立ちます。既にリストのリストがある場合、collectidを使ってフラット化できます。

[2..5] |> List.collect (fun x -> [x; x*x; x*x*x] )
// [2; 4; 8; 3; 9; 27; 4; 16; 64; 5; 25; 125]

// collectと"id"の使用
let list1 = [1..3]
let list2 = [4..6]
[list1; list2] |> List.collect id
// [1; 2; 3; 4; 5; 6]

Seq.cast

最後に、Seq.castは、ジェネリックではなく特殊なコレクションクラスを持つBCLの古い部分を扱う際に便利です。

たとえば、正規表現ライブラリにこの問題があります。以下のコードは、MatchCollectionIEnumerable<T>ではないためコンパイルできません。

open System.Text.RegularExpressions

let matches = 
    let pattern = "\d\d\d"
    let matchCollection = Regex.Matches("123 456 789",pattern)
    matchCollection
    |> Seq.map (fun m -> m.Value)     // エラー
    // エラー: 型 'MatchCollection' は型 'seq<'a>' と互換性がありません
    |> Seq.toList

修正方法は、MatchCollectionSeq<Match>にキャストすることです。そうすればコードはうまく動作します。

let matches = 
    let pattern = "\d\d\d"
    let matchCollection = Regex.Matches("123 456 789",pattern)
    matchCollection
    |> Seq.cast<Match> 
    |> Seq.map (fun m -> m.Value)
    |> Seq.toList
// 出力 = ["123"; "456"; "789"]


18. 各要素に対する繰り返し処理

通常、コレクションを処理する際は、mapを使って各要素を新しい値に変換します。 しかし、時には有用な値を生成しない関数(「unit関数」)ですべての要素を処理する必要があります。

使用例

unit関数の最も一般的な例は、すべて副作用に関するものです。コンソールへの出力、データベースの更新、キューへのメッセージの配置などです。 以下の例では、単にunit関数としてprintfnを使います。

[1..3] |> List.iter (fun i -> printfn "iは%iです" i)
(*
iは1です
iは2です
iは3です
*)

// または部分適用を使用
[1..3] |> List.iter (printfn "iは%iです")

// またはfor loopを使用
for i = 1 to 3 do
    printfn "iは%iです" i

// またはfor-in loopを使用
for i in [1..3] do
    printfn "iは%iです" i

前述のように、iterやfor-loop内の式はunitを返す必要があります。以下の例では、要素に1を加えようとしてコンパイルエラーが発生します。

[1..3] |> List.iter (fun i -> i + 1)
//                               ~~~
// エラー FS0001: 型 'unit' は型 'int' と一致しません

// for-loopの式は*必ず*unitを返す必要があります
for i in [1..3] do
     i + 1  // エラー
     // この式は型 'unit' を持つべきですが、
     // 型 'int' を持っています。'ignore' を使用してください...

コードに論理的なバグがないことを確信し、このエラーを解消したい場合は、結果をignoreにパイプできます。

[1..3] |> List.iter (fun i -> i + 1 |> ignore)

for i in [1..3] do
     i + 1 |> ignore


19. 繰り返し処理を通じた状態の受け渡し

fold関数は、コレクション操作の中で最も基本的かつ強力な関数です。他のすべての関数(unfoldのような生成器を除く)は、これを使って書くことができます。以下の例を参照してください。

fold関数は「左畳み込み」、foldBackは「右畳み込み」とも呼ばれます。

scan関数はfoldに似ていますが、中間結果も返すため、繰り返し処理の追跡やモニタリングに使えます。

fold関数のように、scanは「左スキャン」、scanBackは「右スキャン」とも呼ばれます。

最後に、mapFoldmapfoldを1つの素晴らしい超能力に組み合わせます。mapfoldを別々に使うよりも複雑ですが、より効率的です。

foldの例

foldの考え方の1つは、reduceと同じですが、初期状態のための追加パラメータがあるというものです。

["a";"b";"c"] |> List.fold (+) "hello: "    
// "hello: abc"
// "hello: " + "a" + "b" + "c"

[1;2;3] |> List.fold (+) 10    
// 16
// 10 + 1 + 2 + 3

reduceと同様に、foldfoldBackは非常に異なる結果を生む可能性があります。

[1;2;3;4] |> List.fold (fun state x -> (state)*10 + x) 0
                                // 各ステップでの状態
1                               // 1
(1)*10 + 2                      // 12 
((1)*10 + 2)*10 + 3             // 123 
(((1)*10 + 2)*10 + 3)*10 + 4    // 1234
// 最終結果は1234

そして、これがfoldBackのバージョンです。

List.foldBack (fun x state -> x + 10*(state)) [1;2;3;4] 0
                                // 各ステップでの状態  
4                               // 4
3 + 10*(4)                      // 43  
2 + 10*(3 + 10*(4))             // 432  
1 + 10*(2 + 10*(3 + 10*(4)))    // 4321  
// 最終結果は4321

foldBackfoldとはパラメータの順序が異なることに注意してください。リストは最後から2番目、初期状態が最後にあるため、パイピングが便利ではありません。

再帰 vs 繰り返し

foldfoldBackを混同しやすいです。私はfold繰り返しに関するもの、foldBack再帰に関するものと考えると理解しやすいと感じています。

リストの合計を計算したいとします。繰り返しの方法では、for-loopを使うでしょう。 (ミュータブルな)アキュムレータから始めて、各繰り返しを通じてそれを更新していきます。

let iterativeSum list = 
    let mutable total = 0
    for e in list do
        total <- total + e
    total // 合計を返す

一方、再帰的なアプローチでは、リストに頭部と尾部がある場合、 まず尾部(より小さなリスト)の合計を計算し、それに頭部を加えます。

尾部がどんどん小さくなり、最終的に空になるまでこれを繰り返します。

let rec recursiveSum list = 
    match list with
    | [] -> 
        0
    | head::tail -> 
        head + (recursiveSum tail)

どちらのアプローチが良いでしょうか?

集計の場合、繰り返し方法(fold)の方が理解しやすいことが多いです。 しかし、新しいリストの構築などの場合、再帰的な方法(foldBack)の方が理解しやすいです。

たとえば、各要素を対応する文字列に変換する関数をゼロから作成する場合、 次のように書くかもしれません。

let rec mapToString list = 
    match list with
    | [] -> 
        []
    | head::tail -> 
        head.ToString() :: (mapToString tail)

[1..3] |> mapToString 
// ["1"; "2"; "3"]

foldBackを使えば、同じロジックを「そのまま」転用できます。

  • 空リストに対するアクション = []
  • 非空リストに対するアクション = head.ToString() :: state

結果の関数は次のようになります。

let foldToString list = 
    let folder head state = 
        head.ToString() :: state
    List.foldBack folder list []

[1..3] |> foldToString 
// ["1"; "2"; "3"]

一方、foldの大きな利点は、パイピングとよく合うため「インライン」で使いやすいことです。

幸い、(少なくともリスト構築の場合)最後にリストを反転させれば、foldBackと同じようにfoldを使えます。

// "foldToString"のインラインバージョン
[1..3] 
|> List.fold (fun state head -> head.ToString() :: state) []
|> List.rev
// ["1"; "2"; "3"]

foldを使って他の関数を実装する

前述のように、foldはリストを操作するための中核的な関数であり、ほとんどの他の関数をエミュレートできます。 ただし、カスタム実装ほど効率的ではない場合があります。

たとえば、foldを使ってmapを実装すると次のようになります。

/// 関数"f"をすべての要素にマップする
let myMap f list = 
    // ヘルパー関数
    let folder state head =
        f head :: state

    // メインフロー
    list
    |> List.fold folder []
    |> List.rev

[1..3] |> myMap (fun x -> x + 2)
// [3; 4; 5]

そして、foldを使ってfilterを実装すると次のようになります。

/// "pred"がtrueの要素の新しいリストを返す
let myFilter pred list = 
    // ヘルパー関数
    let folder state head =
        if pred head then 
            head :: state
        else
            state

    // メインフロー
    list
    |> List.fold folder []
    |> List.rev

let isOdd n = (n%2=1)
[1..5] |> myFilter isOdd 
// [1; 3; 5]

もちろん、同様の方法で他の関数もエミュレートできます。

scanの例

先ほど、foldの中間ステップの例を示しました。

[1;2;3;4] |> List.fold (fun state x -> (state)*10 + x) 0
                                // 各ステップでの状態
1                               // 1
(1)*10 + 2                      // 12 
((1)*10 + 2)*10 + 3             // 123 
(((1)*10 + 2)*10 + 3)*10 + 4    // 1234
// 最終結果は1234

この例では、中間状態を手動で計算する必要がありました。

scanを使っていれば、これらの中間状態を無料で手に入れられたでしょう!

[1;2;3;4] |> List.scan (fun state x -> (state)*10 + x) 0
// 左から蓄積 ===> [0; 1; 12; 123; 1234]

scanBackも同じように動作しますが、もちろん逆向きです。

List.scanBack (fun x state -> (state)*10 + x) [1;2;3;4] 0
// [4321; 432; 43; 4; 0]  <=== 右から蓄積

「右スキャン」の場合も、「左スキャン」と比べてパラメータの順序が逆になっています。

scanを使った文字列の切り詰め

scanが役立つ例を紹介します。ニュースサイトがあり、見出しを50文字に収める必要があるとします。

単純に50文字で切り詰めると見栄えが悪くなります。代わりに、単語の境界で切り詰めたいと思います。

scanを使ってこれを行う方法の1つを示します。

  • 見出しを単語に分割します。
  • scanを使って単語を再び連結し、各単語を追加した断片のリストを生成します。
  • 50文字未満の最長の断片を取得します。
// まずテキストを単語に分割
let text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor."
let words = text.Split(' ')
// [|"Lorem"; "ipsum"; "dolor"; "sit"; ... ]

// 一連の断片を蓄積
let fragments = words |> Seq.scan (fun frag word -> frag + " " + word) ""
(*
" Lorem" 
" Lorem ipsum" 
" Lorem ipsum dolor"
" Lorem ipsum dolor sit" 
" Lorem ipsum dolor sit amet,"
など
*)

// 50文字未満の最長の断片を取得
let longestFragUnder50 = 
    fragments 
    |> Seq.takeWhile (fun s -> s.Length <= 50) 
    |> Seq.last 

// 最初の空白を削除
let longestFragUnder50Trimmed = 
    longestFragUnder50 |> (fun s -> s.[1..])

// 結果は:
//   "Lorem ipsum dolor sit amet, consectetur"

Array.scanではなくSeq.scanを使っていることに注意してください。これにより遅延スキャンが行われ、不要な断片の作成を避けられます。

最後に、完全なロジックをユーティリティ関数としてまとめます。

// 全体を関数として
let truncText max (text:string) = 
    if text.Length <= max then
        text
    else
        text.Split(' ')
        |> Seq.scan (fun frag word -> frag + " " + word) ""
        |> Seq.takeWhile (fun s -> s.Length <= max-3) 
        |> Seq.last 
        |> (fun s -> s.[1..] + "...")

"a small headline" |> truncText 50
// "a small headline"

text |> truncText 50
// "Lorem ipsum dolor sit amet, consectetur..."

もちろん、もっと効率的な実装があることは分かっています。しかし、この小さな例がscanの力を示していることを願っています。

mapFoldの例

mapFold関数は、1つのステップでmapとfoldを行うことができ、時々便利です。

以下は、mapFoldを使って加算と合計を1つのステップで組み合わせる例です。

let add1 x = x + 1

// mapを使ってadd1
[1..5] |> List.map (add1)   
// 結果 => [2; 3; 4; 5; 6]

// foldを使って合計
[1..5] |> List.fold (fun state x -> state + x) 0   
// 結果 => 15

// mapFoldを使ってmapと合計
[1..5] |> List.mapFold (fun state x -> add1 x, (state + x)) 0   
// 結果 => ([2; 3; 4; 5; 6], 15)


20. 各要素のインデックスの操作

繰り返し処理を行う際、しばしば要素のインデックスが必要になります。ミュータブルなカウンターを使うこともできますが、ライブラリに任せてリラックスするのはいかがでしょうか?

使用例

['a'..'c'] |> List.mapi (fun index ch -> sprintf "%i番目の要素は'%c'です" index ch)
// ["0番目の要素は'a'です"; "1番目の要素は'b'です"; "2番目の要素は'c'です"]

// 部分適用を使用
['a'..'c'] |> List.mapi (sprintf "%i番目の要素は'%c'です")
// ["0番目の要素は'a'です"; "1番目の要素は'b'です"; "2番目の要素は'c'です"]

['a'..'c'] |> List.iteri (printfn "%i番目の要素は'%c'です")
(*
0番目の要素は'a'です
1番目の要素は'b'です
2番目の要素は'c'です
*)

indexedはインデックスを含むタプルを生成します - mapiの特定の使用法のショートカットです。

['a'..'c'] |> List.mapi (fun index ch -> (index, ch) )
// [(0, 'a'); (1, 'b'); (2, 'c')]

// "indexed"は上の短縮版です
['a'..'c'] |> List.indexed
// [(0, 'a'); (1, 'b'); (2, 'c')]


21. コレクション全体を異なるコレクション型に変換する

あるコレクションの種類から別のコレクションに変換する必要がよくあります。これらの関数がその役割を果たします。

ofXXX関数はXXXからモジュールの型に変換するために使います。たとえば、List.ofArrayは配列をリストに変換します。

toXXX関数はモジュールの型からXXX型に変換するために使います。たとえば、List.toArrayはリストを配列に変換します。

使用例

[1..5] |> List.toArray      // [|1; 2; 3; 4; 5|]
[1..5] |> Array.ofList      // [|1; 2; 3; 4; 5|]
// など

破棄可能なリソースを含むシーケンスの使用

これらの変換関数の重要な用途の1つは、遅延評価されるシーケンス(seq)を完全に評価されたコレクション(listなど)に変換することです。特に ファイルハンドルやデータベース接続などの破棄可能なリソースが関係している場合に重要です。シーケンスをリストに変換しないと、 要素にアクセスする際にエラーが発生する可能性があります。詳細はセクション28を参照してください。


22. コレクション全体の動作を変更する

コレクション全体の動作を変更する特別な関数(Seqのみ)がいくつかあります。

  • (Seqのみ)cache: source:seq<'T> -> seq<'T> 入力シーケンスのキャッシュされたバージョンに対応するシーケンスを返します。この結果のシーケンスは入力シーケンスと同じ要素を持ちます。結果は 複数回列挙できます。入力シーケンスは最大で1回だけ、必要な分だけ列挙されます。
  • (Seqのみ)readonly : source:seq<'T> -> seq<'T> 与えられたシーケンスオブジェクトに委譲する新しいシーケンスオブジェクトを構築します。これにより、元のシーケンスが型キャストによって再発見され変更されることがないようにします。
  • (Seqのみ)delay : generator:(unit -> seq<'T>) -> seq<'T> 与えられた遅延シーケンス仕様から構築されるシーケンスを返します。

cacheの例

cacheの使用例を示します。

let uncachedSeq = seq {
    for i = 1 to 3 do
        printfn "%iを計算中" i
        yield i
    }

// 2回繰り返す    
uncachedSeq |> Seq.iter ignore
uncachedSeq |> Seq.iter ignore

シーケンスを2回繰り返した結果は予想通りです。

1を計算中
2を計算中
3を計算中
1を計算中
2を計算中
3を計算中

しかし、シーケンスをキャッシュすると...

let cachedSeq = uncachedSeq |> Seq.cache

// 2回繰り返す    
cachedSeq |> Seq.iter ignore
cachedSeq |> Seq.iter ignore

...各項目は1回だけ出力されます。

1を計算中
2を計算中
3を計算中

readonlyの例

readonlyを使ってシーケンスの基底型を隠す例を示します。

// シーケンスの基底型を出力
let printUnderlyingType (s:seq<_>) =
    let typeName = s.GetType().Name 
    printfn "%s" typeName 

[|1;2;3|] |> printUnderlyingType 
// Int32[]

[|1;2;3|] |> Seq.readonly |> printUnderlyingType 
// mkSeq@589   // 一時的な型

delayの例

delayの例を示します。

let makeNumbers max =
    [ for i = 1 to max do
        printfn "%dを評価中。" i
        yield i ]

let eagerList = 
    printfn "eagerListの作成開始" 
    let list = makeNumbers 5
    printfn "eagerListの作成完了" 
    list

let delayedSeq = 
    printfn "delayedSeqの作成開始" 
    let list = Seq.delay (fun () -> makeNumbers 5 |> Seq.ofList)
    printfn "delayedSeqの作成完了" 
    list

上のコードを実行すると、eagerListを作成するだけで全ての「評価中」メッセージが出力されますが、delayedSeqの作成ではリストの繰り返しが開始されません。

eagerListの作成開始
1を評価中。
2を評価中。
3を評価中。
4を評価中。
5を評価中。
eagerListの作成完了

delayedSeqの作成開始
delayedSeqの作成完了

シーケンスが繰り返されるときにのみリストの作成が行われます。

eagerList |> Seq.take 3  // リストはすでに作成済み
delayedSeq |> Seq.take 3 // リスト作成が開始される

delayを使う代わりに、リストをseqに埋め込む方法もあります。

let embeddedList = seq {
    printfn "embeddedListの作成開始" 
    yield! makeNumbers 5 
    printfn "embeddedListの作成完了" 
    }

delayedSeqと同様に、シーケンスが繰り返されるまでmakeNumbers関数は呼び出されません。


23. 2つのリストの操作

2つのリストがある場合、mapやfoldなどの一般的な関数のほとんどに相当するものがあります。

使用例

これらの関数は簡単に使えます。

let intList1 = [2;3;4]
let intList2 = [5;6;7]

List.map2 (fun i1 i2 -> i1 + i2) intList1 intList2 
//  [7; 9; 11]

// ヒント:||>演算子を使ってタプルを2つの引数としてパイプできます
(intList1,intList2) ||> List.map2 (fun i1 i2 -> i1 + i2) 
//  [7; 9; 11]

(intList1,intList2) ||> List.mapi2 (fun index i1 i2 -> index,i1 + i2) 
 // [(0, 7); (1, 9); (2, 11)]

(intList1,intList2) ||> List.iter2 (printf "i1=%i i2=%i; ") 
// i1=2 i2=5; i1=3 i2=6; i1=4 i2=7;

(intList1,intList2) ||> List.iteri2 (printf "index=%i i1=%i i2=%i; ") 
// index=0 i1=2 i2=5; index=1 i1=3 i2=6; index=2 i1=4 i2=7;

(intList1,intList2) ||> List.forall2 (fun i1 i2 -> i1 < i2)  
// true

(intList1,intList2) ||> List.exists2 (fun i1 i2 -> i1+10 > i2)  
// true

(intList1,intList2) ||> List.fold2 (fun state i1 i2 -> (10*state) + i1 + i2) 0 
// 801 = 234 + 567

List.foldBack2 (fun i1 i2 state -> i1 + i2 + (10*state)) intList1 intList2 0 
// 1197 = 432 + 765

(intList1,intList2) ||> List.compareWith (fun i1 i2 -> i1.CompareTo(i2))  
// -1

(intList1,intList2) ||> List.append
// [2; 3; 4; 5; 6; 7]

[intList1;intList2] |> List.concat
// [2; 3; 4; 5; 6; 7]

(intList1,intList2) ||> List.zip
// [(2, 5); (3, 6); (4, 7)]

必要な関数がない場合は?

fold2foldBack2を使えば、簡単に独自の関数を作成できます。たとえば、filter2関数は次のように定義できます。

/// ペアの各要素に関数を適用
/// いずれかの結果が合格すれば、そのペアを結果に含める
let filterOr2 filterPredicate list1 list2 =
    let pass e = filterPredicate e 
    let folder e1 e2 state =    
        if (pass e1) || (pass e2) then
            (e1,e2)::state
        else
            state
    List.foldBack2 folder list1 list2 ([])

/// ペアの各要素に関数を適用
/// 両方の結果が合格した場合のみ、そのペアを結果に含める
let filterAnd2 filterPredicate list1 list2 =
    let pass e = filterPredicate e 
    let folder e1 e2 state =     
        if (pass e1) && (pass e2) then
            (e1,e2)::state
        else
            state
    List.foldBack2 folder list1 list2 []

// テスト
let startsWithA (s:string) = (s.[0] = 'A')
let strList1 = ["A1"; "A3"]
let strList2 = ["A2"; "B1"]

(strList1, strList2) ||> filterOr2 startsWithA 
// [("A1", "A2"); ("A3", "B1")]
(strList1, strList2) ||> filterAnd2 startsWithA 
// [("A1", "A2")]

セクション25も参照してください。


24. 3つのリストの操作

3つのリストがある場合、利用可能な組み込み関数は1つだけです。ただし、独自の3リスト関数を作成する方法についてはセクション25を参照してください。


25. 3つ以上のリストの操作

3つ以上のリストを操作する場合、組み込みの関数はありません。

これが頻繁に発生しない場合は、zip2zip3を連続して使ってリストを1つのタプルにまとめ、それをmapで処理することができます。

alternatively、アプリカティブを使用して関数を「ジップリスト」の世界に「持ち上げる」こともできます。

let (<*>) fList xList = 
    List.map2 (fun f x -> f x) fList xList 

let (<!>) = List.map

let addFourParams x y z w = 
    x + y + z + w

// "addFourParams"をリストの世界に持ち上げ、整数ではなくリストをパラメータとして渡す
addFourParams <!> [1;2;3] <*> [1;2;3] <*> [1;2;3] <*> [1;2;3] 
// 結果 = [4; 8; 12]

これが魔法のように見える場合は、このシリーズでこのコードが何をしているかの説明を参照してください。


26. コレクションの結合と分割

最後に、コレクションを結合したり分割したりする関数がいくつかあります。

使用例

これらの関数は簡単に使えます。

List.append [1;2;3] [4;5;6]
// [1; 2; 3; 4; 5; 6]

[1;2;3] @ [4;5;6]
// [1; 2; 3; 4; 5; 6]

List.concat [ [1]; [2;3]; [4;5;6] ]
// [1; 2; 3; 4; 5; 6]

List.zip [1;2] [10;20] 
// [(1, 10); (2, 20)]

List.zip3 [1;2] [10;20] [100;200]
// [(1, 10, 100); (2, 20, 200)]

List.unzip [(1, 10); (2, 20)]
// ([1; 2], [10; 20])

List.unzip3 [(1, 10, 100); (2, 20, 200)]
// ([1; 2], [10; 20], [100; 200])

zip関数は長さが同じである必要があることに注意してください。

List.zip [1;2] [10] 
// ArgumentException: リストの長さが異なります。


27. その他の配列専用関数

配列はミュータブル(変更可能)なので、リストやシーケンスには適用できない関数がいくつかあります。

  • セクション15の「その場でのソート」関数を参照してください。
  • Array.blit: source:'T[] -> sourceIndex:int -> target:'T[] -> targetIndex:int -> count:int -> unit 最初の配列から一定範囲の要素を読み取り、2番目の配列に書き込みます。
  • Array.copy: array:'T[] -> 'T[] 与えられた配列の要素を含む新しい配列を作成します。
  • Array.fill: target:'T[] -> targetIndex:int -> count:int -> value:'T -> unit 配列の指定範囲の要素を与えられた値で埋めます。
  • Array.set: array:'T[] -> index:int -> value:'T -> unit 配列の要素を設定します。
  • これらに加えて、他のすべてのBCL配列関数も利用可能です。

ここでは例を挙げません。F# コアライブラリのドキュメントを参照してください。


28. 破棄可能なリソースを含むシーケンスの使用

List.ofSeqのような変換関数の重要な用途の1つは、遅延評価されるシーケンス(seq)を完全に評価されたコレクション(listなど)に変換することです。これは特に ファイルハンドルやデータベース接続などの破棄可能なリソースが関係している場合に重要です。リソースが利用可能な間にシーケンスをリストに変換しないと、 後でリソースが破棄された後に要素にアクセスしようとするとエラーが発生する可能性があります。

これは長めの例になるので、まずデータベースとUIをエミュレートするヘルパー関数から始めましょう。

// 破棄可能なデータベース接続
let DbConnection() = 
    printfn "接続を開いています"
    { new System.IDisposable with
        member this.Dispose() =
            printfn "接続を破棄しています" }

// データベースからいくつかのレコードを読み込む
let readNCustomersFromDb dbConnection n =
    let makeCustomer i = 
        sprintf "顧客 %i" i

    seq {
        for i = 1 to n do
            let customer = makeCustomer i
            printfn "DBから%sを読み込んでいます" customer 
            yield customer 
        } 

// UIにいくつかのレコードを表示する
let showCustomersinUI customers = 
    customers |> Seq.iter (printfn "UIで%sを表示しています")

素朴な実装では、接続が閉じられた後にシーケンスが評価されてしまいます。

let readCustomersFromDb() =
    use dbConnection = DbConnection()
    let results = readNCustomersFromDb dbConnection 2
    results

let customers = readCustomersFromDb()
customers |> showCustomersinUI

出力は以下のようになります。接続が閉じられた後にシーケンスが評価されていることがわかります。

接続を開いています
接続を破棄しています
DBから顧客 1を読み込んでいます  // エラー!接続が閉じられています!
UIで顧客 1を表示しています
DBから顧客 2を読み込んでいます
UIで顧客 2を表示しています

より良い実装では、接続が開いている間にシーケンスをリストに変換し、シーケンスを即座に評価します。

let readCustomersFromDb() =
    use dbConnection = DbConnection()
    let results = readNCustomersFromDb dbConnection 2
    results |> List.ofSeq
    // 接続が開いている間にリストに変換

let customers = readCustomersFromDb()
customers |> showCustomersinUI

結果はずっと良くなります。接続が破棄される前にすべてのレコードが読み込まれます。

接続を開いています
DBから顧客 1を読み込んでいます
DBから顧客 2を読み込んでいます
接続を破棄しています
UIで顧客 1を表示しています
UIで顧客 2を表示しています

3つ目の選択肢として、破棄可能なリソースをシーケンス自体に埋め込む方法があります。

let readCustomersFromDb() =
    seq {
        // 破棄可能なリソースをシーケンス内に配置
        use dbConnection = DbConnection()
        yield! readNCustomersFromDb dbConnection 2
        } 

let customers = readCustomersFromDb()
customers |> showCustomersinUI

出力を見ると、接続が開いている間にUI表示も行われていることがわかります。

接続を開いています
DBから顧客 1を読み込んでいます
UIで顧客 1を表示しています
DBから顧客 2を読み込んでいます
UIで顧客 2を表示しています
接続を破棄しています

これは(接続が開いている時間が長くなり)悪いことかもしれませんし、(メモリ使用量が最小限になり)良いことかもしれません。状況によって異なります。


29. 冒険の終わり

最後まで到達しましたね - お疲れさまでした!実際のところ、あまり冒険らしくはありませんでしたね。ドラゴンも何もいませんでした。それでも、役に立つ内容だったことを願っています。

results matching ""

    No results matching ""