GDP Rust Goals 2018-09-07

Tags:
By Max Woerner Chase

In this post:


When I started sketching out stuff for this post, I was focused on how the documentation was obviously lacking. Then I went looking for advice on writing better documentation, and I found the Rust API Guidelines. While I'd happened to internalize some of the ideas it presented, from other sources, there were some areas where my code fell particularly short, particularly in terms of the macro design.

Here's how the interface stacked up when I started looking through the list:


There is one public module, named, and four macros: name, name_impls, named_struct, and named_struct_impls. The "impls" macros are only public because it seemed to break consumers if they were private; they're just helpers for their corresponding macro. In named, we have three structs, Id, Named, PDWrapper, a trait, Name, and two functions, defn and name. Name is a marker trait for name types. Types so marked can fit into the first slot of any name-based struct, such as Named, but also any consumer can define its own structs. defn is a helper function used in the macro-generated definition of Name implementors. It is somewhat obnoxious to call on its own. name is a pretty direct port from the Haskell; evidently Rust imposes some similar constraints on implementation. It basically takes an arbitrary value, and a closure that operates on a named value, then gives the value a unique name, and passes it into the closure, then returns the result. Id is visible only because I can't figure out how to give name the bounds I want, and Id is what it actually uses in practice. PDWrapper is a little convenience newtype that someone else has probably already written, but I didn't look for it.

There's more to go over inside the macros, but I know the macros need an overhaul. Let's look through the API Guidelines.

To make things better, I'm going to:

There are a number of layers of problems with the macros. It's basically variations on "too terse", but manifesting in various ways:

My gut feeling is that each definition where the user has some input should be its own item, which, similarly to non-default fn declarations in trait definitions, should end in a semicolon. The grammar works out to something like ($attribute)* [pub[\($scope\)]] struct $name[<($param)*>]; for name and ($attribute)* [pub[\($scope\)]] struct $name(nothing, or maybe require a pair of type parameters) for named_struct. The "impl" versions should now need to be invoked explicitly, and work something like: impl<$N, $T> $name<$N, $T> { $named_struct_impls* } for named_struct, impl<($param1)*> for name<($param2)*> { $name_impls* }. For reasons, I don't think it'd work to have the bounds in the macro invocation, so just document that the macro needs them not there, and will add them. Anyway, the insides would have to look something like ($attribute)* [pub\($scope\)] fn $name $rest*, dispatch on pub/scope to specialized item macros that get something like ($attribute)* $overall_scope fn $name (type parameters as appropriate) \((params as expected for the function)\) -> $type; or something. The intermediate stuff depends on what's convenient.


Notes: all visibility modifiers that involve parentheses are stricter than without, and "crate" is the least strict. Therefore, if pub(crate) is acceptable, then any other parenthesized modifier is acceptable.

The core of the library was stripped-down enough that it was hard to be "a little wrong" in an absolute sense, without being "almost entirely wrong" in a relative sense. As such, my concerns are around ergonomics and legibility in the helper code.

I was working on this before in my free time, and I'll probably continue to do so. What these weekly projects get me is the impetus to work through some big, unpleasant part of a task. This week, that gets to be Making The Macros Work Good.


Next time, I try to improve the macro implementation.