Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Config#finalize! finalize itself recurively and allow freezing values #105

Merged
merged 2 commits into from
Apr 21, 2022

Conversation

ojab
Copy link
Contributor

@ojab ojab commented Apr 10, 2021

Fixes #104

@ojab ojab changed the title Make config#finalize! freeze itself recurively Make onfig#finalize! freeze itself recurively Apr 10, 2021
@ojab ojab changed the title Make onfig#finalize! freeze itself recurively Make Config#finalize! freeze itself recurively Apr 10, 2021
@ojab ojab marked this pull request as ready for review April 10, 2021 15:04
@ojab ojab requested a review from solnic as a code owner April 10, 2021 15:04
Copy link
Member

@solnic solnic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tackling this! I left one comment. In general, we're trying to wrap up dry-configurable 1.0.0 and since it's used in a bunch of projects (+ we want to use it in hanami 2.0) we need to 100000% sure we're not breaking anything. Some behaviors are really tricky so we probably have to check your branch against dry-schema and dry-system too before merging.

spec/integration/dry/configurable/setting_spec.rb Outdated Show resolved Hide resolved
@@ -94,6 +96,12 @@ def pristine
with(input: Undefined)
end

# @api private
def finalize!
value.is_a?(Config) ? value.finalize! : value.freeze
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that freezing values is the right way. You can't know in advance what values are, it can be anything. For instance, if it's a module or a class (a reasonable assumption for config!) then you won't be able to define new constants there. I don't this is expected.

Copy link
Contributor Author

@ojab ojab Apr 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that it would be questionable approach :)

My reasoning is that if we're finalizing config — it owns all the values and we should take that into account. IMHO

irb(main):005:0> App.config.database.dsn
=> "sqlite:memory"
irb(main):006:0> App.config.database.dsn << 'qwe'
=> "sqlite:memoryqwe"

is quite unexpected after we #finalize!, especially since documentation says # Note that finalized configs cannot be changed. & changelog says Introduce 'Configurable.finalize!' which freezes config and its dependencies.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I understand the reasoning, I would be happy to have everything immutable in the first place. But my position on this it's not up to libraries to mutate objects provided by the user. Freezing is a type of mutation so this rule applies. There are two problems: 1) it's unexpected 2) you can't opt-out of this behavior. I don't think we should be unnecessarily smart about this. We could have a warning shown when a non-frozen value passed to the config but then I guess there'd be a ton of them in a typical project, I'm not sure if this would be helpful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now semantics of #finalize! is not clearly defined, so what is the expected behavior? Freeze just config structure, so we're protected from deleting of used keys?
From the changelog entry I expected that dependencies are config values, not sure what was meant by dependencies if they're not.
Implicit mutation is certainly not a good thing, but #finalize! is explicit user's decision and if semantic would be defined as freeze all the keys & values — why not? If you want to opt-out — just don't call #finalize!.

If we still need to freeze the structure without values — can we change #freeze to freeze the config without values recursively, so the structure is set in stone, and make #finalize! to freeze also the values?

Right now #finalize! seems broken anyway, because freezing only top-level keys has doubtful usefullness.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not that simple. finalized! is called by dry-system and it's an essinatial part in the lifecycle, you can't skip it. When people install new version of dry-system witth current change they'll get new semantics for the "why not?" reason. I agree it shouldn't break things but the problem is we can't be sure and our semantics will be different from what's understood as a default in ruby: calling .freeze on arrays and hashes doesn't freeze values.

WRT recursive finalize! and freezing config structure I don't see problems with this. I agree it's not expected config structure can be changed after freezing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re dry-system, I believe it no longer depends on this hook (in master) but yeah, if the behavior changes here it'll break current dry-system releases.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I checked, it's not called. It's not clear whether it should be though. All in all, I think we could have freezing values as an option .finalize!(freeze_values: true). We can debate if it should be true or false by default but either way, it provides an opt-in/opt-out mechanism which is good. WDYT?

Copy link
Member

@timriley timriley Apr 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that dry-system doesn't call finalize! on its config at the moment. This would be worth investigating before we ship 1.0. I'll file an issue.

In fact, I'm not sure if we really finalize configs much at all across the dry-rb gems at the moment.

Having a freeze_values option to opt in or out of freezing non-Config values like a sensible way to go here 👍🏼

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue for dry-system here: dry-rb/dry-system#168

@ojab
Copy link
Contributor Author

ojab commented Apr 12, 2021

Should I just check dry-schema/dry-system locally or make draft PRs to them using this branch?

@solnic
Copy link
Member

solnic commented Apr 13, 2021

Should I just check dry-schema/dry-system locally or make draft PRs to them using this branch?

Running locally should be enough. CI will later on run specs against dry-configuration master to double-check.

Comment on lines 69 to 71
evaluate if input_defined?
value if input_defined?
Copy link
Member

@timriley timriley Apr 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see why you've removed the private #evaluate method here, to keep all the logic in #value itself, but the net result is that this line here reads a lot worse – it's just a random noun hanging out there on its own line, rather than a verb describing the outcome of the method (which, in this case, is actually about a side effect, so a verb makes sense).

Is this just a change you just made along the way because you thought it would be nice, or is it critical to your work in this PR? I think I'd prefer to see it left unchanged and have this PR focus on the freezing behavior only.

Copy link
Contributor Author

@ojab ojab Apr 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@value ||= instead of evaluated? leads to evaluating value during cloneable? check from initialize_copy which fails if finalize!(freeze_values: true) was called. I don't quite like how it looks either, added alias evaluate -> value.

@ojab ojab force-pushed the finalize_recursively branch 2 times, most recently from 8ae0eb3 to e32be04 Compare April 17, 2021 14:44
@ojab ojab changed the title Make Config#finalize! freeze itself recurively Make Config#finalize! freeze itself recurively and allow freezing values Apr 17, 2021
@ojab
Copy link
Contributor Author

ojab commented Apr 17, 2021

Sorry for delay, changed it to finalize!(freeze_values: false), checked dry-system & dry-schema from git, specs are passing.

@@ -33,9 +33,7 @@ module InstanceMethods
# Finalize the config and freeze the object
#
# @api public
def finalize!
return self if frozen?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#frozen? checks would be unreliable now and I don't think that it's performance-critical part, just drop them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ojab I wonder if this should just be idempotent or maybe if we should raise an error if a config is finalized and something tries to finalize it again 🤔

Copy link
Contributor Author

@ojab ojab Jul 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh my, missed this comment. We can raise an error, but now it's idempotent, so didn't changed it.
IMHO raising an error would only lead to people figuring out how to workaround it and will not catch any real issues.

@ojab
Copy link
Contributor Author

ojab commented Jun 9, 2021

Rebased, anything to be done to get it merged?

@ojab ojab changed the title Make Config#finalize! freeze itself recurively and allow freezing values Make Config#finalize! finalize itself recurively and allow freezing values Jun 9, 2021
@ojab ojab requested a review from solnic June 10, 2021 10:26
@ojab
Copy link
Contributor Author

ojab commented Jul 14, 2021

Gentle ping.

Copy link
Member

@solnic solnic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to just use freeze method and remove finalize!? Then Config#freeze could do its thing and behave just like Hash#freeze and THEN we could have Config#deep_freeze to make things simpler and more explicit.

This would be more flexible and possibly future-proof given that there's a discussion about adding Object#deep_freeze to Ruby.

I think getting this functionality done right is going to be crucial, that's why I'd prefer to really think it through. I should mention I'm integrating dry-configurable into rom-rb's core library and it's going to play major role there. So now all of a sudden I'm deeply interested in having proper config/settings/values freezing 😅 I used finalize! there and of course it didn't do what I assumed it would.

@ojab
Copy link
Contributor Author

ojab commented Jul 20, 2021

Then Config#freeze could do its thing and behave just like Hash#freeze

umm, do you mean with recursion or without? Since it's not recursive/behaves like Hash#freeze right now and IMHO it's pretty useless, making it recursive would make it different from built-in objects. AFAIU it's finalize! name for that exact reason.
OTOH there is no discussion in the PR and it was 4 years ago + it's useless right now, so making it recursive would be sound.

and THEN we could have Config#deep_freeze to make things simpler and more explicit
This would be more flexible and possibly future-proof given that there's a discussion about adding Object#deep_freeze to Ruby.

Heh, maybe with ractors #deep_freeze proposal would be more successful. But #deep_freeze is not in ruby yet, so there is non-zero chance to have different semantics and/or breaking change in dry-configurable when it will be.
Also #deep_freeze implies that we're freezing everything recursively, which wouldn't be the case for the nested objects in setting values (e. g. for Config.new(x: ['string']).deep_freeze only array would be frozen and 'string' wouldn't be), IMHO this would be unexpected.

And underlying implementation probably wouldn't change (two separate recursive freezers would be more complicated, I guess), so feel free to merge it as is and make finalize! private + bikeshed public names later 😜

@AMHOL
Copy link
Member

AMHOL commented Jul 21, 2021

#finalize! was just a pattern that got copied over from rom-rb into dry-container and then here in dry-configurable, wasn't much thought that went into it other than that. I just thought of #finalize! as a method to be called at the end of the setup-phaze of an application, before any requests are made, and it made sense as a good place to freeze values.

@ojab
Copy link
Contributor Author

ojab commented Jul 21, 2021

Oh, I'm not really accustomed to dry/rom ecosystem, with prior art it's perfectly logical naming without further justification.

made sense as a good place to freeze values.

part make me think that maybe we should break API and make it freeze values by default, since we're not 1.0 yet ._. (dunno if dry ecosystem follows semver (google says dry-rb/dry-rb.org#112 (comment), so API could be broken easily /shrug)).
I'd even made freezing opt-out in .configure and wherever else, but while dry-* is a bunch of separate gems, in reality it's quite tightly coupled (e. g. dry-validation pokes dry-schema @private apis) codebase that couldn't be changed independently, so it's probably wishful thinking.

@ojab ojab requested a review from solnic July 21, 2021 19:09
@ojab
Copy link
Contributor Author

ojab commented Sep 13, 2021

😿

1 similar comment
@ojab
Copy link
Contributor Author

ojab commented Dec 3, 2021

😿

@solnic
Copy link
Member

solnic commented Dec 6, 2021

😿

We'll address this eventually. I'm just going back to OSS after a 3+ months break.

Copy link
Member

@timriley timriley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for the delay on our end, @ojab.

Overall I'm happy to see this go in. I think we should try and exercise this as much as we can between now and 1.0 to make sure we're happy with the API and behaviour. Left one small request for you, @ojab, if you don't mind :)

lib/dry/configurable/setting.rb Show resolved Hide resolved
@timriley
Copy link
Member

timriley commented Jan 7, 2022

@solnic @flash-gordon Final check: we all good to merge this? I'm good with it, and given my other recent work here, I can double check this in all our dry-rb projects and then roll it into a release sometime next week.

@timriley
Copy link
Member

timriley commented Jan 7, 2022

@ojab Just noticed you pushed some changes, and I should have checked with you too, sorry! Are you OK with the current state of this PR?

@ojab
Copy link
Contributor Author

ojab commented Jan 7, 2022

Yeah, I just rebased since there was a merge conflict and I noticed it after opening/reading your comment in GH UI, no worries.

@flash-gordon
Copy link
Member

@timriley yeah I'm good with it though I don't remember all the details :)

@ojab
Copy link
Contributor Author

ojab commented Apr 20, 2022

😿

@timriley
Copy link
Member

@ojab I'll release this in the next few days!

@timriley
Copy link
Member

@ojab Thank you not only for your patch, but for your incredible patience and persistence here. I'd like to give you a trophy of some kind, but instead of hope you can settle for a new dry-configurable release 😄 Merging this one now, and will check your other PR (#131) next.

@timriley timriley merged commit 44f6e45 into dry-rb:main Apr 21, 2022
@timriley
Copy link
Member

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

finalize! should freeze config recursively
5 participants