F#のコードをユニットテストで検証する その3(xUnit)
前回と同じく Visual Studio 2013 のテストエクスプローラーから xUnit.net を使ったユニットテストを行います。今回は1つのテスト関数に複数のテストデータを渡す方法を試します。
xUnit.net を使ったコードの例
- module Sample2
- open Xunit
- let fibonacci (nth: int) : int=
- let rec auxFib (acc1: int) (acc0: int) (count: int) : int =
- match count with
- | 0 -> acc0
- | 1 -> acc1
- | _ -> auxFib (acc1 + acc0) acc1 (count - 1)
- auxFib 1 0 nth
- [<Theory>]
- [<InlineData( 0, 0)>]
- [<InlineData( 1, 1)>]
- [<InlineData( 2, 1)>]
- [<InlineData( 3, 2)>]
- [<InlineData( 4, 3)>]
- [<InlineData( 5, 5)>]
- [<InlineData( 6, 8)>]
- [<InlineData( 7, 13)>]
- [<InlineData( 8, 21)>]
- [<InlineData( 9, 34)>]
- [<InlineData(10, 55)>]
- [<InlineData(11, 89)>]
- [<InlineData(12, 144)>]
- [<InlineData(13, 233)>]
- [<InlineData(14, 377)>]
- [<InlineData(15, 610)>]
- let ``Fibonacci number``(nth: int, expected: int) : unit =
- let actual = fibonacci nth
- Assert.Equal(expected, actual)
4行目の fibonacci というフィボナッチ数列の第 n 項(n は0以上の整数)を求める関数をユニットテストの対象とします。
12行目 Theory アトリビュートをテスト関数 ``Fibonacci number`` (29行目から始まる)に付加します。
Theory 以降に続く13~28行目の InlineData アトリビュートの引数で指定したテストデータが ``Fibonacci number`` の引数として渡ります。InlineData からデータを渡す回数分のテストが実行されます。ここでは16個のテストデータを置いたので16回のテストが実行されることになります。
テストデータ用のフィボナッチ数の値は、サイト「THE ONーLINE ENCYCLOPEDIA OF INTEGER SEQUENCES® 」からお借りしました。
ユニットテストの実行結果
テスト関数の定義は一つですが16回のテストが実行されていることがわかります。
F#のコードをユニットテストで検証する その2(xUnit)
xUnit.net を利用してVisual Studio 2013のテストエクスプローラーでユニットテストを実行してみます。
その準備として以下の xUnit.net および xUnit.net テストランナーの NuGet パッケージをプロジェクトにインストールします。
NuGet パッケージ管理画面からインストール
xUnit.net を利用したコードの例
- module Sample1
- open Xunit
- let countWordsInStr (str: string) : int =
- let clist: char list = [ for c in str -> c ] // stringをcharの配列に変換
- let rec inWord (curList: char list) (count: int) : int =
- match curList with
- | [(**)] -> count
- | hd::tl when hd = ' ' -> inSpace tl count
- | _::tl -> inWord tl count
- and inSpace (curList: char list) (count: int) : int=
- match curList with
- | [(**)] -> count
- | hd::tl when hd = ' ' -> inSpace tl count
- | _::tl -> inWord tl (count + 1) // 空白→文字の遷移でインクリメント
- inSpace clist 0
- [<Fact>]
- let ``Word Count - Part0``() =
- Assert.Equal(0, countWordsInStr " ")
- [<Fact>]
- let ``Word Count - Part1``() =
- Assert.Equal(1, countWordsInStr " aaa")
- [<Fact>]
- let ``Word Count - Part2``() =
- Assert.Equal(2, countWordsInStr "aaa bbbb")
- [<Fact>]
- let ``Word Count - Part3``() =
- Assert.Equal(3, countWordsInStr "aaa bbbb ccccccc ")
- [<Fact>]
- let ``Word Count - Part4``() =
- Assert.Equal(4, countWordsInStr "a b c d")
4行目の countWordsInStr がテスト対象となる関数です。引数で与えられた文字列に含まれている単語数を数えます。ここではホワイトスペースのみを区切り文字として認識するように書いています。
6~15行目はこの関数内で使用するローカル関数です。let rec...and...の構文使って相互再帰(mutual recursion)する関数を2つ定義しています。16行目でこのローカル関数を使ってcountWordsInStr の戻り値を作り、そのまま返しています。
18行目以降の5つの関数がテスト用関数の定義になります。Factアトリビュートを付加した関数がテストランナーによって実行されます。xUnit の Assert.Equal メソッドで引数に渡された二つの値が等しいかを検査しています。
注)8行目・13行目にある(**)はF#用コメントです。はてなブログでは角括弧が消えてしまうことがあるのでその対策として入れてあります。
Visual Studio2013のテストエクスプローラーの実行結果
F#のコードをユニットテストで検証する その1
デフォルトのVisual Studio 2013の環境で、F#のコードのユニットテストを実行してみます。以下は普通のコンソールアプリケーションのプロジェクトに含まれているコードです。
Microsoft.VisualStudio.TestTools.UnitTesting名前空間を使うので、予め Microsoft.VisualStudio.QualityTools.UnitTestFramework への参照をプロジェクトに追加しておきます。
コードの例
- module Test01
- (* 参照の追加:Microsoft.VisualStudio.QualityTools.UnitTestFramework *)
- open Microsoft.VisualStudio.TestTools.UnitTesting
- let rangeData = [1..10]
- [<TestClass>]
- type ``A unit testing exercise``() =
- let mutable myList = []
- [<TestInitialize>]
- member this.setup(): unit =
- // テストの準備
- myList <- rangeData
- [<TestMethod>]
- member this.body() : unit =
- let sum = List.fold (+) 0 myList // リスト要素の総和を求める
- Assert.AreEqual(55 , sum)
- [<TestCleanup>]
- member this.teardown(): unit =
- // テストの後始末
- ()
F#ではキーワード type を使ってclass宣言を行います。8行目の``A unit testing exercise``がユニットテストに用いるクラス名です。ユニットテストで使うことを明示するために TestClassアトリビュートを付加しておきます。クラス名をバッククォート記号「`」二つずつを前後に使って囲っているのは識別子名にホワイトスペースが含まれているからです。F#ではバッククォート記号で識別子名を囲むことによって識別子に使えない文字を混ぜることができます。
12行目のTestInitializeアトリビュートの付いたメソッドsetupはテスト実行前に必要な処理を書きます。F#ではアトリビュート識別子は [< ... >] で囲みます。
メソッドシグネチャは(unit -> unit)。
17行目のTestMethodアトリビュートの付いたメソッドbodyはテストで使われる本体部分です。
メソッドシグネチャは(unit -> unit)。ここではリストの要素(1から10までの整数)の総和を求めて55に等しいかを Assert.AreEqual メソッドを使って検査しています。
22行目のTestCleanupアトリビュートの付いたメソッドteardownはテスト完了後に必要な後処理を書きます。この例では特に何もしないので()を返しています。
メソッドシグネチャは(unit -> unit)。
テストを実行するために、メニューバーから「テスト」→「ウィンドウ」→「テストエクスプローラー」を選択してテスト画面を開きます。
テストクスプローラー上で「すべて実行」をクリックするとソースコードはビルドされユニットテストが実行されて結果が表示されます。以下はこのテストが成功した場面。
ここで先ほどのコード19行目の Assert.AreEqual を Assert.AreNotEqual に書き変えてわざとテストを失敗させてみます。再度テストを実行すると以下のような結果が得られます。
F#でなるべく簡単にグラフ表示をさせたい その4
今回はチャート・ライブラリ F# Chartingを利用してグラフを作成します。
VisualStudio 2013のプロジェクトにNugetを利用してライブラリをインストールするためにプロジェクトの「参照設定」から、右クリックメニュー項目「Nugetパッケージの管理」を選択。
Nugetパッケージ管理の画面で、オンラインで「fsharp.charting」と文字列検索します。「FSharp.Charting」の項目が見つかったら「インストール」のボタンを押してインストール実行。
ライブラリを使って折れ線グラフを描くコードの例
プロジェクトの参照設定に、FSharp.Charting・System.Windows.Forms・System.Drawingが追加されていることを確認。
open System open FSharp.Charting [<EntryPoint; STAThread>] let main _ = let fscChart: FSharp.Charting.ChartTypes.GenericChart = [ for x in 0.0.. 0.02 .. (Math.PI * 2.0) -> (x, Math.Sin x) ] |> Chart.Line let chartCtrl: FSharp.Charting.ChartTypes.ChartControl = new FSharp.Charting.ChartTypes.ChartControl(fscChart) chartCtrl.Dock <- System.Windows.Forms.DockStyle.Fill let form = new System.Windows.Forms.Form(Width = 700, Height = 400, Text = "FSharp.Chartingのテスト") form.Controls.Add(chartCtrl) System.Windows.Forms.Application.Run(form) 0
実行結果
Windowsアプリケーションとしてビルドして実行すると、以下のようなウィンドウが表示されます。
[2016-02-04 追記]
X軸方向のグリッド位置にハンパな値を割り振られてしまうので修正。
let fscChart: FSharp.Charting.ChartTypes.GenericChart = [ for x in 0.0.. 0.02 .. (Math.PI * 2.0) -> (x, Math.Sin x) ] |> Chart.Line
上記の部分に1行追加。
let fscChart: FSharp.Charting.ChartTypes.GenericChart = [ for x in 0.0.. 0.02 .. (Math.PI * 2.0) -> (x, Math.Sin x) ] |> Chart.Line |> Chart.WithXAxis(Min = 0.0)
ChartクラスのWithXAxisメソッドを呼び出してX軸方向の最小値を明示的に指定。この追加行において、クラスのメソッド引数のみで利用可能な「名前付き引数(Named Arguments)」及び「省略可能なパラメータ(Optional Arguments)」の文法が使われている。詳細はMSDN:パラメーターと引数 (F#)を参照のこと。
実行結果
F#でリスト要素のデカルト積を求める
二つのリスト
[0; 1]
[2; 3]
のデカルト積(Cartesian product)、つまり互いのリスト要素どうしのすべての組み合わせによる集合をリストで表すと
[ [0; 2]; [0; 3]; [1; 2]; [1; 3] ]
と表せます。また、三つのリスト
[0; 1]
[2; 3]
[4; 5]
ならば
[ [0; 2; 4]; [0; 2; 5]; [0; 3; 4]; [0; 3; 5]; [1; 2; 4]; [1; 2; 5]; [1; 3; 4]; [1; 3; 5] ]
と表せます。Haskellなどの文法と違って、F#はリスト要素を','(コンマ)ではなく';'(セミコロン)で区切ります。(F#でコンマはタプル(tuple)の要素を区切るときに使います。)
複数のリストのデカルト積を求めるコードをF#で書いてみます。参考にした資料は『Thinking Functionally with Haskell』chapter 5 - A simple Sudoku solver。資料中にある以下のHaskellのコードの動作をF#で真似してみます。
cp :: [[a]] -> [[a]]
cp [] = [[]]
cp (xs:xss) = [x:ys | x <- xs, ys <- yss]
where yss = cp xss
これをF#でできるだけ似た形のコードに書き直してみます。
// 再帰呼び出しがあるので'rec'を付ける let rec cartesianp (sources: list<list<'a>>): list<list<'a>> = match sources with | [] -> [[]] | xs::xss -> // Haskellと違ってcons演算子のコロンは二つ [ for x in xs do for ys in (cartesianp xss) do // 再帰呼び出し yield x::ys ]
Haskellの多相データ型の型変数に似た文法として、F#ではジェネリック型パラメータを「'a」のようにシングルクォートで始まる識別子を使って書くことができます。
再帰呼び出しとリスト内包表記(list comprehension)を用いています。F#にはHaskellのwhere節に相当する文法はないので、再帰呼び出し箇所はリスト内包表記部分の内側に直接埋め込んであります。
F#にもリスト内包表記の文法があるためHaskellのリスト内包表記に似たコードの書き方ができます。ただしHaskellと違って遅延評価は行われません。
Visual Studio の F# Interactive の環境でこの関数を実行してみます。
実行例
> cartesianp [[0;1]; [2; 3]];;
val it : int list list = [[0; 2]; [0; 3]; [1; 2]; [1; 3]]
> cartesianp [[0;1]; [2; 3]; [4; 5]];;
val it : int list list =
[[0; 2; 4]; [0; 2; 5]; [0; 3; 4]; [0; 3; 5]; [1; 2; 4]; [1; 2; 5]; [1; 3; 4];
[1; 3; 5]]
さらにプログラマ向け質問サイト Stack Overflow にもデカルト積に関連する質問が載っていたので参考にしました。こちらのコードもHaskellで書かれています。
cartesianProduct :: [[a]] -> [[a]]
cartesianProduct sequences = foldr aggregator [[]] sequences
where aggregator sequence accumulator =
[ item:accseq |item <- sequence, accseq <- accumulator ]引用元:haskell - Calculate n-ary Cartesian Product - Stack Overflow
リストの右畳み込み(foldr)を使ったコードが載っているのでこれをF#の標準ライブラリでも用意されている右畳み込み関数foldBackを利用して書き直してみます。
let cartesianProduct (sequences: list<list<'a>>) : list<list<'a>> = let aggregator (sequence: list<'a>) (accumulator: list<list<'a>>) : list<list<'a>> = [ for item in sequence do for accseq in accumulator do yield item::accseq ] List.foldBack aggregator sequences [[]] // 引数の順序がHaskellとは異なるので注意
F#にはfoldrという関数はないので同様の機能を持つList.foldBackで代用しています。ただし、Haskellのfoldrとは引数の与え方が異なるので注意が必要です。
また、where節に相当する文法はないので、where節に書かれた内容をList.foldBackを呼び出す行よりも前の位置に書いています。
実行例
> cartesianProduct [[0; 1]; [2; 3]];; val it : int list list = [[0; 2]; [0; 3]; [1; 2]; [1; 3]]
> cartesianProduct [[0; 1]; [2; 3]; [4; 5]];; val it : int list list = [[0; 2; 4]; [0; 2; 5]; [0; 3; 4]; [0; 3; 5]; [1; 2; 4]; [1; 2; 5]; [1; 3; 4]; [1; 3; 5]]