Pretty-printing complex types


#1

One of the things that I love about Rust is the ability to use its flexible type system to enforce many different kinds of constraints at compile time with no cost at runtime. This can sometimes come at the cost of complex type signatures. Recently I hit an error which contained the following type (as you can see, I am working with the futures crate):

futures::stream::ForEach<futures::stream::MapErr<futures::stream::Zip<tokio_core::net::Incoming, futures::stream::IterOk<std::ops::RangeFrom<usize>, std::io::Error>>, [closure@src/lib.rs:175:18: 175:24]>, [closure@src/lib.rs:176:19: 187:10 new_codec:F, controller:sink_splitter::Controller<usize, futures::sink::SinkMapErr<futures::stream::SplitSink<tokio_io::codec::Framed<tokio_core::net::TcpStream, C>>, [closure@src/lib.rs:179:56: 179:62]>>, mux_sender:futures::unsync::mpsc::UnboundedSender<(usize, II)>, handle:&tokio_core::reactor::Handle], std::result::Result<(), ()>>

I would love it if this type were printed something like:

futures::stream::ForEach<
  futures::stream::MapErr<
    futures::stream::Zip<
      tokio_core::net::Incoming,
      futures::stream::IterOk<
        std::ops::RangeFrom<usize>,
        std::io::Error
      >
    >,
    [closure@src/lib.rs:175:18: 175:24]
  >,
  [closure@src/lib.rs:176:19: 187:100 
    new_codec:F,
    controller:sink_splitter::Controller<
      usize,
      futures::sink::SinkMapErr<
        futures::stream::SplitSink<
          tokio_io::codec::Framed<
            tokio_core::net::TcpStream,
            C
          >
        >,
        [closure@src/lib.rs:179:56: 179:62]
      >
    >,
    mux_sender:futures::unsync::mpsc::UnboundedSender<
      (usize, II)
    >,
    handle:&tokio_core::reactor::Handle
  ],
  std::result::Result<(), ()>
>

I realize that this is not a fully-formed proposal yet. Some of the open questions that I can think of include:

  • Is there better way to approach the problem of making complex types easier to understand?
  • When should types should be displayed as multiple lines? Above, I’ve shown std::ops::RangeFrom<usize> as a single line rather than splitting it onto multiple lines because I find it easier to read as a single line.
  • How should closures be formatted?
  • Are there things besides generic types and closures that will need to be formatted specially?

Is this an idea worth pursuing and, if so, what are the right next steps?


#2

I think this is absolutely worth pursuing.

That’s a long and complex question that the style team looked at extensively. You might take a look at some of the heuristics discussed in the fmt-rfcs repository regarding “simple” expressions. But at a minimum, a type such as the one you posted that extends well beyond the normal line width should get wrapped and indented appropriately. The only question is where to break the lines.


#3

If the user specifies an impl Trait return the error messages are nicely cleaned up. However, this doesn’t work for legacy code or when the user needs to conditionally implement types. Maybe there should be an annotation to “hide” the type in error traces with impl Trait so, std::iter::Map might have this annotation so it hides all of the iterator guts.


#4

I’m imaging some Trait or const_fn 's that return a “formatter” for pretty printing this type’s name… in a more declarative format instead of nesting angle brackets… However there’re security concerns of this.


#5

@josh thanks for the suggestion! I can see that there is an extremely rich trove of information in issue #47, Define ‘short’. Currently discussion on at least #47 seems to be dormant. Is it possible that type pretty-printing could provide a way to explore some of the ideas articulated in this thread by implementing them? Whereas changing the behavior of rustfmt could be annoying, type formatting seems easier to change without worrying about backwards compatibility.


#6

One way to make these error messages easier to read would be to track the span of the call which instantiated the type. If there’s only one span, (ie. if the type is only created in one location in the program), then we can point to it in the error message. Then an error message involving an AndThen could point to the corresponding .and_then in the code.