loading...
gumi TECH Blog

Elixir: inspect/2関数について

gumitech profile image gumi TECH ・4 min read

本稿は「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関数の場合は:prettytrueのときしか効果がありません。逆に、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?/2trueの場合には文字列として、それ以外の場合はビット構文として出力されます。
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?/2trueの場合には文字のリストとして、それ以外の場合はリストとして出力されます。

実装を見るかぎり、アルファベットや空白文字(改行やスペース、タブ)以外、たとえば日本語が含まれている場合はリストとして表示されるようです。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エスケープコードに対応したコンソールに出力すると、指定した色で表示されます。

qiita_1901_001_001.png

: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"

コンソールに出力すると、指定した色で表示されます。

qiita_1901_001_002.png

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

  1. 要素を表示するたびに:limitがひとつずつ減り、0以下になったら表示しないという再帰処理の結果を示したのが以下の図です。ロジックを詳しく知りたい方は、Inspect.Algebra.container_doc/6実装をご参照ください。 qiita_1901_001_003.png 

  2. ドキュメントのInspect.Opts には、バイト数("number of bytes")と説明されています。けれど、コードをみるかぎり、コードポイントを数えているようです。ただし、UTF-8として無効な文字列であってもエスケープして表示しようとしますので、コードポイントというのも正確な表現ではありません。 

Posted on by:

gumitech profile

gumi TECH

@gumitech

gumi TECH は、株式会社gumiのエンジニアによる技術記事公開やDrinkupイベントなどの技術者交流を行うアカウントです。 gumi TECH Blog: http://dev.to/gumi / gumi TECH Drinkup: http://gumitech.connpass.com

gumi TECH Blog

株式会社gumiのエンジニアによる技術記事を公開しています。

Discussion

pic
Editor guide