Is there a way to inspect a list of integers without accidentally ending up with a charlist?

Story

skip this if you are not interested in the Why

Today I was working on a rewrite of a feature, heavily relying on a 3rd Party HTTP API response, no tests available yet, so I decided to bite the bullet and do what I felt like I had to do. Fortunately all the API connections are done in a way, so at least parts of the return values can be mocked, and no actual request has to be made, but you need to provide a valid response.
The response in this case happened to be a big map - 140+x lines of code after parsing the JSON response and running it through the Elixir Formatter. So I thought I was clever, and ran a real example, writing the response to a txt file, and using File.write!(path, inspect(response), so I could copy-paste a real response into a fixture. The problem was that one part %{..., something_ids: Enum.map(raw["something"], & &1["id"]), ... returned [123], which everyone intuitively knows equals to '{' when inspected, because ?{ == [123].
I was running into weird bugs, because '{' was passed down to an ecto query, which would of course find nothing with those ids, and I was banging my head against the wall, why the hell the changeset error was “invalid association” (a lot more going on behind the scenes, but you get the idea). So, I put IEx.pry into multiple places, and saw the error in the fixture. I did not think much about it, replaced it with the value I knew it should be and… It still did not work.
Of course I put IO.inspect(something_ids) in multiple places and always got '{'. At that time I was already stressed out, because that was NOT the bug I was hunting, this was something in my test setup!

After some more head-banging-against-the-wall, I pasted a screen recording into our developer chat, asking what’s going on, and only 1 minute later someone said "does this explain anything? ?{ #=> [123], and I was very ready to bang my head even more, this time voluntarily, for being such an idiot.

Problem

I think I have a basic understanding of why a list of integers might be displayed as a string, but it gave us problems debugging multiple time now - in fact the guy who gave me the clue, was someone I had given the same clue a few months ago.
Not only debugging tends to get harder, if your outputs are displayed differently than what you would expect, but also admin UIs: I frequently use inspect in interfaces, for example to display exq failed jobs and the arguments, error message etc.

Question

I understand this might be a problem with internal representations, but is there any way to display a list of integers (that could, or could not) be a list of characters?

Sidenote

The universe decided to give me a headache with this, because incidentally, the original bug had the same error message.

Take a look at Inspect.Opts and the :charlists option.

[123, 124] |> IO.inspect(charlists: :as_charlists) # => '{|'
[123, 124] |> IO.inspect(charlists: :as_lists) # => [123, 124]
[123, 124] |> IO.inspect(charlists: :infer) # => '{|'

When the default :infer , the list will be printed as a charlist if it is printable, otherwise as list. See List.ascii_printable?/1 to learn when a charlist is printable.

8 Likes

Thank you so much… I remember googling for this issue some time ago, and not finding a satisfying solution. I hope this answer will pop up for future search engine users, running into this problem.

You can also use i/1 in iex to have everything in one output:

iex(1)> i 'abc'
Term
  'abc'
Data type
  List
Description
  This is a list of integers that is printed as a sequence of characters
  delimited by single quotes because all the integers in it represent printable
  ASCII characters. Conventionally, a list of Unicode code points is known as a
  charlist and a list of ASCII characters is a subset of it.
Raw representation
  [97, 98, 99]
Reference modules
  List
Implemented protocols
  Collectable, Enumerable, IEx.Info, Inspect, List.Chars, String.Chars

Thanks, that’s something I did not consider.

How would you use that in debugging in a running system? Can you pipe it into IO.inspect?

EDIT: sorry, didn’t read that right… You can use it in IEx.pry but you would have to remember what the issue might be :stuck_out_tongue:

You can pipe into everything… And the nice thing about IO.inspect is, its an identity function. It will return exactly what you gave it. Together with the :label option, its ideal to do ad-hoc debugging of pipelines:

1
|> IO.inspect(label: "before")
|> Kernel.+(1)
|> IO.inspect(label: "after")
1 Like

How would that work? Suppose this code

defmoudle Thing do
  def my_func(args)
    args
    |> do_something()
  end
end

can you do something like:

def my_func(args) do
  require IEx
  IEx.i(args) |> IO.inspect()
  # ...
end

You shouldn’t use IEx.i/1 from within code…

1 Like

Then how would you use IO.inspect in a meaningful way, together with IEx.i?

Why should I?

I use IEx.i/1 from iex, while I use IO.inspect/2 from within my code.

1 Like

I see. I had not considered that option, because my original problem came from writing stuff to a file, which gave me “{” instead of “[123]” and I was too blind to see.

I appreciate the suggestion of using i xxx in debugging, but my question was, can you use it as a debugging tool somewhere in your code (like old IO.inspect) You kind of confirmed that you can’t though :wink: Am I right in my assumption, that you would not do something like i(arg["x"]) |> IO.inspect()?

i/1 is a helper in iex, it describes the data it sees and prints this description into the terminal, and nothing else. And because some magic in iex its return value is omitted.

Though by doing exactly that, piping the result of i/1 into IO.inspect I’ve learned that it returns :"do not show this result in output".

1 Like

TIL :smiley: