Getting a Good Grasp of F# (仮)

関数型言語F#をもっと楽しみたい

F#のコードをユニットテストで検証する その2(xUnit)

xUnit.net を利用してVisual Studio 2013のテストエクスプローラーでユニットテストを実行してみます。

その準備として以下の xUnit.net および xUnit.net テストランナーの NuGet パッケージをプロジェクトにインストールします。

 NuGet パッケージ管理画面からインストール

f:id:pongitsune:20160213214520p:plain

xUnit.net を利用したコードの例
  1. module Sample1
  2. open Xunit
  3.  
  4. let countWordsInStr (str: string) : int =
  5.     let clist: char list = [ for c in str -> c ] // stringをcharの配列に変換
  6.     let rec inWord (curList: char list) (count: int) : int =
  7.         match curList with
  8.         | [(**)] -> count
  9.         | hd::tl when hd = ' ' -> inSpace tl count
  10.         |  _::tl               -> inWord  tl count
  11.     and inSpace (curList: char list) (count: int) : int=
  12.         match curList with
  13.         | [(**)] -> count
  14.         | hd::tl when hd = ' ' -> inSpace tl count
  15.         |  _::tl               -> inWord  tl (count + 1) // 空白→文字の遷移でインクリメント
  16.     inSpace clist 0
  17.      
  18. [<Fact>]
  19. let ``Word Count - Part0``() =
  20.     Assert.Equal(0,  countWordsInStr " ")
  21.  
  22. [<Fact>]
  23. let ``Word Count - Part1``() =
  24.     Assert.Equal(1,  countWordsInStr " aaa")    
  25.  
  26. [<Fact>]
  27. let ``Word Count - Part2``() =
  28.     Assert.Equal(2,  countWordsInStr "aaa bbbb")    
  29.  
  30. [<Fact>]
  31. let ``Word Count - Part3``() =
  32.     Assert.Equal(3,  countWordsInStr "aaa bbbb ccccccc ")
  33.    
  34. [<Fact>]
  35. let ``Word Count - Part4``() =
  36.     Assert.Equal(4,  countWordsInStr "a b c d")

4行目の countWordsInStr がテスト対象となる関数です。引数で与えられた文字列に含まれている単語数を数えます。ここではホワイトスペースのみを区切り文字として認識するように書いています。

6~15行目はこの関数内で使用するローカル関数です。let rec...and...の構文使って相互再帰(mutual recursion)する関数を2つ定義しています。16行目でこのローカル関数を使ってcountWordsInStr の戻り値を作り、そのまま返しています。

18行目以降の5つの関数がテスト用関数の定義になります。Factアトリビュートを付加した関数がテストランナーによって実行されます。xUnitAssert.Equal メソッドで引数に渡された二つの値が等しいかを検査しています。

注)8行目・13行目にある(**)はF#用コメントです。はてなブログでは角括弧が消えてしまうことがあるのでその対策として入れてあります。

Visual Studio2013のテストエクスプローラーの実行結果

f:id:pongitsune:20160213214246p:plain

 

F#のコードをユニットテストで検証する その1

デフォルトのVisual Studio 2013の環境で、F#のコードのユニットテストを実行してみます。以下は普通のコンソールアプリケーションのプロジェクトに含まれているコードです。
Microsoft.VisualStudio.TestTools.UnitTesting名前空間を使うので、予め Microsoft.VisualStudio.QualityTools.UnitTestFramework への参照をプロジェクトに追加しておきます。

コードの例
  1. module Test01
  2. (* 参照の追加:Microsoft.VisualStudio.QualityTools.UnitTestFramework *)
  3. open Microsoft.VisualStudio.TestTools.UnitTesting
  4.  
  5. let rangeData = [1..10]
  6.  
  7. [<TestClass>]
  8. type ``A unit testing exercise``() =
  9.     let mutable myList = []
  10.  
  11.     [<TestInitialize>]  
  12.     member this.setup(): unit =
  13.         // テストの準備
  14.         myList <- rangeData
  15.  
  16.     [<TestMethod>]
  17.     member this.body() : unit =
  18.         let sum = List.fold (+) 0 myList  // リスト要素の総和を求める
  19.         Assert.AreEqual(55 , sum)
  20.  
  21.     [<TestCleanup>]  
  22.     member this.teardown(): unit =
  23.         // テストの後始末
  24.         ()

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)。

テストを実行するために、メニューバーから「テスト」→「ウィンドウ」→「テストエクスプローラー」を選択してテスト画面を開きます。

f:id:pongitsune:20160210204115j:plain

テストクスプローラー上で「すべて実行」をクリックするとソースコードはビルドされユニットテストが実行されて結果が表示されます。以下はこのテストが成功した場面。

f:id:pongitsune:20160210204322j:plain

ここで先ほどのコード19行目の Assert.AreEqual を Assert.AreNotEqual に書き変えてわざとテストを失敗させてみます。再度テストを実行すると以下のような結果が得られます。

f:id:pongitsune:20160210204655j:plain

F#でなるべく簡単にグラフ表示をさせたい その4

今回はチャート・ライブラリ F# Chartingを利用してグラフを作成します。

VisualStudio 2013のプロジェクトにNugetを利用してライブラリをインストールするためにプロジェクトの「参照設定」から、右クリックメニュー項目「Nugetパッケージの管理」を選択。

f:id:pongitsune:20160203185959p:plain

 Nugetパッケージ管理の画面で、オンラインで「fsharp.charting」と文字列検索します。「FSharp.Charting」の項目が見つかったら「インストール」のボタンを押してインストール実行。

f:id:pongitsune:20160203190633p:plain

ライブラリを使って折れ線グラフを描くコードの例

プロジェクトの参照設定に、FSharp.Charting・System.Windows.Forms・System.Drawingが追加されていることを確認。

open System
open FSharp.Charting  
 
[<EntryPointSTAThread>]
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アプリケーションとしてビルドして実行すると、以下のようなウィンドウが表示されます。

f:id:pongitsune:20160203193900p:plain

[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:id:pongitsune:20160204211149p:plain

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

 引用元:『Thinking Functionally with Haskell

 

これを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で代用しています。ただし、Haskellfoldrとは引数の与え方が異なるので注意が必要です。

また、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]]

 

F#でなるべく簡単にグラフ表示をさせたい その3

名前空間:System.Windows.Forms.DataVisualization.Chartingのクラスを使って.NET標準ライブラリのみでグラフを描いてみます。

Windowsフォーム(Formオブジェクト)に下図のような階層で必要なオブジェクトを追加していきます。

Form
ChartControl
ChartArea(データのプロット領域、座標軸やグリッド線の設定)
Title(タイトル)
Series(グラフデータその1)
Series(グラフデータその2)
Legend(凡例)

sin(x)とcos(x)のグラフを描くコードの例

  1. open System.Drawing
  2. open System.Windows.Forms
  3. open System.Windows.Forms.DataVisualization.Charting
  4. open System
  5. open System.Diagnostics
  6.  
  7. [<EntryPoint; STAThread>]
  8. let main _ =
  9.     let area = new ChartArea() in // チャートエリア
  10.     do  
  11.         area.Name <- "CHART_AREA"
  12.         // グリッドは灰色で破線
  13.         area.AxisX.MajorGrid.LineColor <- Color.LightGray
  14.         area.AxisX.MajorGrid.LineDashStyle <- ChartDashStyle.Dash
  15.  
  16.         area.AxisY.MajorGrid.LineColor <- Color.LightGray
  17.         area.AxisY.MajorGrid.LineDashStyle <- ChartDashStyle.Dash
  18.  
  19.         // グリッド間隔は0.2
  20.         area.AxisX.Interval <- 0.2
  21.         area.AxisY.Interval <- 0.2
  22.         // グリッド描画開始点をずらす
  23.         area.AxisX.IntervalOffset <- - (Math.PI |> Math.Floor |> (+) 0.2)
  24.  
  25.         // 描画範囲指定
  26.         area.AxisX.Minimum <- area.AxisX.IntervalOffset
  27.         area.AxisY.Minimum <- -1.0  
  28.         area.AxisY.Maximum <- 1.0
  29.  
  30.         // X軸とY軸を原点で交差させる
  31.         area.AxisX.Crossing <- 0.0
  32.         area.AxisY.Crossing <- 0.0
  33.  
  34.     let chartCtrl = new Chart() in
  35.     do  
  36.         let title = new Title("ここにチャートコントロールに表示するタイトルが入ります",
  37.                                 Docking.Top,
  38.                                 new Font("Meiryo", 12.0f),
  39.                                 Color.Brown)
  40.         chartCtrl.Titles.Add(title)
  41.         chartCtrl.Dock <- DockStyle.Fill
  42.  
  43.     chartCtrl.ChartAreas.Add(area)  // チャートコントロールにチャートエリアを追加
  44.  
  45.     let series1 = new Series() in // グラフその1
  46.     do  series1.ChartType <- SeriesChartType.Line  // 折れ線
  47.         series1.Color <- Color.Blue
  48.         series1.BorderWidth <- 2   // 線の太さ
  49.         series1.LegendText <- "sin x"
  50.  
  51.     chartCtrl.Series.Add(series1)  // チャートコントロールにグラフその1を追加
  52.  
  53.     let series2 = new Series() in // グラフその2
  54.     do  series2.ChartType <- SeriesChartType.Line  // 折れ線
  55.         series2.Color <- Color.Red
  56.         series2.BorderWidth <- 2  // 線の太さ
  57.         series2.LegendText <- "cos x"
  58.  
  59.     chartCtrl.Series.Add(series2)  // チャートコントロールにグラフその2を追加
  60.  
  61.     let legend = new Legend() in // 凡例
  62.     do  legend.Title <- "凡例"
  63.         legend.DockedToChartArea <- area.Name  // 凡例を描画するチャートエリアの名前を指定
  64.         legend.Alignment <- StringAlignment.Near
  65.     
  66.     chartCtrl.Legends.Add(legend)  // チャートコントロールに凡例を追加
  67.  
  68.     let mainForm = new Form(Visible = true,
  69.                             Width = 1200,
  70.                             Height = 600,
  71.                             Text= "標準ライブラリを用いたグラフの例")
  72.  
  73.     mainForm.Controls.Add(chartCtrl)  // フォームにチャートコントロールを追加
  74.  
  75.     (*** プロットするデータを準備 ***)
  76.     let horizData = [ for x in -Math.PI..0.02..Math.PI -> x ]
  77.     let vertData1 = [ for x in horizData -> Math.Sin x ]  
  78.     let vertData2 = [ for x in horizData -> Math.Cos x ]
  79.  
  80.     Debug.Assert(Seq.length horizData = Seq.length vertData1)
  81.     Debug.Assert(Seq.length horizData = Seq.length vertData2)
  82.  
  83.     series1.Points.DataBindXY (horizData, vertData1)
  84.     series2.Points.DataBindXY (horizData, vertData2)
  85.  
  86.     Application.Run(mainForm)
  87.     0 // 整数の終了コードを返します

Windowsフォームアプリケーションなので、プロジェクトのビルド出力の設定を「Windowsアプリケーション」に設定。

f:id:pongitsune:20160127182941p:plain

実行結果

f:id:pongitsune:20160127160000p:plain