プロトタイピングとしてのF#

はじめに

いきなりですが、皆さんはプログラムを書くときにどのように書いていますか?

弊社は定型的なコードはともかく、まずは簡単なプロトタイプを書いて考えをまとめてからプロダクトコードを書いています。

いままでは適当なコンソールアプリケーションをVBなりC#で作ってそこでプロトタイプを書いていましたが、そういうプロトタイピングにF#ってすごく向いているんじゃね?と。 というわけでVBが猛威を振るっている環境でプロダクトコードとしてのF#の導入はもはや絶望的以外の何物でもありませんが、プロトタイピングとしてのF#の布教の為にこの記事を書いてみました。

例題

ここで例を示しながらF#でプロトタイプを書いてみましょう。

あなたは帳票作成担当のプログラマです。 帳票には相手先の会社名を印字しなくてはなりません。 しかし、相手先の会社名を印刷するスペースは横に20文字分しかありません。

ほとんどの会社名は20文字以内に収まりますが、ごく一部の会社名は20文字を超えます。 そこで、会社名を印字する領域を2行にしたうえで以下の規則で改行することにしました。*1

  • 20文字以下の場合は、そのまま1行目に印刷
  • 40文字以上の場合は、20文字×2行を表示し残りは切り捨て
  • 21文字以上40文字未満の場合は以下の規則に従う
    1. かならず2行の中に会社名をすべて収める
    2. 可能な限り『株式会社』の文字列は分断しない(優先1)
    3. 可能な限り「っぁぃぅぇぉッァィゥェォー」を2行目の先頭に持ってこない(優先2)
    4. 可能な限り「・」、「&」「‐(ハイフン)」で区切り、これらの文字を2行目の先頭に持ってくる(優先3)
    5. 可能な限り1行目を2行目よりも長くする(優先4)

ただし、会社名は法務省告示に従うものとする。

妙に生々しいですが、特に例題としての意味以外はありません。

とりあえず、各文字数のパターンに応じて処理を分岐するコードを書いてみましょう。

let (|Normal|Long|TooLong|) (name:string) =
    if name.Length <= 20 then
        Normal
    elif name.Length < 40 then
        Long
    else
        TooLong
 
let splitCompanyName (name:string) : (string * string option) =
    match name with
    | Normal -> failwith "not implemented"
    | Long -> failwith "not implemented"
    | TooLong -> failwith "not implemented"

上二つは簡単そうなので、サクッと実装します。

let normalCompanyNameLength (name:string) =
    (name, None)

let tooLongCompanyNameLength (name:string) =
    (name.Substring(0, 20), Some(name.Substring(20, 20)))

printfn "%A" (normalCompanyNameLength "株式会社ユカリ・テレグラフ")
printfn "%A" (tooLongCompanyNameLength "株式会社寿限無寿限無五劫の擦り切れ海砂利水魚の水行末雲来末風来末食う寝る処に住む処藪ら柑子の藪柑子")
("株式会社ユカリ・テレグラフ", null)
("株式会社寿限無寿限無五劫の擦り切れ海砂利", Some "水魚の水行末雲来末風来末食う寝る処に住む")

しっかりとしたプロダクトコードなら境界のテストを書いたりをするのですが、まぁここはプロトタイプなのでいいでしょうと。

さて、問題の二番目ですが、ここではすべての組み合わせから最も良さげのを選択する戦略で行ってみようと思います。 1.を満たしたすべての組み合わせをリストアップする関数を書いてみましょう。

let listupCandidate (name:string) =
    [1..name.Length-1]
    |> List.map (fun it -> (name, name.Substring(0, it), name.Substring(it, name.Length - it)))
    |> List.filter (fun (_, x, y) -> x.Length <= 20 && y.Length <= 20)

あとは満たすべき条件の優先度が上のものほどスコアを多く与えつつ条件を満たしているかどうか判定する関数を書きましょう。

まずは『株式会社』を分断していないか確認してみましょう。

let splitKK (fullname:string, x:string, y:string) =
    if fullname.Contains("株式会社") then
        if x.Contains("株式会社") || y.Contains("株式会社") then 8 else 0
    else
        8

printfn "%A" (splitKK("株式会社あいうえお・かきくけこ&さしすせそ", "株", "式会社あいうえお・かきくけこ&さしすせそ"))
printfn "%A" (splitKK("株式会社あいうえお・かきくけこ&さしすせそ", "株式会社あいうえお", "・かきくけこ&さしすせそ"))
0
8

次に禁則文字が先頭に来ていないか確認しましょう。

let kinsoku (fullname:string, x:string, y:string) =
    let kin = ["っ";"ぁ";"ぃ";"ぅ";"ぇ";"ぉ";"ッ";"ァ";"ィ";"ゥ";"ェ";"ォ";"ー"]
    if kin |> List.exists (fun it -> y.StartsWith(it)) then 0 else 4
0
4
let kugiri (fullname:string, x:string, y:string) =
    let ku = ["・";"&";"‐";]
    if ku |> List.exists (fun it -> y.StartsWith(it)) then 2 else 0

printfn "%A" (kugiri("株式会社あいうえお・かきくけこ&さしすせそ", "株式会社あいうえお・か", "きくけこ&さしすせそ"))
printfn "%A" (kugiri("株式会社あいうえお・かきくけこ&さしすせそ", "株式会社あいうえお", "・かきくけこ&さしすせそ"))
0
2
let nagasa (fullname:string, x:string, y:string) =
    if x.Length > y.Length then 1 else 0
    
printfn "%A" (nagasa("株式会社あいうえお・かきくけこ&さしすせそ", "株式会社あいうえお", "・かきくけこ&さしすせそ"))
printfn "%A" (nagasa("株式会社あいうえお・かきくけこ&さしすせそ", "株式会社あいうえお・か", "きくけこ&さしすせそ"))
0
1

あとはこいつらをまとめ上げてスコアが高いものを選択するようにすればOKです。

let (|Normal|Long|TooLong|) (name:string) =
    if name.Length <= 20 then
        Normal
    elif name.Length <= 40 then
        Long
    else
        TooLong
 
let normalCompanyNameLength (name:string) =
    (name, None)

let tooLongCompanyNameLength (name:string) =
    (name.Substring(0, 20), Some(name.Substring(20, 20)))

let listupCandidate (name:string) =
    [1..name.Length-1]
    |> List.map (fun it -> (name, name.Substring(0, it), name.Substring(it, name.Length - it)))
    |> List.filter (fun (_, x, y) -> x.Length <= 20 && y.Length <= 20)

let splitKK (fullname:string, x:string, y:string) =
    if fullname.Contains("株式会社") then
        if x.Contains("株式会社") || y.Contains("株式会社") then 8 else 0
    else
        8

let kinsoku (fullname:string, x:string, y:string) =
    let kin = ["っ";"ぁ";"ぃ";"ぅ";"ぇ";"ぉ";"ッ";"ァ";"ィ";"ゥ";"ェ";"ォ";"ー"]
    if kin |> List.exists (fun it -> y.StartsWith(it)) then 0 else 4
    
let kugiri (fullname:string, x:string, y:string) =
    let ku = ["・";"&";"‐";]
    if ku |> List.exists (fun it -> y.StartsWith(it)) then 2 else 0
    
let nagasa (fullname:string, x:string, y:string) =
    if x.Length > y.Length then 1 else 0 
 
let calcScore (fullname:string, x:string, y:string) =
    let score = [ splitKK; kinsoku; kugiri; nagasa; ]
                |> List.map (fun it -> it (fullname, x, y))
                |> List.sum
    (fullname, x, y, score)
 
let longCompanyNameLength (name:string) =
    let score = [splitKK;kinsoku;kugiri;nagasa]
    listupCandidate name
    |> List.map (fun it -> calcScore it)
    |> List.sortBy (fun (_, _, _, score) -> score)
    |> List.map (fun (_, x, y, _) -> (x, Some(y)))
    |> List.rev
    |> List.head

let splitCompanyName (name:string) : (string * string option) =
    match name with
    | Normal -> normalCompanyNameLength name
    | Long -> longCompanyNameLength name
    | TooLong -> tooLongCompanyNameLength name

let c = [
    "株式会社ユカリ・テレグラフ"
    "株式会社あいうえお・かきくけこ&さしすせそ"
    "株式会社寿限無寿限無五劫の擦り切れ海砂利水魚の水行末雲来末風来末食う寝る処に住む処藪ら柑子の藪柑子"]

for it in c do
    printfn "%A" (splitCompanyName it)
("株式会社ユカリ・テレグラフ", null)
("株式会社あいうえお・かきくけこ", Some "&さしすせそ")
("株式会社寿限無寿限無五劫の擦り切れ海砂利", Some "水魚の水行末雲来末風来末食う寝る処に住む")

まぁ、よさげじゃないですか?細かくはテストしていませんが。

おわりに

細かい話はおいておいて、とりあえずこのコンセプトで行けそうですね。 ってところまでは出来たので、あとはお好きなVBでテストを書きつつ実装すればOKっぽいです。

とまぁ、月並みな感想ですがF#はプログラムを関数としてパーツで書いていってあとでまとめることが出来るので、試しで書いてみるときには結構便利です。*2 特にLINQPadと組み合わせると『書く』→『実行』が流れるようにできるのでサクサクと試せてすごい便利です。

ちなみにLINQPadは無償版はIntelliSenseもどき*3が効かないので、「足らぬ足らぬは工夫が足らぬ」精神で頑張るか、素直にライセンスを買うのがいいと思います。

おわり

*1:これはあくまでここでの例題での規則であって、別に弊社は各種法令やビジネスマナーについて論じたい訳ではないです。

*2:VBでもできなくはないけど、ラムダ式の構文が鬼畜

*3:IntelliSenseはマイクロソフト登録商標なので

VB.NETでもコマンドライン引数を楽にパースしたい(またの名をVB.NETとF#で連携したい)

はじめに

好きなイヤホンはBOSEQuietComfort 20 どうも弊社です。

前になんかいい感じのコマンドライン引数をパースするライブラリを見つけたいとか書いたのですが、DSL付きで見つけました。

なんとこちら、DSLなのに.NETの型システムと高い親和性を持っていたりマイクロソフトがサポートを改めて表明したりと至れり尽くせりなんです。F#って言うんですって

VB.NETとFSharp

とまぁ、やっぱりF#はこういう方面はめっぽう強いから好きです。

namespace CommandLine.Parser

open System

type Param = { Level:int; IsDryRun:bool; OutputName:string option; Files:string list }

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module Param = 
    
    let rec parseImpl input param =
            match input with
            | "--level" :: x :: tail ->
                match Int32.TryParse(x) with
                | (true, x) when 1 <= x && x <= 5 -> parseImpl tail { param with Level = x }
                | _ -> parseImpl tail param
            | "--dry-run" :: tail -> 
                parseImpl tail { param with IsDryRun = true }
            | "--output-name" :: name :: tail ->
                parseImpl tail { param with OutputName = Some(name) }
            | head :: tail ->
                parseImpl tail { param with Files = head :: param.Files }
            | [] -> param

    [<CompiledName("Parse")>]
    let parse input =
        parseImpl (Array.toList input) { Level = 1; IsDryRun = false; OutputName = None; Files = [] } 

    [<CompiledName("Show")>]
    let show x = printfn "%A" x

これだけのコードで割と複雑なコマンドライン引数をパースできます。

Imports CommandLine.Parser

Module Program

    Sub Main()
        Dim param1 = ParamModule.Parse({"--level", "2", "--dry-run", "c.txt"})
        ParamModule.Show(param1)

        Console.WriteLine()

        Dim param2 = ParamModule.Parse({"--level", "2", "--output-name", "hoge.exe", "a.txt", "b.txt"})
        ParamModule.Show(param2)
    End Sub

End Module
{Level = 2;
 IsDryRun = true;
 OutputName = None;
 Files = ["c.txt"];}

{Level = 2;
 IsDryRun = false;
 OutputName = Some "hoge.exe";
 Files = ["b.txt"; "a.txt"];}

言語のちゃんぽんはあまりオススメ出来ませんが、異なるパラダイムをごった煮出来るのが.NET Frameworkの良さ*1なのだと思います。

おわりに

まぁ、例のごとくgithubから見つけてきた手法の紹介でアレなのですが、githubは宝の山ですね。

このライブラリはどうやってこの機能を実現しているのだろうと思ったときに調べればすぐに出てきますし、すごい人はやっぱり凄いなと感心するばかりです。

あと、本題とは違う形での紹介で申し訳ないのですが、このPersimmonと言うユニットテストフレームも結構いい感じなので、興味があろうとなかろうと一回使ってみるといいかもしれません。

github.com

おわり

*1:この辺はJavaScala・Kotlinが先を行っている感がありますが

VB.NETでもクラスとモジュールの違いを知りたい

はじめに

好きな沿海域戦闘艦はフリーダム級沿海域戦闘艦 どうも弊社です。 計画倒れとか言われても、47ノットで洋上を爆走する姿はかっこいいと思います。

皆さんは新人くんの

VBのクラスとモジュールの違いは何ですか?

の質問にどのように答えているでしょうか。

こんな時の対応は大体以下の感じになると思います。

  1. よくわからないから「ググれ」で済ませて軽蔑される
  2. 機能面での話をさらっとして尊敬される
  3. 機能面での話をすっとばして内部実装の細かい話をしてドン引きされる
  4. 知りたいか、教えてやる。金を払え。

正直、2.の機能面の話は死ぬほどいろんなところで書かれているはず*1なので、ここでは3.の方面でまとめてみようと思います。

仕様上のモジュール

過去の記事でVBの仕様書が見つからないと言ったな。アレは嘘だ。

というわけでVisual Studio 2017をインストールするとx64環境だと

C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VB\Specifications\1041

あたりに付いてくるっぽいです。しかも日本語で

今回の件から弊社が得る教訓は"SDKをインストールした際についてくるドキュメントも存外馬鹿にはできない"ということだ。

さて、仕様書からモジュールについて引用すると

“標準モジュール” は、メンバーが暗黙的にSharedである型です。標準モジュールのスコープは、標準モジュール宣言自体だけではなく、標準モジュールが含まれる名前空間の宣言空間まで及びます。標準モジュールはインスタンス化できません。標準モジュール型の変数を宣言すると、エラーになります。

標準モジュールのメンバーには 2 つの完全修飾名があります。1 つは標準モジュール名を持たず、もう 1 つは標準モジュール名を持ちます。名前空間内の複数の標準モジュールで、特定の名前の 1 つのメンバーを定義できます。そのため、いずれかのモジュールの外部でその名前を修飾せずに参照すると、あいまいになります。

~中略~

モジュールは名前空間内でのみ宣言でき、別の型の中で入れ子にすることはできません。標準モジュールはインターフェイスを実装できません。標準モジュールは暗黙的にObjectから派生し、Sharedコンストラクターだけを持ちます。

標準モジュールのメンバーは、メンバー宣言によって導入されたメンバーと、Objectから継承されたメンバーです。標準モジュールは、インスタンス コンストラクター以外の任意の型のメンバーを持つことができます。標準モジュール型のすべてのメンバーは、暗黙的にSharedになります。

通常、標準モジュールのメンバー宣言に指定できるのは、PublicFriend、またはPrivateのアクセスだけです。ただし、Objectを継承するメンバーをオーバーライドする場合は、Protectedおよび Protected Friendのアクセス修飾子も指定できます。標準モジュールのメンバー宣言にアクセス修飾子が含まれない場合は、既定でPublicアクセスが宣言されます。ただし、変数の場合、既定はPrivateアクセスです。

前述のとおり、標準モジュール メンバーのスコープは、標準モジュール宣言が含まれる宣言です。Objectを継承するメンバーはこの特殊なスコープに含まれません。これらのメンバーにはスコープがなく、常にモジュールの名前で修飾する必要があります。メンバーにFriendアクセスが指定されている場合、そのスコープは同じプログラムまたはFriendアクセスが指定されたアセンブリ内で宣言された名前空間メンバーだけに及びます。

“拡張メソッド” を使用すると、型宣言の外部からメソッドを型に追加できます。拡張メソッドとは、System.Runtime.CompilerServices.ExtensionAttribute属性が適用されているメソッドのことです。拡張メソッドは、標準モジュール内でだけ宣言でき、メソッドによって拡張される型を指定する 1 つ以上のパラメーターを持つ必要があります。

Microsoft®Visual Basic®言語仕様 - © 2012 Microsoft Corporation.All Rights Reserved.

だそうです。

実用上で注意しないといけないのは

  • モジュール内のメンバーはすべてSharedC#でいうstatic)になる
  • モジュール型の変数は宣言できない
  • 名前空間内でのみ宣言でき、入れ子で宣言できない
  • モジュールのメンバー宣言に指定できるのは、PublicFriendC#internal)、またはPrivate
  • 拡張メソッドはモジュールにしか宣言できない
  • 名前空間内で一意なメンバー名であればモジュール名を省略できる

といったところです。

ここまで説明出来て100点です。

モジュールの内部実装

表面上の使い方で満足しないのが我々エクストリームVBerです。

単純なコードのILを確認してみましょう。

なお、ここでは以下の環境で検証しています。

Public Module Hello
    Public Sub Hoge()
        Console.WriteLine("Hello")
    End Sub
End Module
.class public sealed auto ansi 
  ConsoleApp1.Hello
    extends [mscorlib]System.Object
{
  .custom instance void [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.StandardModuleAttribute::.ctor() 
    = (01 00 00 00 )

  .method public static void 
    Hoge() cil managed 
  {
    .maxstack 8
    IL_0000: nop          
    IL_0001: ldstr        "Hello"
    IL_0006: call         void [mscorlib]System.Console::WriteLine(string)
    IL_000b: nop          
    IL_000c: ret          
  }
}

つまり、以下のコードとほぼ等価なことが分かります。

<Microsoft.VisualBasic.CompilerServices.StandardModule>
Public NotInheritable Class World
    Private Sub New()
        ' Nothing to do.
    End Sub

    Public Shared Sub Hoge()
        Console.WriteLine("World")
    End Sub
End Class
.class public sealed auto ansi 
  ConsoleApp1.World
    extends [mscorlib]System.Object
{
  .custom instance void [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.StandardModuleAttribute::.ctor() 
    = (01 00 00 00 )

  .method private specialname rtspecialname instance void 
    .ctor() cil managed 
  {
    .maxstack 8
    IL_0000: nop          
    IL_0001: ldarg.0      // this
    IL_0002: call         instance void [mscorlib]System.Object::.ctor()
    IL_0007: nop          
    IL_0008: ret          
  }

  .method public static void 
    Hoge() cil managed 
  {
    .maxstack 8
    IL_0000: nop          
    IL_0001: ldstr        "World"
    IL_0006: call         void [mscorlib]System.Console::WriteLine(string)
    IL_000b: nop
    IL_000c: ret          
  }
}

デフォルトコンストラクタはさすがに封殺できないので、プライベートコンストラクタを明示的に宣言することでデフォルトコンストラクタを消し去っています。

StandardModuleAttributeがキモのようで、VBコンパイラはこの属性が付与されたクラスをモジュールとして認識しているようです。 そのため、C#のクラスにStandardModuleAttributeを付与するだけでVBから利用できないクラスを爆誕させることが出来ます。

f:id:jyuch:20170603001750p:plain

f:id:jyuch:20170603001808p:plain

余談ですが、同一ソリューション内であればVBコンパイラStandardModuleAttributeが付与されたクラスとモジュールを区別できるようです。 これがロスリンの力か・・・

おわりに

個人的な話でアレですが、弊社としては拡張メソッドを定義する以外ではモジュールは使いたくないですね。 オブジェクト指向に反するというのも若干ありますが、別に弊社はオブジェクト指向原理主義ではないのでそれについてはあまり気にしません。 やはり名前空間の汚染は正直気持ちのいいものではありませんし、レガシーVBの作法を持ち込まれてもねぇ・・・といった感じです。*2

まぁ、いずれにしろ最終的に金になればそれでいいのです

この記事を読んだな。金を払え。

*1:調べてはいない

*2:そもそもで言えば、VB.NETという言語自体がレガシーVBの作法を.NET Frameworkに持ち込むための言語なのですが