Coding 2023-01-23

Tags:
By Max Woerner Chase

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.