14
8

More than 5 years have passed since last update.

これは知らなかった... F#ドキュメントには未掲載のコレクション関数(List編)

Posted at
  • ※以下はF#のドキュメントが更新されれば不必要になるでしょう。
  • 前置きをスキップするときは「Listモジュールの未掲載関数」からお読みください。

F#はMicrosoftのDon Syme氏が開発したプログラミング言語で、命令型、オブジェクト指向、関数型の要素を併せ持つマルチパラダイム言語と位置付けられています。実装はオープンソースのMITライセンスのもと、githubのリポジトリで公開されています。

実行環境は.NET Frameworkおよび.NET CoreやMonoといったマルチプラットフォームに対応していますが、サポート状況については各プラットフォームごとの情報を参照してください(「F# の概要」など)。

ドキュメントは豊富だが...

F#はMicrosoftが開発しただけあって、ドキュメントが豊富に揃っており、F#に関するさまざまな情報をWebで参照できます。

また、F#の実行環境となる.NET Frameworkについても、多くのドキュメントが揃っていますので、バージョンの違いさえ把握していれば、充分な情報を得られます。

それでもF#の実装がオープンソースで進められているせいか、ドキュメントの情報が(現時点では)実装に追いついていないところもあるようです。実際、ドキュメント上はF# 4.0が最新ですが、実装はF# 4.1がリリースされています。Linuxのfsharpモジュールのバージョンもディストリビューションによってサポートされるバージョンにばらつきがあるようです。日本語版ドキュメントは更に情報が遅くなる場合があります。

そこで、ここではF#のプログラミングでよく使われるリストを扱うListモジュール(リファレンス:日本語, 英語)で実装されているのにドキュメント(英語版2017/8/18更新分)には掲載されていない関数(以下、未掲載関数という)を抜粋して取り上げます。F#のバージョンは4.1(FSharp.Core 4.4.1.0)とします。

リストとListモジュール

未掲載関数を紹介する前に、F#のリストListモジュールについて簡単に整理しておきます。

F#のリスト

F#のリストは[要素; 要素; ...]で表されます。同一リスト内で要素の型はすべて同じです。型名は'T listで表されます('Tintstringなどリストに対応した型)。

[2; 5; 8]           // int list
["F#"; "fsharp"]    // string list
// ["F#"; 4.1] などは無効

F#ではリスト以外にも複数の要素を持つ型がいくつかありますが、リストで特徴的なのは::という演算子です。これはh::tのようにすると、hに先頭の要素、tにそれ以外の要素を持つリストが割り当てられることを表します。

let a = [1; 2; 3]    // letは値の定義(再代入不可)を表す。let mutableで再代入可能になる
match a with         // aの内容に応じて処理を振り分ける
| h::t -> printfn "h = %A, t = %A" h t    // "h = 1, t = [2; 3]"と表示
| _    -> ()         // (ここではこの部分は実行されない)

これにより、再帰関数による繰り返し処理が可能になります。

let rec sum a =           // recは再帰関数を表す
    match a with
    | h::t -> h + sum t   // ここで再帰処理(先頭の要素 + sum 残りの要素を持つリスト)
    | [] -> 0             // 再帰処理の終結(リストの要素がなくなったら再帰終了)
// sum [2; 5; 8] = 15

F#のListモジュール

Listモジュールでは、リストを扱うさまざまな関数が定義されているので、知っていると改めて関数を定義する手間が省けます。上記のsum関数も、実はList.sumとしてあらかじめ定義されています。

List.sum [2; 5; 8]        // 15
List.sum [2.2; 5.5; 8.8]  // 16.5

ただ、Listモジュールの関数を扱うには注意すべき点もあります。それは例外処理が必要な場合があることです。

たとえばList.findはリストから条件に適合する最初の要素を抽出する関数ですが、そうした要素が存在しないときは例外が発生(F#ではraiseなど)します。

例外処理がない場合
List.find (fun (s: string) -> s.IndexOf("g") >= 0) ["F#"; "fsharp"]
// (fun...)に適合するリストの要素が存在しないため、System.Collections.Generic.KeyNotFoundExceptionが発生

このように、条件に適合する要素を見つけられない可能性があるときはList.tryFind関数に置き換えると、戻り値が'T option型になるため例外処理を回避できます。'T option型の戻り値は、条件に適合する値aがあるときはSome aを、そうした値がないときはNoneとなります。

List.tryFind関数の実行例
List.tryFind (fun (s: string) -> s.IndexOf("f") >= 0) ["F#"; "fsharp"]
// Some "fsharp"
List.tryFind (fun (s: string) -> s.IndexOf("g") >= 0) ["F#"; "fsharp"]
// None

'T option型のSome aからaを取り出すには、match-withOptionモジュールが使えます。

let v = Some 3
match v with
| Some a -> a    // Some a からaを取り出す
| None   -> 0

Option.get (Some 3)    // 3 - Some aからaを取り出す
Option.iter (fun s -> printfn "%s" s) (Some "F#")    // "F#"と表示

Listモジュールの未掲載関数

前置きが長くなりましたが、ここからListモジュールの未掲載関数を抜粋で紹介していきます。

List.except リスト1 リスト2

リスト2の要素からリスト1の要素を削除します。同じ値であれば、重複する要素もすべて取り除かれます。

List.except関数の実行例
List.except [2; 5] [5; 2; 8; 5; 2; 11]    // [8; 11]

List.findBack 条件 リスト /
List.tryFindBack 条件 リスト

List.findBackは条件に適合する要素のうち、リストの末尾に最も近いものを抽出します。List.findとは逆方向からの探索になります。適合する要素が見つからないときはList.findと同じくSystem.Collections.Generic.KeyNotFoundExceptionが発生します。

List.findとList.findBackの実行例
let a = [(1, "F#"); (2, "F#"); (3, "F#")]
List.find     (fun (n, s) -> s = "F#") a    // (1, "F#")
List.findBack (fun (n, s) -> s = "F#") a    // (3, "F#")
List.findBack (fun (n, s) -> s = "C#") a    // 例外発生「System.Collections.Generic.KeyNotFoundException」

List.tryFindBackfindBackと同様の処理をしますが、結果は'T option型となります。条件に適合する要素aが見つかった時はSome a、見つからなかったときはNoneです。

List.tryFindBackの実行例
let a = [(1, "F#"); (2, "F#"); (3, "F#")]
List.tryFindBack (fun (n, s) -> s = "F#") a    // Some (3, "F#")
List.tryFindBack (fun (n, s) -> s = "C#") a    // None

List.findIndexBack 条件 リスト /
List.tryFindIndexBack 条件 リスト

List.findIndexBackは条件に適合する最後の要素の位置を得られます。リストの先頭は0とします。条件に適合する最初の要素の位置はList.findIndexで得られます。どちらの関数でもそうした要素を見つけられなかったときはSystem.Collections.Generic.KeyNotFoundExceptionが発生します。

List.findIndexとList.findIndexBackの実行例
let a = [(1, "F#"); (2, "F#"); (3, "F#")]
List.findIndex     (fun (n, s) -> s = "F#") a    // 0
List.findIndexBack (fun (n, s) -> s = "F#") a    // 2
List.findIndexBack (fun (n, s) -> s = "C#") a    // 例外発生「System.Collections.Generic.KeyNotFoundException」

List.tryFindIndexBackList.findIndexBackと同様の処理をしますが、結果は'T option型となります。条件に適合する要素の位置aが見つかった時はSome a、見つからなかったときはNoneです。

List.tryFindIndexBackの実行例
let a = [(1, "F#"); (2, "F#"); (3, "F#")]
List.tryFindIndexBack (fun (n, s) -> s = "F#") a    // Some 2
List.tryFindIndexBack (fun (n, s) -> s = "C#") a    // None

List.groupBy キー振り分け処理 リスト

振り分け処理に基づいてキーごとにリストの要素を振り分けます。結果はキーとリストをタプルで組にした要素のリストとなります。

List.groupByの実行例
List.groupBy (fun n -> if n % 2 = 0 then "even" else "odd") [1..9]
// [("odd", [1; 3; 5; 7; 9]); ("even", [2; 4; 6; 8])]

List.last リスト / List.tryLast リスト

List.lastはリスト内の最後の要素を抽出します。リストの要素数が0のときはSystem.ArgumentExceptionが発生します。これを配列に対して行うArray.lastも定義されています。

List.lastの実行例
List.last [1; 2; 3]  // 3
List.last<int> []    // 例外発生「System.ArgumentException: 入力リストが空でした。」

List.tryLastはリストの末尾に要素aが存在するときにはSome a、存在しないときにはNoneを得られます。これを配列に対して行うArray.tryLastも定義されています。リストの要素数が0になる可能性があるときは、こちらを使うと例外発生を回避できます。

List.tryLastの実行例
List.tryLast [1; 2; 3]  // Some 3
List.tryLast<int> []    // None

List.pairwise リスト

隣同士の要素をタプルで組にした要素のリストを得られます。リストに要素がなければ、結果もそのままです。

List.pairwiseの実行例
List.pairwise [1; 2; 3; 4]    // [(1, 2); (2, 3); (3, 4)]

List.skip 位置 リスト

指定した位置(先頭は0)以降の要素を持つリストを得られます。位置の値がリストの要素数と同じときは[](要素なしのリスト)、それを超えるときはSystem.ArgumentExceptionが発生します。位置の値が0より小さい時は0とみなされます。

List.skipの実行例
List.skip 2 [1; 2; 3; 4; 5]    // [3; 4; 5]
List.skip 6 [1; 2; 3; 4; 5]    // 例外発生「System.ArgumentException: インデックスは有効範囲外です。」

List.skipWhile 条件 リスト

リストの先頭から条件に適合する要素が連続していれば、それらが取り除かれたリストを得られます。そうした要素が先頭から連続していないところに存在していても取り除かれません。そのためList.sortの実行後には特に有効でしょう。

List.skipWhileの実行例
List.skipWhile (fun n -> n < 3) [1; 2; 3; 4; 5]          // [3; 4; 5]

// ソートせずに実行
List.skipWhile (fun n -> n < 3) [1; 2; 3; 4; 1; 2; 5]    // [3; 4; 1; 2; 5]

// ソートしてから実行
[3; 1; 2; 5; 3; 4; 2; 1]
|> List.sort
|> List.skipWhile (fun n -> n < 3)    // [3; 3; 4; 5]

List.splitInto 分割数 リスト

リストを指定された数のリストに分割します。結果は分割されたリストをネストしたリストになります。分割されたリストは先頭に近いほうが要素数が多くなります。分割数が0以下のときはSystem.ArgumentExceptionが発生します。一方、分割数がリストの要素数より多い時は要素を1つ持つリストをネストしたリストになります。

List.splitIntoの実行例
List.splitInto 0 [0..4]    // 例外発生「System.ArgumentException: 入力は正である必要があります。」
List.splitInto 1 [0..4]    // [[0; 1; 2; 3; 4]]
List.splitInto 2 [0..4]    // [[0; 1; 2]; [3; 4]]
List.splitInto 3 [0..4]    // [[0; 1]; [2; 3]; [4]]
List.splitInto 4 [0..4]    // [[0; 1]; [2]; [3]; [4]]
List.splitInto 5 [0..4]    // [[0]; [1]; [2]; [3]; [4]]
List.splitInto 6 [0..4]    // [[0]; [1]; [2]; [3]; [4]]

List.take 要素数 リスト

リストの先頭から指定された数の要素をもつリストを得られます。要素数が負の数の時はSystem.ArgumentExceptionが発生します。また、要素数がリストの要素数より多い時はSystem.InvalidOperationExceptionが発生します。要素数が0のときは[](要素なしのリスト)となります。

これと同様の処理を行うList.truncate関数では例外が発生しません。必要に応じて使い分けてください。

List.takeの実行例
List.take -1 [0..3]    // 例外発生「System.ArgumentException: 入力は負以外である必要があります。」
List.take  0 [0..3]    // []
List.take  2 [0..3]    // [0; 1]
List.take  5 [0..3]    // 例外発生「System.InvalidOperationException: 入力シーケンスには十分な数の要素がありません。」

List.takeWhile 条件 リスト

リストの先頭から連続して条件に適合する要素を持つリストを得られます。リストの途中だけにそうした要素があっても抽出されません。

List.takeWhileの実行例
List.takeWhile (fun n -> n < 3) [2; 1; 5; 8; 1; 2]    // [2; 1]

List.truncate 要素数 リスト

List.takeと同様に、先頭から要素数だけ連続した要素を持つリストを得られます。結果は、要素数が0以下のときは[](要素なしのリスト)で、要素数がリストのそれより多いときは元のリストが得られるため、List.takeのような例外は発生しません。必要に応じて使い分けてください。

List.truncateの実行例
List.truncate -1 [1; 2; 3]    // [] (例外は発生しない)
List.truncate  0 [1; 2; 3]    // []
List.truncate  1 [1; 2; 3]    // [1]
List.truncate  2 [1; 2; 3]    // [1; 2]
List.truncate  3 [1; 2; 3]    // [1; 2; 3] (元のリストと同じ)
List.truncate  4 [1; 2; 3]    // [1; 2; 3] (例外は発生しない)

List.tryHead リスト

リストの先頭の要素aが存在するときはSome a、空のリストの時はNoneを得られます。

List.tryHeadの実行例
List.tryHead<int> [1; 2; 3]    // Some 1
List.tryHead<int> []           // None

List.tryItem 位置 リスト

指定した位置(先頭は0)に要素aが存在すればSome a、存在しなければNoneを得られます。インデックスでリストの要素数を超えた位置([1; 2; 3].[4]など)を指定するとSystem.ArgumentExceptionが発生しますが、List.tryItemでは結果が'T option型となり、例外が発生しません。

List.tryItemの実行例
List.tryItem  1 [1; 2; 3]    // Some 2
List.tryItem -1 [1; 2; 3]    // None (位置の指定が負の数)
List.tryItem  4 [1; 2; 3]    // None (位置が要素数を超えている)

List.windowed 要素数 リスト

現在位置から指定した数だけ連続した要素を持つリストをネストしたリストを得られます。要素数が0以下のときはSystem.ArgumentExceptionが発生します。

List.windowedの実行例
List.windowed 3 [1; 3; 5; 7; 9]    // [[1; 3; 5]; [3; 5; 7]; [5; 7; 9]]
List.windowed 0 [1; 3; 5; 7; 9]    // 例外発生「System.ArgumentException: 入力は正である必要があります。」

ここまでドキュメント(英語版2017/8/18更新分)には掲載されていない未掲載関数を抜粋で紹介しました。ドキュメントの進捗状況はgithubのissueでまとめられていて、ドキュメントができているものにはチェックがつけられています。これによると、Listモジュール以外にもドキュメントが進んでいない関数を見て取れます。

ドキュメントが更新されたら、このページは不要になると思いますが、その時までご覧になった方々の参考になればと思います。

14
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
8