Weird behaviour in iex with `StringIO.open` and pipe operator

Hello,

I am using iex to learn Elixir and I have noticed something weird in iex while playing with StringIO.open and Stream.chunk_while.

If I copy and paste this code directly in iex, I will have an error:

open = fn -> {:ok, pid} = StringIO.open("12345"); pid end
chunk_fun = fn char, acc -> {:cont, char, [char | acc]} end
after_fun = fn acc -> {:cont, acc} end

# Note the break lines here
result =
  open.()
  |> IO.binstream(1)
  |> Stream.chunk_while([], chunk_fun, after_fun)

Enum.to_list(result)

# The error:
** (Protocol.UndefinedError) protocol Enumerable not implemented for #PID<0.251.0> of type PID. This protocol is implemented for the following type(s): Date.Range, File.Stream, Function, GenEvent.Stream, HashDict, HashSet, IO.Stream, List, Map, MapSet, Range, Stream
    (elixir 1.13.4) lib/enum.ex:1: Enumerable.impl_for!/1
    (elixir 1.13.4) lib/enum.ex:143: Enumerable.reduce/3
    (elixir 1.13.4) lib/enum.ex:4144: Enum.reverse/1
    (elixir 1.13.4) lib/enum.ex:3491: Enum.to_list/1

For some reason, a pid is assigned to result before calling Enum.to_list

But if I try different other ways, it will work:

  • This works with pipes on a single line:
open = fn -> {:ok, pid} = StringIO.open("12345"); pid end
chunk_fun = fn char, acc -> {:cont, char, [char | acc]} end
after_fun = fn acc -> {:cont, acc} end

# Piping in a single line
result = open.() |> IO.binstream(1) |> Stream.chunk_while([], chunk_fun, after_fun)

Enum.to_list(result)
# => ["1", "2", "3", "4", "5"]
  • This works when not using pipes:
open = fn -> {:ok, pid} = StringIO.open("12345"); pid end
chunk_fun = fn char, acc -> {:cont, char, [char | acc]} end
after_fun = fn acc -> {:cont, acc} end

# No pipes
stream = open.()
binstream = IO.binstream(stream, 1)
result = Stream.chunk_while(binstream, [], chunk_fun, after_fun)

Enum.to_list(result)
# => ["1", "2", "3", "4", "5"]
  • This will works if I pipe and do not add a break line for the first pipe:
open = fn -> {:ok, pid} = StringIO.open("12345"); pid end
chunk_fun = fn char, acc -> {:cont, char, [char | acc]} end
after_fun = fn acc -> {:cont, acc} end

# 1 pipe on the first line, second pipe on the next line
result = open.() |> IO.binstream(1)
  |> Stream.chunk_while([], chunk_fun, after_fun)

Enum.to_list(result)
# => ["1", "2", "3", "4", "5"]

If I put the not working version in a module then call the function module from iex, it works without issues.

Pasting chains of pipes in iex doesn’t quite work like in a file. Take note of when output prints here:

iex(1)> open = fn -> {:ok, pid} = StringIO.open("12345"); pid end
#Function<45.65746770/0 in :erl_eval.expr/5>
iex(2)> chunk_fun = fn char, acc -> {:cont, char, [char | acc]} end
#Function<43.65746770/2 in :erl_eval.expr/5>
iex(3)> after_fun = fn acc -> {:cont, acc} end
#Function<44.65746770/1 in :erl_eval.expr/5>
iex(4)> 
nil
iex(5)> # Note the break lines here
nil
iex(6)> result =
...(6)>   open.()
#PID<0.116.0>

iex(7)>   |> IO.binstream(1)
%IO.Stream{device: #PID<0.116.0>, line_or_bytes: 1, raw: true}

iex(8)>   |> Stream.chunk_while([], chunk_fun, after_fun)
#Stream<[
  enum: %IO.Stream{device: #PID<0.116.0>, line_or_bytes: 1, raw: true},
  funs: [#Function<3.58486609/1 in Stream.chunk_while/4>]
]>
iex(9)> 
nil
iex(10)> Enum.to_list(result)
** (Protocol.UndefinedError) protocol Enumerable not implemented for #PID<0.116.0> of type PID
    (elixir 1.13.4) lib/enum.ex:1: Enumerable.impl_for!/1
    (elixir 1.13.4) lib/enum.ex:143: Enumerable.reduce/3
    (elixir 1.13.4) lib/enum.ex:4144: Enum.reverse/1
    (elixir 1.13.4) lib/enum.ex:3491: Enum.to_list/1

The iex(6) line is where things diverge: when pasted, result is bound to the return value from open.()!

2 Likes

I totally missed that part. Thank you!