Docker で F# アプリケーションを作成

Microsoft による .NET Core の公式 Docker イメージを使って、単純な F# のコンソールアプリケーションを作成してみました。

今回のソースは http://github.com/fits/try_samples/tree/master/blog/20170619/

(1) Docker コンテナの実行

まずは .NET Core の Docker コンテナを実行しておきます。

.NET Core コンテナの実行
$ docker run -it --rm microsoft/dotnet /bin/bash
root@・・・:/# 

現時点では 1.1.2-sdk が latest となっており、デフォルトで netcoreapp1.1 のアプリケーションを作成する事になります。

(2) F# コンソールプロジェクトの作成

.NET Core SDK では dotnet コマンド (.NET Command Line Tools) を使ってアプリケーションを開発できます。

dotnet new コマンドで様々なプロジェクトの雛型を作成でき、デフォルトでは以下のようなプロジェクトに対応しています。

プロジェクトの雛型一覧
root@・・・# dotnet new -all

・・・
Templates                 Short Name       Language      Tags
-----------------------------------------------------------------------
Console Application       console          [C#], F#      Common/Console
Class library             classlib         [C#], F#      Common/Library
Unit Test Project         mstest           [C#], F#      Test/MSTest
xUnit Test Project        xunit            [C#], F#      Test/xUnit
ASP.NET Core Empty        web              [C#]          Web/Empty
ASP.NET Core Web App      mvc              [C#], F#      Web/MVC
ASP.NET Core Web API      webapi           [C#]          Web/WebAPI
Nuget Config              nugetconfig                    Config
Web Config                webconfig                      Config
Solution File             sln                            Solution
・・・

今回は F# のコンソールアプリケーションを作成します。

適当なディレクトリを作成・移動し、dotnet new console を実行します。 F# の場合は -lang オプションで指定します。 (デフォルトは C#

F# コンソールプロジェクトの作成
root@・・・# mkdir sample
root@・・・# cd sample
root@・・・# dotnet new console -lang F#

以下のファイルが生成されました。

Program.fs
open System

[<EntryPoint>]
let main argv =
    printfn "Hello World from F#!"
    0 // return an integer exit code
sample.fsproj
<Project Sdk="FSharp.NET.Sdk;Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp1.1</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <Compile Include="Program.fs" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="FSharp.Core" Version="4.1.*" />
    <PackageReference Include="FSharp.NET.Sdk" Version="1.0.*" PrivateAssets="All" />
  </ItemGroup>
</Project>

(3) ビルドと実行

とりあえず、このままビルドして実行してみます。

まずは dotnet restore を実行してプロジェクトの依存関係を復元します。

これにより依存関係などが定義された obj/project.assets.json ファイルが作られます。

リストア
root@・・・# dotnet restore

・・・
  Installing FSharp.Compiler.Tools 4.1.17.
  Installing FSharp.NET.Sdk 1.0.5.
  Installing FSharp.Core 4.1.17.
  ・・・
  Installed:
      3 package(s) to /sample/sample.fsproj

ビルドは dotnet build で行います。 なお、dotnet build しなくても dotnet run すればビルドも実施してくれるようです。

ビルド
root@・・・# dotnet build

・・・
Build succeeded.
    0 Warning(s)
    0 Error(s)

dotnet run で実行します。

実行
root@・・・# dotnet run

Hello World from F#!

(4) Chiron パッケージの追加

次に、Chiron という F# で JSON を処理するためのパッケージを使ってみます。

依存パッケージは dotnet add package <パッケージ名> で追加できますが、(現時点で)デフォルトの Chiron 6.2.1 を追加しようとすると以下のようにエラーとなりました。

Chiron パッケージ(6.2.1)追加 - 失敗
root@・・・# dotnet add package chiron

・・・
log  : Installing Chiron 6.2.1.
error: Package Chiron 6.2.1 is not compatible with netcoreapp1.1 (.NETCoreApp,Version=v1.1). Package Chiron 6.2.1 supports:
error:   - net40 (.NETFramework,Version=v4.0)
error:   - portable-net45+win8+wp8+wpa81 (.NETPortable,Version=v0.0,Profile=Profile259)
・・・
error: One or more packages are incompatible with .NETCoreApp,Version=v1.1.
error: Package 'chiron' is incompatible with 'all' frameworks in project '/sample/sample.fsproj'.

netcoreapp1.1 (.fsproj の TargetFramework で設定) を Chiron 6.2.1 がサポートしてない事が原因のようです。

nuget の Chiron のページ を見てみると .NETStandard 1.6 に対応したバージョン 7.0.0-alpha-170410 がありました。

netcoreapp1.1 は .NETStandard 1.6 をサポートしているので、このバージョンなら使えそうです。

add package では -v オプションでバージョンを指定できるので、以下のようにバージョンを指定して Chiron を追加します。

Chiron パッケージ(7.0.0-alpha-170410)追加 - 成功
root@・・・# dotnet add package chiron -v 7.0.0-alpha-170410

・・・
log  : Installing FParsecCS 1.0.3-alpha-170404.
log  : Installing FParsec 1.0.3-alpha-170404.
log  : Installing Chiron 7.0.0-alpha-170410.
info : Package 'chiron' is compatible with all the specified frameworks in project '/sample/sample.fsproj'.
info : PackageReference for package 'chiron' version '7.0.0-alpha-170410' added to file '/sample/sample.fsproj'.

今度は成功し、.fsproj は以下のようになりました。

sample.fsproj (Chiron 追加後)
<Project Sdk="FSharp.NET.Sdk;Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp1.1</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <Compile Include="Program.fs" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="chiron" Version="7.0.0-alpha-170410" />
    <PackageReference Include="FSharp.Core" Version="4.1.*" />
    <PackageReference Include="FSharp.NET.Sdk" Version="1.0.*" PrivateAssets="All" />
  </ItemGroup>
</Project>

(5) Chiron サンプルアプリケーションの実装

動作確認のため Chiron を使った単純な処理を実装してみました。 (Chiron 7 ではモジュール構成等が変わっているようでした)

Program.fs
open System
open Chiron.Inference

[<EntryPoint>]
let main argv =
    let d = Map ["a", 123]
    let res = Json.serialize d

    printfn "json = %s" res
    0

(6) Chiron サンプルアプリケーションのビルドと実行

(3) と同様にリストアしてから実行します。

リストア
root@・・・# dotnet restore

  Restoring packages for /sample/sample.fsproj...
  ・・・
ビルド
root@・・・# dotnet build

・・・
Build succeeded.
    0 Warning(s)
    0 Error(s)
実行
root@・・・# dotnet run

json = {"a":123}

ジニ不純度の算出2 - Ruby, C#, F#, Erlang

前回 に続き、今回は下記のようなプログラム言語でジニ不純度(ジニ係数)の算出処理を同じように実装してみました。

今回のソースは http://github.com/fits/try_samples/tree/master/blog/20140608/

Ruby で実装

Ruby では group_by で要素毎の Hash オブジェクトを取得できます。
(下記では {"A"=>["A", "A"], "B"=>["B", "B", "B"], "C"=>["C"]}

なお、Hash で map した結果は配列になります。
(下記 list.group_by {|x| x }.map {|k, v| [k, v.size.to_f / xs.size] } の結果は [["A", 0.33・・・], ["B", 0.5], ["C", 0.16・・・]]

また、combination(2) で前回の Scala の関数と同様に 2要素の組み合わせを取得できます。
(下記では、[["A", 0.33・・・], ["B", 0.5]], [["A", 0.33・・・], ["C", 0.16・・・]], [["B", 0.5], ["C", 0.16・・・]]

gini.rb
#coding:utf-8

# (a) 1 - (AA + BB + CC)
def giniA(xs)
    1 - xs.group_by {|x| x }.inject(0) {|a, (k, v)| a + (v.size.to_f / xs.size) ** 2 }
end

# (b) AB × 2 + AC × 2 +  BC × 2
def giniB(xs)
    xs.group_by {|x| x }.map {|k, v| [k, v.size.to_f / xs.size] }.combination(2).inject(0) {|a, t| a + t.first.last * t.last.last * 2}
end

list = ["A", "B", "B", "C", "B", "A"]

puts giniA(list)
puts giniB(list)
実行結果
> ruby gini.rb

0.6111111111111112
0.611111111111111

C# で実装

LINQGroupBy メソッドを使えば要素毎にグルーピングした IGrouping<TKey, TSource> のコレクションを取得できます。

要素の組み合わせも LINQ のクエリ式を使えば簡単に作成できます。(下記の combination メソッド

gini.cs
using System;
using System.Collections.Generic;
using System.Linq;

class Gini
{
    public static void Main(string[] args)
    {
        var list = new List<string>() {"A", "B", "B", "C", "B", "A"};

        Console.WriteLine("{0}", giniA(list));
        Console.WriteLine("{0}", giniB(list));
    }

    // (a) 1 - (AA + BB + CC)
    private static double giniA<K>(IEnumerable<K> xs)
    {
         return 1 - xs.GroupBy(x => x).Select(x => Math.Pow((double)x.Count() / xs.Count(), 2)).Sum();
    }

    // (b) AB + AC + BA + BC + CA + CB
    private static double giniB<K>(IEnumerable<K> xs)
    {
        return
            combination(
                countBy(xs).Select(t =>
                    Tuple.Create(t.Item1, (double)t.Item2 / xs.Count())
                )
            ).Select(x => x.Item1.Item2 * x.Item2.Item2).Sum();
    }

    private static IEnumerable<Tuple<K, int>> countBy<K>(IEnumerable<K> xs) {
        return xs.GroupBy(x => x).Select(g => Tuple.Create(g.Key, g.Count()));
    }

    // 異なる要素の組み合わせを作成
    private static IEnumerable<Tuple<Tuple<K, V>, Tuple<K, V>>> combination<K, V>(IEnumerable<Tuple<K, V>> data) {
        return
            from x in data
            from y in data
            where !x.Item1.Equals(y.Item1)
            select Tuple.Create(x, y);
    }
} 
実行結果
> csc gini.cs
> gini.exe

0.611111111111111
0.611111111111111

F# で実装

  • F# 3.1

F# では Seq.countBy で要素毎のカウント値を取得できます。
(下記では seq [("A", 2); ("B", 3); ("C", 1)]

要素の組み合わせは内包表記を使えば簡単に作成できます。 (下記の combinationCount)

gini.fs
let size xs = xs |> Seq.length |> float

// (a) 1 - (AA + BB + CC)
let giniA xs = xs |> Seq.countBy id |> Seq.sumBy (fun (k, v) -> (float v / size xs) ** 2.0) |> (-) 1.0

let combinationCount cs = [
    for x in cs do
        for y in cs do
            if fst x <> fst y then
                yield (snd x, snd y)
]

// (b) AB + AC + BA + BC + CA + CB
let giniB xs = xs |> Seq.countBy id |> combinationCount |> Seq.sumBy (fun (x, y) -> (float x / size xs) * (float y / size xs))

let list = ["A"; "B"; "B"; "C"; "B"; "A";]

printfn "%A" (giniA list)
printfn "%A" (giniB list)
実行結果
> fsc gini.fs
> gini.exe

0.6111111111
0.6111111111

Erlang で実装

Erlang ではグルーピング処理等は用意されていないようなので自前で実装しました。 (今回は dict モジュールを使いました)

リスト内包表記 ([<構築子> || <限定子>, ・・・]) で使用するジェネレータ (<変数> <- <式>) の右辺はリストになる式を指定する必要があるため、dict:to_list() でリスト化しています。

gini.erl
-module(gini).
-export([main/1]).

groupBy(Xs) -> lists:foldr(fun(X, Acc) -> dict:append(X, X, Acc) end, dict:new(), Xs).

countBy(Xs) -> dict:map( fun(_, V) -> length(V) end, groupBy(Xs) ).

% (a) 1 - (AA + BB + CC)
giniA(Xs) -> 1 - lists:sum([ math:pow(V / length(Xs), 2) || {_, V} <- dict:to_list(countBy(Xs)) ]).

combinationProb(Xs) -> [ {Vx, Vy} || {Kx, Vx} <- Xs, {Ky, Vy} <- Xs, Kx /= Ky ].

% (b) AB + AC + BA + BC + CA + CB
giniB(Xs) -> lists:sum([ (Vx / length(Xs)) * (Vy / length(Xs)) || {Vx, Vy} <- combinationProb(dict:to_list(countBy(Xs))) ]).

main(_) ->
    List = ["A", "B", "B", "C", "B", "A"],

    io:format("~p~n", [ giniA(List) ]),
    io:format("~p~n", [ giniB(List) ]).
実行結果
> escript gini.erl

0.6111111111111112
0.611111111111111

なお、groupBy(List)countBy(List) の結果を出力すると下記のようになりました。

groupBy(List) の出力結果
{dict,3,16,16,8,80,48,
      {[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
      {{[],
        [["B","B","B","B"]],
        [],[],[],[],
        [["C","C"]],
        [],[],[],[],[],
        [["A","A","A"]],
        [],[],[]}}}
countBy(List) の出力結果
{dict,3,16,16,8,80,48,
      {[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
      {{[],
        [["B"|3]],
        [],[],[],[],
        [["C"|1]],
        [],[],[],[],[],
        [["A"|2]],
        [],[],[]}}}

非同期処理でWebコンテンツをダウンロードする方法 - Groovy, Scala, F#

前回(id:fits:20110925)、並列コレクション等で Web コンテンツをダウンロードする処理を実装してみましたが、今回はその非同期処理版を Groovy, Scala, F# で実装してみました。(主な仕様は前回と同じ)

実行例
groovy async_download_web.groovy destdir < urls.txt

今回、非同期処理のために使用した機能は以下の通りです。

  • Groovy : GPars
  • Scala : 限定継続 + Actor
  • F# : 非同期ワークフロー

サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20111016/

Groovy の場合: GPars

非同期処理でも GPars が使えます。

GParsPool.withPool や GParsExecutorsPool.withPool 内*1で、クロージャに対して async() を実行すると非同期化されたクロージャを取得できます。

非同期化されたクロージャを実行すると Future が返り、Future に対して get() を呼び出す事で処理結果を取得します。(ただし、処理が完了していないと完了するまで待ちになる)
非同期化されたクロージャ内で発生した例外も get() 実行時に throw されます。


今回は指定の URL に接続する処理とダウンロードしてローカルファイルを作成する処理を非同期化し、全 URL を非同期で処理した後にループで 1件ずつ完了待ちするようにしてみました。
ダウンロードを完了したものからでは無く、1件ずつ順番に処理結果待ちと文字列出力("downloaded: ・・・" の出力)を行っているため、ダウンロード処理の遅いものが途中にあると文字列出力が停滞してしまう点は改善が必要かと思います。(ちなみに、each の代わりに eachParallel にすると URL 接続に失敗する等で "failed: ・・・" が出力された際にスクリプトが終わらなくなる等の不都合がありました)

また、GParsExecutorsPool よりも GParsPool の方が速く感じたので、今回は GParsPool を使っています。(withPool も数値を指定した方が速かった)

  • Groovy 1.8.2
async_download_web.groovy
import groovyx.gpars.*

def dir = args[0]

GParsPool.withPool(10) {
    //非同期化したURL接続処理
    def openUrl = { it.newInputStream() }.async()
    //非同期化したダウンロード処理
    def downloadUrl = { f, ou -> f.bytes = ou.get().bytes }.async()

    //全 URL に対して非同期処理を実行
    System.in.readLines() collect {
        def url = new URL(it)
        def file = new File(dir, new File(url.file).name)

        //downloadUrl や openUrl は Future を直ぐに返し、
        //非同期的に実行される
        [url: url, file: file, result: downloadUrl(file, openUrl(url))]

    } each {
        try {
            //1件ずつ処理の完了待ち
            it.result.get()
            println "downloaded: ${it.url} => ${it.file}"
        } catch(e) {
            println "failed: ${it.url}, $e"
        }
    }
}

Scala の場合: 限定継続 + Actor

今回、Scala では限定継続と Actor を使って非同期処理を実装してみました。(限定継続の処理内容に関しては id:fits:20100207 参照)

Groovy 等と比べるとコード量は多くなりますが、継続オブジェクト(下記サンプルでは k の箇所)を Actor に渡す事で非同期処理を同期的に実装できるようになります。

下記サンプルでは、(1) 〜 (6) のような処理の流れが非同期的に実行されるようになっています。(foreach はすぐに完了し Actor 処理の実行/完了待ちになります)

非同期化している箇所は Groovy 版のサンプルとほぼ同じですが、ダウンロードが完了したものから処理結果を文字列出力するようになっています。

  • Scala 2.9.1(JavaSE 7 依存)

なお、Scala 2.9.1 で限定継続を使う場合は実行時に -P:continuations:enable オプション指定が必要になります。

実行例(限定継続を有効化)
scala -P:continuations:enable async_download_web.scala destdir < urls.txt
async_download_web.scala
import scala.actors.Actor
import scala.actors.Actor._
import scala.io.Source
import scala.util.continuations._

import java.io.{InputStream, File}
import java.net.URL
import java.nio.file.{Paths, Files, Path}
import java.nio.file.StandardCopyOption._

val using = (st: InputStream) => (block: InputStream => Unit) => try {block(st)} finally {st.close()}

//URL接続用パラメータ
case class URLOpen(val url: URL, val k: (InputStream => Unit))
//ダウンロード用パラメータ
case class URLDownload(val url: URL, val stream: InputStream, 
                val destDir: String, val k: (Path => Unit))

class URLActor extends Actor {
    def act() {
        loop {
            react {
                //URL接続処理 (2)
                case uo: URLOpen => {
                    try {
                        //継続(uo.k)を実行することで
                        //val stream に openStream の結果が設定され、
                        // (3) から処理が継続される
                        uo.k(uo.url.openStream())
                    }
                    catch {
                        //例外発生時は結果を出力して Actor を停止
                        case e: Exception => failStop(e, uo.url)
                    }
                }
                //ダウンロード処理 (5)
                case rs: URLDownload => {
                    val f = new File(rs.url.getFile()).getName()
                    val filePath = Paths.get(rs.destDir, f)

                    try {
                        using (rs.stream) {stream =>
                            Files.copy(stream, filePath, REPLACE_EXISTING)
                        }
                        //継続(rs.k)を実行することで
                        //val file に filePath が設定され、
                        // (6) から処理が継続される
                        rs.k(filePath)
                    }
                    catch {
                        //例外発生時は結果を出力して Actor を停止
                        case e: Exception => failStop(e, rs.url)
                    }
                }
            }
        }
    }

    def stop() {
        exit
    }

    def failStop(e: Exception, url: URL) {
        printf("failed: %s, %s\n", url, e)
        exit
    }
}

val dir = args(0)

Source.stdin.getLines.toList.foreach {u =>
    val url = new URL(u)
    reset {
        val actor = new URLActor()
        //Actor を開始
        actor.start

        //URL接続処理
        val stream = shift {k: (InputStream => Unit) =>
            //URL接続処理の非同期呼び出し (1)
            actor ! URLOpen(url, k)
        }
        // (3)

        //ダウンロード処理
        val file = shift {k: (Path => Unit) =>
            //ダウンロード処理の非同期呼び出し (4)
            actor ! URLDownload(url, stream, dir, k)
        }
        // (6)

        printf("downloaded: %s => %s\n", url, file)
        //Actor を停止
        actor.stop
    }
}

F# の場合: 非同期ワークフロー

F# では非同期ワークフローという機能が用意されているので、比較的容易に非同期処理を同期的に実装できます。

非同期ワークフローでは、async ブロック内に非同期処理を実装、非同期処理の呼び出し箇所で let! や do! のような ! を付けた命令を使用し、あとは async ブロックを Async.RunSynchronously メソッド等に渡すだけで非同期処理を実装できます。

下記サンプルでは、Groovy や Scala のサンプルとは異なり、Web コンテンツの読み込みとローカルファイルへの書き込み処理もそれぞれ非同期処理しています。

また、Async.Parallel で並列化、Async.RunSynchronously で処理が全て完了するまで終了しないようにしており、ignore はビルド時にワーニングが出るのを防ぐために使っています。(Async.RunSynchronously の戻り値を無視するため)

async_download_web.fs
open System
open System.IO
open System.Net

[<EntryPoint>]
let main(args: string[]) = 
    let downloadFile (dir: string) (url: string) = 
        async {
            try
                let req = WebRequest.Create(url)
                //URL接続(非同期)
                let! res = req.AsyncGetResponse()

                let fileName = Path.Combine(dir, Path.GetFileName(url))

                use stream = res.GetResponseStream()
                use fs = new FileStream(fileName, FileMode.Create)

                //Webコンテンツの読み込み(非同期)
                let! buf = stream.AsyncRead(int res.ContentLength)
                //ローカルファイルへの書き込み(非同期)
                do! fs.AsyncWrite(buf, 0, buf.Length)

                stdout.WriteLine("downloaded: {0} => {1}", url, fileName)
            with
            | _ as e -> stdout.WriteLine("failed: {0}, {1}", url, e.Message)
        }

    stdin.ReadToEnd().Split([| Environment.NewLine |], StringSplitOptions.RemoveEmptyEntries)
        |> Array.map (downloadFile args.[0])
        |> Async.Parallel
        |> Async.RunSynchronously
        |> ignore

    0

*1:厳密には withPool に渡すクロージャ内

LINQやコレクションAPIを使ってCSVファイルからデータ抽出 - C#, F#, Scala, Groovy, Ruby の場合

id:fits:20110702 や id:fits:20110709 にて、SQL を使ってデータ抽出した処理を LINQ やコレクション API を使って実施し直してみました。(ただし、今回は station_g_cd でのソートを実施していない等、以前使った SQL と完全に同じではありません)

今回は C#, F#, Scala, Groovy, RubyJRuby)で実装していますが、どの言語も似たような API を用意しており、同じように実装できる事が分かると思います。

以前使った SQL
SELECT *
FROM (
    SELECT
        pref_name,
        station_g_cd,
        station_name,
        count(*) as lines
    FROM
      CSVREAD('m_station.csv') S
      JOIN CSVREAD('m_pref.csv') P ON S.pref_cd = P.pref_cd
    GROUP BY station_g_cd, station_name
    ORDER BY lines DESC, station_g_cd
)
WHERE ROWNUM <= 10

csv ファイルの内容は id:fits:20110702 を参照。
サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20110718/

C# の場合(LINQ)

C# 4.0 で LINQ を使って実装してみました。
実装内容は以下の通り。

  • File.ReadAllLines(・・・) で行単位のコレクションを取得
  • Skip でヘッダー行を無視
  • join で都道府県を結合
  • group by でグループ化
    • 匿名型を使ってグルーピング
    • 戻り値の型は IEnumerable>
  • order by でソート
  • Take を使って 10件取得

LINQ を使うと SQL 風に実装できます。

listup_station.cs
using System;
using System.Linq;
using System.IO;
using System.Text;

class ListUpStation
{
    public static void Main(string[] args)
    {
        //都道府県の取得
        var plines = File.ReadAllLines("m_pref.csv", Encoding.Default);
        var prefs = 
            from pline in plines.Skip(1)
                let p = pline.Split(',')
            select new {
                PrefCode = p[0],
                PrefName = p[1]
            };

        //路線数の多い駅の抽出
        var slines = File.ReadAllLines("m_station.csv", Encoding.Default);
        var list = (
            from sline in slines.Skip(1)
                let s = sline.Split(',')
            join p in prefs on s[10] equals p.PrefCode
            group s by new { 
                StationName = s[9],
                PrefName = p.PrefName, 
                StationGroupCode = s[5]
            } into stGroup
            orderby stGroup.Count() descending
            select stGroup
        ).Take(10);

        //結果出力
        foreach(var s in list)
        {
            Console.WriteLine("{0}駅 ({1}) : {2}", s.Key.StationName, s.Key.PrefName, s.Count());
        }
    }
}
実行結果
> csc listup_station.cs
> listup_station.exe
新宿駅 (東京都) : 12
横浜駅 (神奈川県) : 11
東京駅 (東京都) : 10
渋谷駅 (東京都) : 10
池袋駅 (東京都) : 9
大宮駅 (埼玉県) : 9
新橋駅 (東京都) : 7
大船駅 (神奈川県) : 7
上野駅 (東京都) : 7
千葉駅 (千葉県) : 7

F# の場合

F# 2.0.0 では LINQ を使わずコレクションを使って実装してみました。
実装内容は以下の通り。

  • File.ReadAllLines(・・・) で行単位のコレクションを取得
  • Seq.skip でヘッダー行を無視
  • 都道府県を Map.ofSeq で Map 化
  • Seq.groupBy でグループ化
    • レコード定義を使ってグルーピング
  • List.sortWith でソート
    • sortWith を使うために List.ofSeq を使って List 化
  • Seq.take を使って 10件取得

なお、グルーピングの際にレコード定義を使ってますが、Tuple を使っても特に問題ありません。

listup_station.fsx
open System
open System.IO
open System.Text

//レコード定義
type Station = {
    StationName: string
    PrefName: string
    StationGroupCode: string
}

//都道府県の取得
let prefMap = File.ReadAllLines("m_pref.csv", Encoding.Default)
                |> Seq.skip 1
                |> Seq.map (fun l -> 
                        let items = l.Split(',')
                        (items.[0], items.[1])
                    )
                |> Map.ofSeq

//路線数の多い駅の抽出
let lines = File.ReadAllLines("m_station.csv", Encoding.Default)
let list = lines 
            |> Seq.skip 1 
            |> Seq.map (fun l -> l.Split(',')) 
            |> Seq.groupBy (fun s -> 
                    {
                        StationName = s.[9]
                        PrefName = Map.find s.[10] prefMap
                        StationGroupCode = s.[5]
                    }
                ) 
            |> List.ofSeq 
            |> List.sortWith (fun a b -> Seq.length(snd b) - Seq.length(snd a)) 
            |> Seq.take 10

//結果出力
for s in list do
    let st = fst s
    stdout.WriteLine("{0}駅 ({1}) : {2}", st.StationName, st.PrefName, Seq.length((snd s)))
実行結果
> fsi listup_station.fsx
新宿駅 (東京都) : 12
横浜駅 (神奈川県) : 11
東京駅 (東京都) : 10
渋谷駅 (東京都) : 10
池袋駅 (東京都) : 9
大宮駅 (埼玉県) : 9
新橋駅 (東京都) : 7
大船駅 (神奈川県) : 7
上野駅 (東京都) : 7
千葉駅 (千葉県) : 7

Scala の場合

Scala 2.9.0.1 もコレクションで実装してみました。
実装内容は以下の通り。

  • Source.fromFile(・・・).getLines() で行単位のコレクションを取得
  • drop でヘッダー行を削除
  • 都道府県を toMap で Map 化
  • groupBy でグループ化
    • ケースクラスを使ってグルーピング
  • sortWith でソート
  • take を使って 10件取得

なお、グルーピングでケースクラスを使ってますが、F# と同様に Tuple を使っても問題ありません。

listup_station.scala
import scala.io.Source

case class Station(val stationName: String, val prefName: String, val stationGroupCode: String)

//都道府県の取得
val prefMap = Source.fromFile("m_pref.csv").getLines().drop(1).map {l =>
    val items = l.split(",")
    items(0) -> items(1)
}.toMap

//路線数の多い駅の抽出
val lines = Source.fromFile("m_station.csv").getLines()
val list = lines.drop(1).toList.map(_.split(",")).groupBy {s =>
    Station(s(9), prefMap.get(s(10)).get, s(5))
}.toList.sortWith {(a, b) => 
    a._2.length > b._2.length
} take 10

//結果出力
list.foreach {s =>
    printf("%s駅 (%s) : %d\n", s._1.stationName, s._1.prefName, s._2.length)
}
実行結果
> scala listup_station.scala
新宿駅 (東京都) : 12
横浜駅 (神奈川県) : 11
東京駅 (東京都) : 10
渋谷駅 (東京都) : 10
池袋駅 (東京都) : 9
大宮駅 (埼玉県) : 9
大船駅 (神奈川県) : 7
京都駅 (京都府) : 7
新橋駅 (東京都) : 7
千葉駅 (千葉県) : 7

Groovy の場合

Groovy 1.8.0 もコレクションで実装してみました。
実装内容は以下の通り。

  • new File(・・・).readLines() で行単位のコレクションを取得
  • tail でヘッダー行以外を取得
  • 都道府県を collectEntries で Map 化
  • groupBy でグループ化
    • 配列を使ってグルーピング
  • sort でソート
  • List の getAt(Range) を使って 10件取得(asList()[0..9] の箇所)
    • getAt(Range) を使うために entrySet() で取得した Set を asList() で List 化

なお、a <=> b は a.compareTo(b) と同じです。

listup_station.groovy
//都道府県の取得
def prefMap = new File("m_pref.csv").readLines() tail() collectEntries {
    def items = it.split(",")
    [items[0], items[1]]
}

//路線数の多い駅の抽出
def list = new File("m_station.csv").readLines() tail() collect {
    it.split(",")
} groupBy {
    [it[9], prefMap[it[10]], it[5]]
} sort {a, b -> 
    b.value.size <=> a.value.size
} entrySet() asList()[0..9]

//結果出力
list.each {
    println "${it.key[0]}駅 (${it.key[1]}) : ${it.value.size}"
}
実行結果
> groovy listup_station.groovy
新宿駅 (東京都) : 12
横浜駅 (神奈川県) : 11
東京駅 (東京都) : 10
渋谷駅 (東京都) : 10
池袋駅 (東京都) : 9
大宮駅 (埼玉県) : 9
新橋駅 (東京都) : 7
大船駅 (神奈川県) : 7
上野駅 (東京都) : 7
千葉駅 (千葉県) : 7

Ruby の場合

RubyJRuby 1.6.3)では csv モジュールを使わずに実装してみました。
実装内容は以下の通り。

  • IO.readlines(・・・) で行単位のコレクションを取得
  • drop でヘッダー行を削除
  • chop で改行文字を削除
  • 都道府県を Hash[] で Hash 化
  • group_by でグループ化
    • 配列を使ってグルーピング
  • sort でソート
  • take を使って 10件取得
listup_station.rb
#都道府県の取得
prefMap = Hash[IO.readlines("m_pref.csv").drop(1).map {|l| l.chop.split(',')}]

#路線数の多い駅の抽出
list = IO.readlines("m_station.csv").drop(1).map {|l|
    l.chop.split(',')
}.group_by {|s|
    [s[9], prefMap[s[10]], s[5]]
}.sort {|a, b|
    b[1].length <=> a[1].length
}.take 10

#結果出力
list.each do |s|
    puts "#{s[0][0]}駅 (#{s[0][1]}) : #{s[1].length}"
end
実行結果
> jruby listup_station.rb
新宿駅 (東京都) : 12
横浜駅 (神奈川県) : 11
渋谷駅 (東京都) : 10
東京駅 (東京都) : 10
大宮駅 (埼玉県) : 9
池袋駅 (東京都) : 9
新橋駅 (東京都) : 7
大船駅 (神奈川県) : 7
京都駅 (京都府) : 7
岡山駅 (岡山県) : 7

Windows Azure 上で F# の Web ロールを実行

Windows Azure 上で単純な F# の Web ロール用アプリを実行してみました。

id:fits:20100906 の F# による ASP.NET のサンプルをそのまま使ってみるつもりだったのですが、Compute Emulator でのページ表示時に以下のようなエラーが発生し、解決策が見つからなかったので、今回は動的コンパイルを諦めて F# のソース部分を事前にビルドする方法をとりました。(FSharp.Core.dll と同じ場所に FSharp.Core.sigdata 等を配置してみても駄目でした)

コンパイル エラー メッセージ: 0: error FS1221: FSharp.Core.sigdata not found alongside FSharp.Core

今回使用した環境は以下の通りです。

また、今のところ Windows Azure では .NET Framework 2.0 がデフォルトで使われるようなので、.NET Framework 4.0 を使うには以下のようなファイルが必要になる点に注意が必要です。

properties.txt (.NET Framework のバージョンを指定する設定ファイル)
TargetFrameWorkVersion=v4.0

サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20110417/

アプリケーション作成

まず、F# でページのイベント処理を定義します。

DefaultPage.fs (ページのイベント処理)
namespace Fits.Sample

open System
open System.Web
open System.Web.UI
open System.Web.UI.WebControls

type DefaultPage() =
    inherit Page()

    [<DefaultValue>] val mutable InfoText : TextBox
    [<DefaultValue>] val mutable InfoButton : Button
    [<DefaultValue>] val mutable InfoLabel : Label

    //ページロード時の処理
    member this.Page_Load(sender : obj, e : EventArgs) =
        this.InfoLabel.Text <- "hello"

    //ボタンクリック時の処理
    member this.InfoButton_Click(sender : obj, e : EventArgs) = 
        this.InfoLabel.Text <- "入力: " + this.InfoText.Text

次は、ASP.NET ページです。F# の動的コンパイルは使わない(使えなかった)ので Language を C# にし、F# で定義した Fits.Sample.DefaultPage を Inherits に指定しています。

Default.aspx (ページの内容)
<%@ Page Language="C#" AutoEventWireup="true" Inherits="Fits.Sample.DefaultPage"%>
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>F# Sample</title>
</head>
<body>
  <form runat="server">
    <div>
      <asp:TextBox runat="server" id="InfoText" />
      <asp:Button runat="server" id="InfoButton" text="Button" onClick="InfoButton_Click" />
    </div>
    <div>
      <asp:Label runat="server" id="InfoLabel" />
    </div>
  </form>
</body>
</html>

あとは、クラウドアプリケーション用の設定ファイルを作成します。

サービス定義ファイルは以下のようにしました。ポート番号は 8080 を使うように設定しています。

ServiceDefinition.csdef(サービス定義ファイル)
<?xml version="1.0" encoding="utf-8"?>
<ServiceDefinition name="Fits Sample F# ASP.net" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition">
  <WebRole name="WebRole1">
    <Endpoints>
      <InputEndpoint name="HttpIn" protocol="http" port="8080" />
    </Endpoints>
  </WebRole>
</ServiceDefinition>

ちなみに、上記のように 要素が未定義だと cspack の実行時に Warning CloudServices078 : ・・・ と表示されますが、今回は無視しています。

サービス設定ファイルは以下の通りです。Web ロールのインスタンス数を 1 にしています。

ServiceConfiguration.cscfg (サービス設定ファイル)
<?xml version="1.0" encoding="utf-8"?>
<ServiceConfiguration serviceName="Fits Sample F# ASP.net" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration" osFamily="1" osVersion="*">
  <Role name="WebRole1">
    <Instances count="1" />
  </Role>
</ServiceConfiguration>

最後に、.NET Framework 4 を使用するためのファイルも作成しておきます。

properties.txt(設定ファイル)
TargetFrameWorkVersion=v4.0

なお、今回は F# の動的コンパイルを使わないので、Web.config は特に設定する必要ありません。

ビルド

DefaultPage.fs をビルドして bin ディレクトリに配置します。(.NET 4 用の fsc.exe を使う点に注意)

DefaultPage.fs のビルド
> fsc DefaultPage.fs /target:library /out:bin\Fits.Sample.dll

bin ディレクトリに F# 2.0 November 2010 Community Technology Preview 内の v4.0\bin\gac\FSharp.Core.dll ファイルをコピーします。(必要に応じて他のアセンブリも bin ディレクトリに配置します)

ファイル構成は以下のようになります。

ファイル構成
  • fsharp_aspnet
    • bin
      • Fits.Sample.dll
      • FSharp.Core.dll
    • Default.aspx
    • properties.txt
    • ServiceConfiguration.cscfg
    • ServiceDefinition.csdef
    • Web.config

なお、サービス定義ファイル(ServiceDefinition.csdef)やサービス設定ファイル(ServiceConfiguration.cscfg)を別のディレクトリに配置してあっても特に問題ありません。

Compute Emulator 上での実行

それでは、ローカル環境の Compute Emulator 上で実行してみます。

まず、cspack.exe を使って Compute Emulator 用にパッケージングします。
/role でロールの指定、/rolePropertiesFile で設定ファイル(properties.txt)の指定を行います。

  • /role:ロール名;ディレクトリ
  • /rolePropertiesFile:ロール名;ファイル

/copyonly を指定する事で zip 化されずに、パッケージング用のディレクトリ・ファイル構成がそのまま残ります。

なお、cspack はアプリ用に作成したディレクトリの上位ディレクトリで実行します。

Compute Emulator 用のパッケージング実行
> cd ..
> cspack fsharp_aspnet\ServiceDefinition.csdef /role:WebRole1;fsharp_aspnet /out:output /rolePropertiesFile:WebRole1;fsharp_aspnet\properties.txt /copyonly

Warning  CloudServices078 : The web role 'WebRole1' is configured using
 a legacy syntax that specifies that it runs in Hostable Web Core. 
・・・

Warning が表示されますが、今回は無視します。

次に、csrun.exe を使って Compute Emulator 上でアプリを実行します。

Compute Emulator 上での実行
> csrun /run:output;fsharp_aspnet\ServiceConfiguration.cscfg

Windows(R) Azure(TM) Desktop Execution Tool version 1.4.0.0
for Microsoft(R) .NET Framework 3.5
Copyright (c) Microsoft Corporation. All rights reserved.

Using session id 1
Created: deployment(18)
Started: deployment(18)
Deployment input endpoint HttpIn of role WebRole1 at http://127.0.0.1:8080/.

http://127.0.0.1:8080/ にアクセスすればページが表示されます。

クラウド上での実行

それでは、クラウド環境用にパッケージングします。
Compute Emulator の時との違いは /copyonly をつけずに、/out で出力ファイルを指定する点です。

クラウド用パッケージング
> cspack fsharp_aspnet\ServiceDefinition.csdef /role:WebRole1;fsharp_aspnet /out:fsharp_aspnet.cspkg /rolePropertiesFile:WebRole1;fsharp_aspnet\properties.txt

Windows Azure 上で新規の Hosted Service を作成し、上記で作成されたパッケージファイルとサービス設定ファイル(ServiceConfiguration.cscfg)をアップロードします。

アップロードが完了し Web ロールのステータスが Ready になった後、http://xxxxx.cloudapp.net:8080/ にアクセスすればページが表示されます。(ステージング環境の場合、xxxxx には Azure で設定された Guid を指定します)

なお、Web ロールのステータスが Initializing(初期化中)から Ready に変わるまで、思ったよりもずっと時間がかかるのでご注意ください。

F# でパーサーコンビネータを使った CSV ファイルのパース処理 - FParsec 使用

前回(id:fits:20101226)実施したパーサーコンビネータによる CSV ファイルのパース処理を FParsec を使って F# でやってみました。

環境は以下の通り。

サンプルのソースは http://github.com/fits/try_samples/tree/master/blog/20101231/

事前準備 - FParsec のビルド

https://bitbucket.org/fparsec/main/downloads/ から FParsec のソースコードをダウンロードした後、適当なディレクトリに解凍しビルドを行います。

ビルド
> cd fparsec\Build\VS10
> msbuild FParsec.fsproj

bin\Debug ディレクトリに作成された FParsec.dll と FParsecCS.dll ファイルをサンプルを作成するディレクトリにコピーしておきます。

CSVファイルのパース

Haskell で書いたサンプルとほぼ同じ実装でよかったので、いきなり以下の CSV ファイルをパースしてみる事にします。

test.csv
1,テスト1,"改行
含み"
2,test2,"カンマ,含み"
3,てすと3,"ダブルクォーテーション""含み"

Haskell の Parsec との違いは以下のような点です。

  • try の代わりに attempt を使う
  • char の代わりに pchar を使う
  • string の代わりに pstring を使う
  • return の代わりに preturn を使う
  • many の代わりに manyChars を使う
  • endBy の代わりに sepEndBy を使う

また、Scala の ~> 等の代わりに以下が使えました。

  • ~> の代わりに >>. を使う
  • <~ の代わりに .>> を使う

ちなみに、newline は FParsec.CharParsers で定義されているので、自前で定義する必要はありません。

parse_csv.fs
open System
open FParsec.Primitives
open FParsec.CharParsers

let quotedChar = noneOf "\"" <|> attempt (pstring "\"\"" >>. preturn '"')
let quotedCell = pchar '"' >>. manyChars quotedChar .>> pchar '"'
let cell = quotedCell <|> manyChars (noneOf ",\n")
let line = sepBy cell (pchar ',')
let csvFile = sepEndBy line newline

let cs = Console.In.ReadToEnd()
let res = run csvFile cs

match res with
| Success (v, _, _) -> Console.WriteLine(v)
| Failure (msg, _, _) -> Console.WriteLine(msg)
ビルド・実行結果
> fsc /r:FParsec.dll parse_csv.fs
> parse_csv.exe < test.csv
[[1; テスト1; 改行
含み]; [2; test2; カンマ,含み]; [3; てすと3; ダブルクォーテーション"含み]; ... ]

Groovy, Scala, F#, Haskell による関数・クロージャの合成

Groovy 1.8 のクロージャ合成の機能を試したついでに、Scala, F#, Haskell での関数合成の機能も簡単にまとめてみました。

サンプルのソースコードhttp://github.com/fits/try_samples/tree/master/blog/20101213/

Groovy の場合

Groovy では >> や << を使います。(1.8 から導入された Closure composition の機能)

  • Groovy 1.8.0 beta2
compose_sample.groovy
def plus = {x -> x + 3}
def times = {x -> x * 2}

def f = plus >> times
def g = plus << times

// times(plus(4)) = 14
println f(4)
// plus(times(4)) = 11
println g(4)

Scala の場合

Scala では andThen や compose 等を使います。

compose_sample.scala
val plus = (x: Int) => x + 3
val times = (x: Int) => x * 2

val f = plus andThen times
val g = plus compose times

// times(plus(4)) = 14
println(f(4))
// plus(times(4)) = 11
println(g(4))

F# の場合

F# では Groovy と同様に >> や << を使います。

  • F# 2.0.0
compose_sample.fs
let plus x = x + 3
let times x = x * 2

let f = plus >> times
let g = plus << times

// times(plus(4)) = 14
printfn "%i" (f 4)
// plus(times(4)) = 11
printfn "%i" (g 4)

Haskell の場合

Haskell では . を使います。

  • HaskellPlatform 2010.2.0.0 (GHC 6.10.4)
plus x = x + 3
times x = x * 2

f = times . plus
g = plus . times

main = do
    -- times(plus(4)) = 14
    putStrLn $ show $ f 4
    -- plus(times(4)) = 11
    putStrLn $ show $ g 4