ScalaでもリフレクションでToStringしたい

はじめに

好きなプリンタメーカーはCanon どうも弊社です

特に需要は無いのですが、とりあえずScalaを始めたのでリフレクションでToStringしてみました。 弊社にとってリフレクションでToStringするのはもはや挨拶のようなものです。

環境

Scalaのバージョンは2.12.3です。

真のジェネリック

当たり前かどうかは分かりませんが、JVMの型パラメータはコンパイル時に消去されてしまい実行時に取れません。 この辺の問題は『真のジェネリック問題』として激しく議論されているようですが、流れ弾はいやなのでとりあえずスルーします。

当然JVMの民*1であるScalaもこの制約を受けます。その為、CLRの民であるVBのように

Sub Hoge(Of T)(obj As T)
    Dim t As Type = GetType(T)
End Sub

のようにGetType(T)で一発で型情報を取得してアビャーするわけにはいかないようです。

そこで、Scalaはコンテキストバインドという形で型に関する情報をコンパイル時に必要なメソッド等に引き渡すコードを生成し、そこで型情報をやり取りするようです。

仮に

def reflectToString[T: TypeTag : ClassTag](value: T): Unit

のようなシグネチャを持つメソッドは以下のようにコンパイルされます。

> javap -private .\Hello.class

Compiled from "Hello.scala"
public final class example.Hello {
  public static <T> void reflectToString(T, scala.reflect.api.TypeTags$TypeTag<T>, scala.reflect.ClassTag<T>);
  public static void main(java.lang.String[]);
}

本来の引数の後ろに型情報に関するパラメータを追加し、コンパイラコンパイル時に型情報を暗黙的に追加するようです。

package example

import scala.reflect._
import scala.reflect.runtime.universe._

class Hoge(val name: String)

class Person(val name: String, val age: Int)

object Hello {
  def main(args: Array[String]): Unit = {
    val p = new Person("Hoge", 10)
    reflectToString(p)
    println()
    val h = new Hoge("HogeHoge")
    reflectToString(h)
  }

  def reflectToString[T: TypeTag : ClassTag](value: T): Unit = {
    val fs: Iterable[TermSymbol] = typeOf[T]
      .decls
      .filter(_.isTerm)
      .filter(!_.isMethod)
      .map(_.asTerm)
    val rm: Mirror = typeTag[T].mirror
    val im: InstanceMirror = rm.reflect(value)

    for (it <- fs) {
      val fm: FieldMirror = im.reflectField(it)
      println(s"${it.name}= ${fm.get}")
    }
  }
}
name = Hoge
age = 10

name = HogeHoge

TypeTagとってTermSymbolとってRuntimeMirrorとってInstanceMirrorとってFieldMirrorとってgetするのは.NETのリフレクションに慣れた身からするとなんか複雑に感じますが、まぁしゃーない。

おわり

*1:Scala.jsというのもあるようですが

VB.NETでもパーサコンビネータを実装したい(その2)

はじめに

得意なことはC#ライブラリをVBに移植する事と、.NET4.5ライブラリを.NET4.0に移植する事。どうも弊社です

Dictionary(Of TKey, TValue)IReadOnlyDictionary(Of TKey, TValue)を実装するようになった系のコレクションのインターフェース実装の追加が地味にキツいです。 インターフェースの後刺しが出来ない以上、クラスを丸ごと置き換えないといけないのでいろいろアレがアレでマジファッキンって感じです。*1

とまぁ、別にこの件に関係無いのですがSpracheの一部を移植する形でパーサコンビネータを実装したと言うかしているのでそれについてです。

github.com

前回のはほとんどおもちゃですが、今回は違います。

燻製ニシンの虚偽

例によってあまり真っ当な動機でないのですが、以下の要件を満たすように実装しています。

で、使い方はとっても簡単。ParserCombinator.vbをコピーして適当な所に張り付けてあとは適当にビャーって書けば弊社的にはOKです。

Imports RedHerrings

Module Module1

    Public Property Num As Parser(Of Integer) =
        Parse.Regex("[0-9]+").Select(Function(it) Integer.Parse(it))

    Public Property AddExpr As Parser(Of Integer) =
        From a In Num
        From add In Parse.PString("+")
        From b In Num
        Select a + b

    Sub Main()
        Console.WriteLine(AddExpr.Parse("1+2"))
    End Sub

End Module

おわりに

ソースコードをそのまま放り込むタイプなので、足りない機能があればその場で追加できるというメガシンカにも匹敵するすごい事が出来るので、出来れば皆さんには真っ当なパーサコンビネータライブラリを使用していただけたらと思います。

ふざけたお話はここまでで、確かに弊社の必要に迫られて実装した面もありますが*2、何かとdisられがちなVBでなんらかの実績を作りたかったというのがあります。 (将来は分かりませんが)現状はVBC#の言語仕様を遅れてはいますが追従してますし、VBでもこのようなライブラリを書けるというのを示したかったという感じです。

おわり

*1:一番ファッキンなのは時代錯誤なフレームワークと開発環境を使わせる[削除済]

*2:だからあんなニッチな要件を設定している

プロトタイピングとしての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はマイクロソフト登録商標なので