17899


以下のようなFormatをつかったデータを出力する関数があります:

[code]

    type e = Int of int | Add of e * e
    
    let rec pp_e ppf = function
      | Int(i) -> Format.fprintf ppf "Int(%d)@?" i
      | Add(e1, e2) -> Format.fprintf ppf "Add(%a,%a)@?" pp_e e1 pp_e e2
    
[/code]

この関数は以下のようにすると標準出力へ出力出来ます:

[code]

    let _ = pp_e Format.std_formatter (Add(Int 1, Int 2))
    
[/code]

ここで、この関数を利用して以下のような型のeを文字列に変換する関数を作りたいのですが:

[code]

    let show_e (e:e):string = ...
    
[/code]

一般的にFormatを使った関数を使って文字列へ変換する方法を教えてください。


CC BY-SA 3.0
2015-10-09T02:57:42.943
17458
2015-10-21T11:35:47.530
2015-10-21T11:35:47.530
898
7532
question
ocaml
OCamlでFormat.fprintfを使った関数の文字列への変換


上の例を改変して答えます。Format で AST をプリントする場合、間違いなく box を使うものですし (でなければ Printf
でよい)、その場合、 flush するとレイアウトが狂います。また、flush が入っていると後の説明にも支障をきたすので、外してあります。

[code]

    open Format
    
    type e = Int of int | Add of e * e
    
    let rec pp_e ppf = function
      | Int(i) -> fprintf ppf "Int(%d)" i
      | Add(e1, e2) -> fprintf ppf "Add(@[%a,@ %a@])" pp_e e1 pp_e e2
    
    let rec make = function
      | 0 -> Int 0
      | n -> Add (Int n, Add (make (n-1), make (n-1)))
    
    let e = make 3
    
[/code]

一番簡単なのは `str_formatter` に書込み、結果を `flush_str_formatter ()` で取り出す方法です。  
ただしこれはグローバル状態 `stdbuf` を変更するのでマルチスレッドでは誤動作しません。  
シングルスレッドでも、`stdbuf` に既にゴミが入っているとゴミが出力されます。良い方法ではありません:

[code]

    let to_string' p x =
      p str_formatter x;
      flush_str_formatter ()
    
    let () = prerr_endline (to_string' pp_e e)         
    
[/code]

自前のバッファを用意し、それに書き込むフォーマッタを定義し、そのフォーマッタに書き込むことで、  
グローバル状態を使うことを避けることができます:

[code]

    let to_string p x =
      let buf = Buffer.create 0 in
      let ppf = formatter_of_buffer buf in
      p ppf x;
      pp_print_flush ppf ();
      Buffer.contents buf
    
    let () = prerr_endline (to_string pp_e e);;
    
[/code]

ここで注意しなければいけないのは出力を取り出す前にフラッシュしなければいけない事。`pp_print_flush ppf ()`
を明示的に呼び出さないかぎり Format
のレイアウトエンジンはレイアウトを終了できず結果をバッファに書き込みできません。これを忘れると結果が全く出て来なかったり中途半端になる可能性があります。

`to_string` のような関数を定義する前に、フォーマット結果を文字列にする方法としてまず思い付くのは `sprintf`
だと思いますが、`sprintf` と `pp_e` を組合せようとしてもうまくいきません:

[code]

    % let s = sprintf "%a" pp_e e;;
    
    Characters 25-29:
    Error: This expression has type Format.formatter -> e -> unit
           but an expression was expected of type unit -> 'a -> string
           Type Format.formatter is not compatible with type unit 
    
[/code]

これは `sprintf` の第一引数の型 `('a, unit, string) format` が十分ではなくフォーマット文字列中に `%a`
を取れないからです。

`%a` を使って最終結果を文字列として得るため、 `format4` というデータ型と`kfprintf` 関数が追加されました。この
`kfprintf` を使うと自前の `%a` が使える「良い」 `sprintf` を定義することができます:

[code]

    let sprintf' fmt =
      let buf = Buffer.create 0 in
      let ppf = formatter_of_buffer buf in
      kfprintf (fun ppf -> pp_print_flush ppf (); Buffer.contents buf) ppf fmt
    
    let () = prerr_endline (sprintf' "%a" pp_e e);;
    
[/code]

以前はこの `sprintf'` を自作する必要がありました。あまりに不便なのでOCaml 4.01.0 からは `asprintf`
という物が用意されています:

[code]

    let () = prerr_endline (asprintf "%a" pp_e e);;
    
[/code]

`%a` が使えない `sprintf` の方はそのままになっていますが、後方互換性のためだと思われます。

`to_string p x` を使うか `asprintf "%a" p x` を使うかですが、場合によります。`"%a"`
なのであれば前者が短く効率も良い。`"result= @[%a@]"` のようにさらにレイアウトを行わせたい場合には `asprintf`
を使うことになります。


CC BY-SA 3.0
2015-10-21T11:29:18.843
17899
2015-10-21T11:29:18.843
898
17458
answer