Elixir: inspect/2関数について
gumi TECH
Posted on January 21, 2019
本稿は「inspect について調べてみた」をもとに加筆・補正し、文章を整えました。
多くのElixirの開発者にとってinspect/2
は「どんなtermでも文字列にしてくれる便利な関数」という認識でしょう。それはもちろん確かです。けれど、さらに調べてみると、奥が深いことに気づきます。
出力フォーマット
inspect/2
は、デフォルトではただ文字列に変換するだけです。けれども、豊富なオプションが備わっています。ドキュメントには、オプションについてはInspect.Opts
を参照とあるものの、あまり詳しくはありません。本稿では、役に立ちそうなオプションについて、ドキュメントに書かれていない点も補ってご説明します。網羅的ではありませんので、オプション一覧はドキュメントでお確かめください。
:limit
タプル、ビットストリング、マップ、リストやその他コレクション全体で表示する要素数を定めます。文字列と文字リストには後述:printable_limit
をお使いください。デフォルトは50個です。:infinity
を与えると制限なく、すべて表示します 。
# 要素数3個までしか表示しない
iex> inspect [1, 2, 3, 4, 5], limit: 3
"[1, 2, 3, ...]"
inspect/2
で出力した要素が途中で...
に隠れてデバッグできないという場合は、:infinity
で確かめられるようになります。ただし、ログの量が激増して動作を遅くすることもありますので、注意してください。
また、:limit
は単純な要素数の指定ではありません。要素を表示するたびに:limit
はひとつずつ減らし、0以下になったら表示しないというロジックを再帰して実行します。たとえば、つぎの例はネストしたデータの場合です。limit: 4
で6要素表示されていることがわかります1。
iex> inspect [{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12}], limit: 4
"[{1, 2, 3}, {4, 5, ...}, {7, ...}, {...}]"
:printable_limit
文字列や文字のリストを表示するコードポイント数を定めます2。デフォルトは4096文字です。:infinity
を与えると制限なく、すべて表示します。:infinity
を使う場合の注意は、前述:limit
と同じです。
iex(3)> inspect ["123456789", "あいうえおかきく"], printable_limit: 5
"[\"12345\" <> ..., \"あいうえお\" <> ...]"
上記のふたつのオプションを合わせて用いれば、すべてのtermが出力されるので、覚えておくと便利でしょう。ただし、ログの量には注意してお使いください。
def dump(value) do
inspect value, limit: :infinity, printable_limit: :infinity
end
:width
出力する横幅の指定で、デフォルトは80文字です。ただし、inspect/2
関数の場合は:pretty
がtrue
のときしか効果がありません。逆に、IO.inspect/2
関数の場合は、つねに:pretty
を無視します。:infinity
を与えると、横幅を気にせず表示します。
そのため、IO.inspect/2
を使ってIO.puts "#{inspect value}"
と同じ表示にするなら、IO.inspect value, width: :infinity
のように:infinity
を指定するのがよいでしょう。
なお、0を定めた場合は、要素ごとに改行します。
# inspect で :pretty を指定しない場合、:width は無視される
iex> inspect [1, 2, 3], width: 0
"[1, 2, 3]"
# pretty: true にすると width: 0 が有効になる
iex> inspect [1, 2, 3], pretty: true, width: 0
"[1,\n 2,\n 3]"
# IO.inspect は pretty オプションを無視して常に pretty print する
iex> IO.inspect [1, 2, 3], pretty: false, width: 0
[1,
2,
3]
[1, 2, 3]
:binaries
渡されたバイナリをどのように扱うかを定めます。文字列も単なるバイナリですので、inspect/2
ではこのオプションを見てどのように表示するかを決めます。値はつぎの3つです。
-
:as_strings
: 文字列として出力され、表示できないバイトはエスケープされます。 -
:as_binaries
: ビット構文で出力されます。 -
:infer
(デフォルト):String.printable?/2
がtrue
の場合には文字列として、それ以外の場合はビット構文として出力されます。
iex> inspect("olá")
"\"olá\""
iex> inspect("olá" <> <<0>>)
"<<111, 108, 195, 161, 0>>"
iex> inspect("olá" <> <<0>>, binaries: :as_strings)
"\"olá\\0\""
iex> inspect("olá", binaries: :as_binaries)
"<<111, 108, 195, 161>>"
:charlists
渡されたリストの扱い方を定めます。文字のリストも単なるリストですので、inspect/2
はこのオプションによりどう表示するかを決めます。値はつぎの3つです。
-
:as_charlists
: リストがすべて文字リストとして出力されます。 -
:as_lists
: すべてリストとして出力されます。 -
:infer
(デフォルト):Inspect.List.printable?/2
がtrue
の場合には文字のリストとして、それ以外の場合はリストとして出力されます。
実装を見るかぎり、アルファベットや空白文字(改行やスペース、タブ)以外、たとえば日本語が含まれている場合はリストとして表示されるようです。Erlangに似た挙動といえます。
iex> inspect('bar')
"'bar'"
iex> inspect('barバー')
"[98, 97, 114, 12496, 12540]"
iex> inspect('barバー', charlists: :as_charlists)
"'barバー'"
iex> inspect('bar', charlists: :as_lists)
"[98, 97, 114]"
:syntax_colors
出力する文字列のカラーを、キーワードリストで定められます。キーはタイプで、値が使う色です。タイプには、:number
、:atom
、:regex
, :tuple
、:map
、:list
、and :reset
が標準で含まれています。
つぎのコードは、atomを:cyan
、マップは:magenta
で、数値を:black
にしたうえで、背景色は:light_blue_background
で出力します。
iex> inspect %{x: 10, y: 20}, syntax_colors: [atom: :cyan, map: :magenta, number: [:black, :light_blue_background]]
"\e[35m%{\e[0m\e[36mx:\e[0m \e[30m\e[104m10\e[0m\e[35m,\e[0m \e[36my:\e[0m \e[30m\e[104m20\e[0m\e[35m}\e[0m"
色のエスケープコードはANSIです。ANSIエスケープコードに対応したコンソールに出力すると、指定した色で表示されます。
:syntax_colors
には、キーとして任意のアトムが渡せます。ただし、標準のキーしか使われません。その他のキーは、独自に拡張したときに利用可能です。
値には IO.ANSI
に定義されている色の関数名(アトム)か、任意の文字列、あるいはそれらのリスト(ネスト可)が指定できます。
Inspect
プロトコルについて
構造体をつくったとき、inspect/2
の呼び出しによるデフォルト表示はつぎのようになります。前述のオプション、たとえば:width
も使えます。このとき、pretty: true
を加えることにご注意ください。
defmodule MyStruct do
defstruct [:x, :y]
end
iex> IO.puts(inspect %MyStruct{x: 10, y: 20})
%MyStruct{x: 10, y: 20}
:ok
iex> IO.puts(inspect %MyStruct{x: 10, y: 20}, pretty: true, width: 0)
%MyStruct{
x: 10,
y: 20
}
:ok
この表示をカスタマイズしたい場合には、Inspect
プロトコルを実装しなければなりません。
Inspect
プロトコルを実装する
inspect/2
は、第1引数に構造体を受け取ります。その値を出力する文字列で表せば、独自のフォーマットが実装できます。
defimpl Inspect, for: MyStruct do
def inspect(term, _opts) do
"(#{term.x}, #{term.y})"
end
end
iex> IO.puts(inspect %MyStruct{x: 10, y: 20})
(10, 20)
:ok
改行を適切に入れる
Inspect
プロトコルで実装した独自のフォーマットに:width
オプションを用いても、そのままでは幅が縮まりません。改行できる値の区切りは、情報としてinspect/2
の戻り値に含めなければならないのです。
iex(2)> IO.puts(inspect %MyStruct{x: 10, y: 20}, pretty: true, width: 0)
(10, 20)
:ok
そこで、区切りやネストなどのメタ情報を加えて返すために使うのがInspect.Algebra
です。:width
や:limit
といったオプションに対して適切に表示するには、Inspect.Algebra.container_doc/6
を用います。
defimpl Inspect, for: MyStruct do
# def inspect(term, _opts) do
def inspect(term, opts) do
# "(#{term.x}, #{term.y})"
Inspect.Algebra.container_doc("(", [term.x, term.y], ")", opts, &Inspect.inspect/2)
end
end
iex> IO.puts(inspect %MyStruct{x: 10, y: 20})
(10, 20)
# :widthを小さくする
iex> IO.puts(inspect %MyStruct{x: 10, y: 20}, pretty: true, width: 0)
(10,
20)
# 構造体をネストする
iex> IO.puts(inspect %MyStruct{x: 10, y: %MyStruct{x: 100, y: 200}}, pretty: true, width: 0)
(
10,
(100,
200)
)
# :limitを定める
iex> IO.puts(inspect %MyStruct{x: 10, y: %MyStruct{x: 100, y: 200}}, pretty: true, width: 0, limit: 2)
(
10,
(...)
)
横幅に応じて適切に改行が入り、しかもネストしたとき行頭の空白も正しく加えられました。
色をつける
Inspect.Algebra.color/3
で色をつけることもできます。第2引数に渡すカラーキー(:my_struct
)は、inspect/2
を呼び出すとき独自に定めて渡します。
defimpl Inspect, for: MyStruct do
def inspect(term, opts) do
Inspect.Algebra.container_doc(
Inspect.Algebra.color("(", :my_struct, opts),
[term.x, term.y],
Inspect.Algebra.color(")", :my_struct, opts),
opts,
&Inspect.inspect/2)
end
end
つぎのコードは、inspect/2
に:syntax_colors
オプションでカラーキー:my_struct
を:red_background
として渡しています。
iex(2)> inspect %MyStruct{x: 10, y: 20}, syntax_colors: [my_struct: :red_background]
"\e[41m(\e[0m10, 20\e[41m)\e[0m"
コンソールに出力すると、指定した色で表示されます。
IO.inspect/2
関数について
「出力フォーマット」の「:width」でも触れたように、IO.puts(inspect value)
とIO.inspect value
は同じ出力にはなりません。IO.inspect/2
関数が異なるのはつぎの点です。
-
:pretty
オプションは無視します。- つねにpretty printです。
-
:label
オプションが使えます。- "my_label: value"のように表示されます。
- 与えられた値を返します。
- パイプラインの途中で値が簡単に確かめられます。
ドキュメントのIO.inspect/2
の項には、「Examples」のひとつとしてつぎのようなコードが紹介されています。
iex> [1, 2, 3] |> IO.inspect(label: "before") |> Enum.map(&(&1 * 2)) |> IO.inspect(label: "after") |> Enum.sum
before: [1, 2, 3]
after: [2, 4, 6]
12
-
要素を表示するたびに
:limit
がひとつずつ減り、0以下になったら表示しないという再帰処理の結果を示したのが以下の図です。ロジックを詳しく知りたい方は、Inspect.Algebra.container_doc/6
の実装をご参照ください。 ↩ -
ドキュメントの
Inspect.Opts
には、バイト数("number of bytes")と説明されています。けれど、コードをみるかぎり、コードポイントを数えているようです。ただし、UTF-8として無効な文字列であってもエスケープして表示しようとしますので、コードポイントというのも正確な表現ではありません。 ↩
Posted on January 21, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.