ようこそ。睡眠不足なプログラマのチラ裏です。

printf系の "%A" 書式指定子における型の表示レイアウトのカスタマイズ

判別共用体を文字列として出力する際に、ケース識別子を宣言する型(判別共用体)の名前を含めたフルネームで文字列化したくなったときのお話。

たとえば、以下を実行すると

type Tree<'T> =
  | Leaf of 'T
  | Node of Tree<'T> * Tree<'T>

let tree1 = Node(Node(Leaf("a"),Node(Leaf("b"),Node(Leaf("c"),Leaf("d")))),Node(Leaf("e"),Leaf("f")))
printfn "%A" tree1

次の出力結果を得られる。

Node
  (Node (Leaf "a",Node (Leaf "b",Node (Leaf "c",Leaf "d"))),
   Node (Leaf "e",Leaf "f"))


それを、以下のような感じに出力するようにしたい。というのが今回のお題。

Tree.Node
  (Tree.Node (Tree.Leaf "a",Tree.Node (Tree.Leaf "b",Tree.Node (Tree.Leaf "c",Tree.Leaf "d"))),
   Tree.Node (Tree.Leaf "e",Tree.Leaf "f"))


StructuredFormatDisplay属性を使う

Core.StructuredFormatDisplayAttribute クラス (F#) - MSDNライブラリ

この属性は、%A printf 書式設定やその他の 2 次元のテキストベースの表示レイアウトを使用する場合に、型を表示する既定の方法を指定するために使用されます。 このバージョンの F# で有効な値は、PreText {PropertyName} PostText 形式の値のみです。 プロパティ名は、オブジェクトそのものの代わりに評価および表示するプロパティを表します。

とある。これを使えば、型を表示する際のレイアウトを自由にカスタマイズすることができる。なお、"このバージョンの F# で有効な値は"とあるが、F#2.0からF#3.1までは変更はない(F#2.0より前は仕様が異なる)。


StructuredFormatDisplayAttribute。自作ライブラリをせっせとこさえていたり、某有名F# ライブラリのソースコード等を読んでたり、コンパイラのソースを見ていたりする人なら見覚えがあるかもしれないが、"%A"書式指定子の表示レイアウトをカスタマイズしたい場面はそんなに多くはないだろうし、そこそこマニアックな(割とどーでもいい)話題かもしれない。こちら、「実践F#」に載っていないというか、「Expert F#3.0」にも載ってなかったと思うし、いまのところ最新の言語仕様書であるところの「The F# 3.0 Language Specification」にも記載されていないようなので、言語仕様書熟読勢も把握できていない可能性がある。でも、「プログラミングF#」にはサラりと載っていたりする(!)。


単純な例

StructuredFormatDisplay属性を使った単純な例は以下のようになる(ここでは例として判別共用体を対象としているが、その限りではない)。

[<StructuredFormatDisplay("Hello{Display}!")>]
type Hello = 
  | Hello of string 
  member private this.Display = 
    match this with 
    | Hello s -> sprintf ", %s" s

let hello = Hello("F#")
printfn "%A" hello

出力結果は以下のようになる。

Hello, F#!

使い方めちゃ簡単。

FizzBuzzしてみる

意味もなくFizzBuzzしてみます。

[<StructuredFormatDisplay("{Display}")>]
type FizzBuzz = 
  | FizzBuzz of Fizz * Buzz
  member private this.Display =
      let (|Mul|_|) x y = if y % x = 0 then Some(y / x) else None
      let fizzbuzz x y = 
        let xy = x * y
        [1..100] |> List.map (function
        | Mul  xy _ -> "FizzBuzz"
        | Mul  x _ -> "Fizz"
        | Mul  y _ -> "Buzz"
        | n -> string n)

      match this with
      | FizzBuzz (Fizz(x), Buzz(y)) -> fizzbuzz x y

and Fizz = Fizz of int
and Buzz = Buzz of int

let fizzbuzz = FizzBuzz(Fizz(3),Buzz(5))
printfn "%A" fizzbuzz
printfn "%s" <| fizzbuzz.ToString()
printfn "%O" fizzbuzz

出力結果

["1"; "2"; "Fizz"; "4"; "Buzz"; "Fizz"; "7"; "8"; "Fizz"; "Buzz"; "11"; "Fizz";
 "13"; "14"; "FizzBuzz"; "16"; "17"; "Fizz"; "19"; "Buzz"; "Fizz"; "22"; "23";
 "Fizz"; "Buzz"; "26"; "Fizz"; "28"; "29"; "FizzBuzz"; "31"; "32"; "Fizz"; "34";
 "Buzz"; "Fizz"; "37"; "38"; "Fizz"; "Buzz"; "41"; "Fizz"; "43"; "44";
 "FizzBuzz"; "46"; "47"; "Fizz"; "49"; "Buzz"; "Fizz"; "52"; "53"; "Fizz";
 "Buzz"; "56"; "Fizz"; "58"; "59"; "FizzBuzz"; "61"; "62"; "Fizz"; "64"; "Buzz";
 "Fizz"; "67"; "68"; "Fizz"; "Buzz"; "71"; "Fizz"; "73"; "74"; "FizzBuzz"; "76";
 "77"; "Fizz"; "79"; "Buzz"; "Fizz"; "82"; "83"; "Fizz"; "Buzz"; "86"; "Fizz";
 "88"; "89"; "FizzBuzz"; "91"; "92"; "Fizz"; "94"; "Buzz"; "Fizz"; "97"; "98";
 "Fizz"; "Buzz"]
Program+FizzBuzz
Program+FizzBuzz


この結果から、StructuredFormatDisplay属性を使って型の表示方法をカスタマイズしても、"%s"および"%O"書式指定子に影響を及ぼしていないことが確認できる。"%s"および"%O"書式指定子を指定した場合、いずれも結果的に対象オブジェクトについて Object.ToString仮想メソッドが呼び出されるかたちになるので、判別共用体の場合は既定では上記のように型名が出力される。 override this.ToString () = sprintf "%A" this.Displayというように、ToStringメソッドをオーバーライドする実装を追加すれば、いずれも "%A"書式指定子を指定した場合と同じ結果が得られるようになる。F#2.0より前のバージョンでは ToStringを経由して表示する際にStructuredFormatDisplay属性を参照していたようだが、F#2.0以降はToStringメソッドを経由する場合にはこれを参照しないよう仕様が変更された。

StructuredFormatDisplay属性で指定した{PropertyName}を実装していない場合

ちょっと例を変えて、レコード型にしてみる。

[<StructuredFormatDisplay("{AsString}")>]
type myRecord = 
  {value : int}
  override this.ToString() = "hello"
  //member this.AsString = this.ToString()

let t = {value=5}
printfn "%s" (t.ToString())
printfn "%O" t
printfn "%A" t

出力結果

hello
hello
<StructuredFormatDisplay exception: メソッド 'Program+myRecord.AsString' が見つかりません。>

とまあ、StructuredFormatDisplay属性で指定した{PropertyName}を実装していない場合は、 コンパイルエラーとなるわけでなく例外となるわけでなく、割と残念な感じの出力結果を得ることになる。コンパイルエラーにしてくれてもいいのにー。

判別共用体(discriminated unions)について、型名も含めて文字列化する

さて、本題。

まずは愚直に書いてみよう

StructuredFormatDisplay属性でマークし、表示をカスタマイズする実装を愚直に書き加える。

[<StructuredFormatDisplay("{Display}")>]
type Tree<'T> =
  | Leaf of 'T
  | Node of Tree<'T> * Tree<'T>
  member private t.Display = 
    match t with
    | Leaf x -> sprintf "%s %A" "Tree.Leaf" x 
    | Node (a,b) -> sprintf "%s %A" "Tree.Node" (a,b) 

let tree1 = Node(Node(Leaf("a"),Node(Leaf("b"),Node(Leaf("c"),Leaf("d")))),Node(Leaf("e"),Leaf("f")))
printfn "%A" tree1

以下の出力結果が得られる。

Tree.Node (Tree.Node (Tree.Leaf "a",
 Tree.Node (Tree.Leaf "b", Tree.Node (Tree.Leaf "c", Tree.Leaf "d"))),
 Tree.Node (Tree.Leaf "e", Tree.Leaf "f"))

おいおい。PreText使おうぜ

あっ。ケース識別子を宣言する型(判別共用体)の名前は固定なので、この場合StructuredFormatDisplay属性のPreTextに集約できるんだったね。

[<StructuredFormatDisplay("Tree.{Display}")>]
type Tree<'T> =
  | Leaf of 'T
  | Node of Tree<'T> * Tree<'T>
  member private t.Display = 
    match t with
    | Leaf x -> sprintf "%s %A" "Leaf" x 
    | Node (a,b) -> sprintf "%s %A" "Node" (a,b) 

let tree1 = Node(Node(Leaf("a"),Node(Leaf("b"),Node(Leaf("c"),Leaf("d")))),Node(Leaf("e"),Leaf("f")))
printfn "%A" tree1

出力結果変わらず。

Tree.Node (Tree.Node (Tree.Leaf "a",
 Tree.Node (Tree.Leaf "b", Tree.Node (Tree.Leaf "c", Tree.Leaf "d"))),
 Tree.Node (Tree.Leaf "e", Tree.Leaf "f"))

このTree<'T>判別共用体の場合だけに関して言えば、とりあえずこれで良さそうに見えるし、この方法を取れば他の判別共用体についても都度対応できそうだ。 でも、毎回個別に対応するなんてダルすぎる。汎用的にしたいよねー。

リフレクションで汎用的に実装しよう

Microsoft.FSharp.Reflectionを利用する。

open Microsoft.FSharp.Reflection 

let stringifyFullName (discriminatedUnion:'T) = 
  if box discriminatedUnion = null  then
    nullArg  "discriminatedUnion"   
  if FSharpType.IsUnion(typeof<'T>)|> not then
    invalidArg "discriminatedUnion" (sprintf "判別共用体じゃないよ:%s" typeof<'T>.FullName)

  let info, objects = FSharpValue.GetUnionFields(discriminatedUnion, typeof<'T>)
  let typeName = 
    if info.DeclaringType.IsGenericType then
      info.DeclaringType.Name.Substring(0, info.DeclaringType.Name.LastIndexOf("`"))  + "." + info.Name
    else
      info.DeclaringType.Name + "." + info.Name
  match objects  with
  | [||] -> typeName
  | elements -> 
    let fields = info.GetFields()
    if fields.Length = 1 then
      sprintf "%s %A" typeName elements.[0]
    else
      let tupleType = 
        fields
        |> Array.map( fun pi -> pi.PropertyType )
        |> FSharpType.MakeTupleType
      let tuple = FSharpValue.MakeTuple(elements, tupleType)
      sprintf "%s %A" typeName tuple

[<StructuredFormatDisplay("{ToStructuredDisplay}")>]
type Tree<'T> =
  | Leaf of 'T
  | Node of Tree<'T> * Tree<'T>
  member private t.ToStructuredDisplay = t.ToString()
  override t.ToString () = stringifyFullName t 

let tree1 = Node(Node(Leaf("a"),Node(Leaf("b"),Node(Leaf("c"),Leaf("d")))),Node(Leaf("e"),Leaf("f")))
printfn "%A" tree1

出力結果

Tree.Node (Tree.Node (Tree.Leaf "a",
 Tree.Node (Tree.Leaf "b", Tree.Node (Tree.Leaf "c", Tree.Leaf "d"))),
 Tree.Node (Tree.Leaf "e", Tree.Leaf "f"))

ヽ(*´∀`)ノ ワーイ、できたよー

と、思ったけど、待って。違うやん。本当は以下のようなレイアウトで表示したかったんだった(だった!)。

Tree.Node
  (Tree.Node (Tree.Leaf "a",Tree.Node (Tree.Leaf "b",Tree.Node (Tree.Leaf "c",Tree.Leaf "d"))),
   Tree.Node (Tree.Leaf "e",Tree.Leaf "f"))


んー、内容的には同じなのでそんなに大きな問題ではないんだけど、若干モヤッとする。 "%A" 書式指定子の表示レイアウトをいい感じに制御するにはどうすればよいのだろう? また、既存の型(例えばOption<'T>型など)の、表示をカスタマイズしたい場合はどうすればよいのだろう?

F#er諸兄、何かご存じであればアドバイス頂きたい。