Coding 2023-01-23
All right, I want to figure out how to redo the artifact.Output class so it's like, less janky, but it's not obvious to me how to get from here to there. Let's take a look at what it has and what I want.
@attr.dataclass(frozen=True)
class Output(Generic[T_co]):
input_accumulator: "This is basically an initial Parametric[io.Input[PathStr]] plus a fold operation over PathStr. Going forward, I want to break this into two fields."
map: "A function from PathStr to T_co. I'm going to keep this, and it should be helpful in eliminating some of the weird cruft I ended up adding."
output: "This is misnamed; it is a Parametric[Sequence[str]]. It should probably have a default value that just evaluates to the empty tuple. Its name should include 'name' somehow."
target_label: "I don't know if 'target' is the right word, but the only change to make here is to probably make it optional."
make_parent: "Here's the big change. This is currently an optional data-y value that produces various kinds of outputs. I want to replace it with a data-y type that combines a function from T_co to a Datum subclass with an InputLabel of that subclass, and has a helper function to wire that all up in the only way that really makes sense."
...
# So, the dream is something like...
TDatum = TypeVar("TDatum", bound=Datum)
# I have no complaints about the new attrs module, but I'd rather wait to
# switch until after I've stopped maintaining two topics in parallel.
# Also, maybe I need to make TDatum covariant or something.
@attr.dataclass(frozen=True)
class ExtraImplicit(Generic[T, TDatum]): # Formerly, this would have been "ExtraExtra" 🤢
label: InputLabel[TDatum]
map: Callable[[T], TDatum]
# Fairly straightforward helper functions go here-ish.
@attr.dataclass(frozen=True)
class Output(Generic[T_co]):
initial_parametric: Parametric[io.Input[PathStr]]
pathstr_accumulator: Callable[[PathStr, PathStr], PathStr]
map: Callable[[PathStr], T_co]
names: Parametric[Sequence[str]]
input_label: InputLabel[T_co] | None
extra_implicit: tuple[ExtraImplicit[T_co, Any], ...]
My main concern, looking at all of that stuff, is how to put together a sensible constructor interface. Part of me wants to just slap a basic fluent interface over top of it all and call it a day, but that doesn't feel good to the rest of me. Perhaps if I consider what the other majorly reworked class would look like...
@attr.dataclass(frozen=True)
class OutputFromInput(Generic[T_co]):
input: Input[T_co]
names: Parametric[Sequence[str]]
input_label: InputLabel[T_co] | None
extra_implicit: tuple[ExtraImplicit[T_co, Any], ...]
This implies that, from a code organization perspective, we need a class representing "how a Parametric[io.Input[T_co]] gets converted to a Parametric[io.Output[T_co]], and, with the help of a callback, then to a Parametric[command.Metadata[installer.TArgs]], although, I've just realized...
I need an additional option on this thing for handling the "make parent" case, because that's an implicit input rather than an implicit output. So, the helper class is something like...
@attr.dataclass(frozen=True)
class OutputConverter(Generic[T]):
names: Parametric[Sequence[str]]
input_label: InputLabel[T] | None
make_parent: ParentMaker[T] | None
extra_implicit: tuple[ExtraImplicit[T, Any], ...]
ParentMaker = Callable[[T], pathlib.Path]
Or something. I don't know if there's an idiomatic way to have "a boolean variable that can only take a True value if a type parameter has a specific value"... Like, this is seeming like a really tempting case for subclassing, even by my standards. I suppose the alternative would be to define a protocol based on its inputs and outputs, and write a wrapper class around the basic converter, then add a fluent method that requires T to be pathlib.Path, and then... yeah, at that point, it should Just Work.
I'm not getting this done tonight, but I'm glad I've got a pretty solid plan now.
Good night.