google / starlark-go

Starlark in Go: the Starlark configuration language, implemented in Go
BSD 3-Clause "New" or "Revised" License
2.26k stars 204 forks source link

Mutable package-level vars are a poor choice for configuration #435

Closed ash2k closed 10 months ago

ash2k commented 1 year ago

Hello there! I have a project where I get this library as a transitive dependency. It's used for "stuff X". I also want to use this library for "stuff Y", unrelated to "stuff X", in the same codebase/binary. I'd like to configure the library and modules differently for different use cases. But in a few places the library uses package-level variables for "global" configuration and I cannot do what I want.

Examples:

In addition to the above, global mutable variables are sometimes abused by third-party libraries. They assume they are the center of the world and mutate such variables in other packages in their init(). No mutable globals -> no such problems.

p.s. thank you for the library! It's great!

adonovan commented 1 year ago

Yes, global state is bad, and the decision to use it here assumes there's only one Starlark client app in the address space, which isn't necessarily true. The safer choice would be to have required extensible configuration structs as parameters to Parse, Resolve, Execute, etc, which is a nuisance but creates more leeway.

I suppose we could add configurable API variants to the existing code, and make the existing API read the globals and put them in the config struct before calling the new API.

NowFunc could be easily fixed by defining a Thread-local variable called "starlark.time.now" and making the time package use that if it exists. Then any client application can override the clock on a per-thread basis.

Universe never needs to be modified, since you can add to the Predeclared set in a hygienic way.

But the Allow* flags are unfortunate, I agree.

ash2k commented 1 year ago

An alternative approach, which is probably a breaking change (or maybe this could be done as a "parallel" API) and I'm not saying it's better, is to introduce an "interpreter" object that is instantiated with all the language-affecting configuration provided (e.g. those Allow* flags). This object provides methods to spawn threads and do other things that need to (now or potentially in the future) use those global config flags (current or any future flags). This object (or something like it) can be injected into callbacks where this library calls use code. Just an idea.

ash2k commented 1 year ago

For modules and for the "interpreter" object I'd suggest to have constructors with functional options, like what gRPC-go/etc does.

adonovan commented 1 year ago

It would be reasonable to construct a parallel API that used pure functions (not globals) for configuration and accepted a FileOptions struct whose zero value embodied the defaults. Each syntax.File would retain its options so that you wouldn't need to repeat it for later stages.

I've sketched it in #477. Please download the patch and try it out and let me know if it works for you.

ash2k commented 1 year ago

Looks good to me! I'll try it next week or so. I think it is what's needed. Thank you!

ash2k commented 1 year ago

I've tried the branch, it works well for my needs. The unaddressed bit is the module (or modules?) that has a global var as config. Thanks!