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

Add a TransformPropertyValueDirectional property value walker #2760

Closed

Conversation

VenelinMartinov
Copy link
Contributor

@VenelinMartinov VenelinMartinov commented Dec 17, 2024

This PR adds a new property value walker function to the propertyvalue module: TransformPropertyValueDirectional.

This is different to TransformPropertyValue in two ways:

  • it visits nodes both on the way up and on the way down
  • the transform function can return a SkipChildrenError to prevent the recursion from walking the children of the value currently being visited.

This is necessary for #2761 to handle set value comparisons, as we need to be able to recursively walk a property value without recursing into nested sets (as nested sets can be reordered during planning).

@VenelinMartinov
Copy link
Contributor Author

VenelinMartinov commented Dec 17, 2024

Copy link

codecov bot commented Dec 17, 2024

Codecov Report

Attention: Patch coverage is 85.00000% with 3 lines in your changes missing coverage. Please review.

Project coverage is 68.61%. Comparing base (c4215af) to head (5627094).
Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
unstable/propertyvalue/propertyvalue.go 85.00% 3 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master    #2760   +/-   ##
=======================================
  Coverage   68.60%   68.61%           
=======================================
  Files         322      322           
  Lines       41223    41238   +15     
=======================================
+ Hits        28281    28294   +13     
- Misses      11347    11351    +4     
+ Partials     1595     1593    -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@VenelinMartinov VenelinMartinov marked this pull request as draft December 17, 2024 19:17
@VenelinMartinov VenelinMartinov force-pushed the vvm/move_sdkv2_set_detailed_diff_tests_to_correct_files branch from a2644af to fd71cbe Compare December 17, 2024 19:22
@VenelinMartinov VenelinMartinov force-pushed the vvm/transform_property_value_limit_descent branch from 7da1aec to dbcd3ad Compare December 17, 2024 19:22
@VenelinMartinov VenelinMartinov force-pushed the vvm/move_sdkv2_set_detailed_diff_tests_to_correct_files branch from fd71cbe to 16dbc86 Compare December 17, 2024 19:34
@VenelinMartinov VenelinMartinov force-pushed the vvm/transform_property_value_limit_descent branch from dbcd3ad to 12baad1 Compare December 17, 2024 19:34
@VenelinMartinov VenelinMartinov force-pushed the vvm/move_sdkv2_set_detailed_diff_tests_to_correct_files branch from 16dbc86 to 83dba22 Compare December 18, 2024 10:07
@VenelinMartinov VenelinMartinov force-pushed the vvm/transform_property_value_limit_descent branch from 12baad1 to 0d4b52a Compare December 18, 2024 10:07
@VenelinMartinov VenelinMartinov force-pushed the vvm/move_sdkv2_set_detailed_diff_tests_to_correct_files branch from 83dba22 to 282ac96 Compare December 18, 2024 10:14
@VenelinMartinov VenelinMartinov force-pushed the vvm/transform_property_value_limit_descent branch from 0d4b52a to 220bad9 Compare December 18, 2024 10:14
@VenelinMartinov VenelinMartinov marked this pull request as ready for review December 18, 2024 10:25
@VenelinMartinov VenelinMartinov force-pushed the vvm/move_sdkv2_set_detailed_diff_tests_to_correct_files branch from 282ac96 to 34568a1 Compare December 18, 2024 11:46
@VenelinMartinov VenelinMartinov force-pushed the vvm/transform_property_value_limit_descent branch from 220bad9 to 9ed2faa Compare December 18, 2024 11:46
Comment on lines 119 to 89
// TransformPropertyValueLimitDescent is a variant of TransformPropertyValue that allows the transformer
// to return a LimitDescentError to indicate that the recursion should not descend into the value without
// aborting the whole transformation.
func TransformPropertyValueLimitDescent(
path resource.PropertyPath,
transformer func(resource.PropertyPath, resource.PropertyValue) (resource.PropertyValue, error),
value resource.PropertyValue,
) (resource.PropertyValue, error) {
Copy link
Member

Choose a reason for hiding this comment

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

This function conflates a bunch of stuff. Traversal order is not related to supporting a LimitDescentError. The idiomatic Go API for tree walking looks like this:

var SkipChildren = skipChildren{}

type Transformer = func(
	resource.PropertyPath, resource.PropertyValue, entering bool,
) (resource.PropertyValue, error)

func TransformPropertyValue(
	path resource.PropertyPath,
	transformer Transformer,
	value resource.PropertyValue,
) (resource.PropertyValue, error)

We can then use TransformPropertyValue to construct all the functions we need:

func TransformPropertyValuePreorder(
	path resource.PropertyPath,
	transformer func(
		resource.PropertyPath, resource.PropertyValue,
	) (resource.PropertyValue, error),
	value resource.PropertyValue,
) (resource.PropertyValue, error) {
	return TransformPropertyValue(path, func(
		path resource.PropertyPath, value resource.PropertyValue, entering bool,
	) (resource.PropertyValue, error) {
		if entering { return transformer(path, value) } 
		return value, nil 
	}, value)
}

Can we keep to a general function to handle the traversal?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Traversal order is not related to supporting a LimitDescentError

Not sure I understand what you mean here - if you visit children before their parents then a visitor doesn't get a chance to return a LimitDescentError.

The API you suggested is cleaner and I have indeed seen the pattern with entering elsewhere, so happy to refactor it like that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored the module and kept just one traversal function. LMK what you think!

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 recall seeing entering somewhere but I can't recall where. This latest version is OK-ish to reason about. The boolean needs to be called entering otherwise the API causes boolean blindness.

Do you have refs to Go libs where you've seen this?

I'd suspect idiomatic Go would be further reliant on mutation :) But there's e.g. https://github.com/hashicorp/go-cty/blob/v1.4.1-0.20200414143053-d3edf31b6320/cty/walk.go#L87 that does not mutate.

Copy link
Member

Choose a reason for hiding this comment

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

I've just found https://github.com/fogfish/golem for light weekend reading, this is going to be fun but we probably don't want to go too far down that route.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

https://github.com/fogfish/golem this looks pretty fun indeed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

err := ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) {

The Markdown parsing library we use uses the entering pattern for its recursion.

@VenelinMartinov VenelinMartinov force-pushed the vvm/transform_property_value_limit_descent branch from e365498 to 1b16b10 Compare December 18, 2024 14:07
@VenelinMartinov VenelinMartinov changed the title Add a TransformPropertyValueLimitDescent property value walker Add a TransformPropertyValueDirectional property value walker Dec 18, 2024
@VenelinMartinov VenelinMartinov force-pushed the vvm/transform_property_value_limit_descent branch from 1b16b10 to 689dea2 Compare December 18, 2024 14:13
@VenelinMartinov VenelinMartinov force-pushed the vvm/move_sdkv2_set_detailed_diff_tests_to_correct_files branch from 34568a1 to b4c0769 Compare December 18, 2024 14:16
@VenelinMartinov VenelinMartinov force-pushed the vvm/transform_property_value_limit_descent branch 2 times, most recently from 70fb526 to d32594b Compare December 18, 2024 14:23
@t0yv0
Copy link
Member

t0yv0 commented Dec 18, 2024

This is generally a bad idea I think.

Once you have variations on the recursion pattern they get difficult to explain, and it is much easier to maintain an actually direct recursive function than to share a recursion pattern. And when debugging you need to step through the concrete function and the general recursion function that is not very intuitive, this is not great.

I'd inline this.

If inlining introduces too much repetitive code into the call site, I'd try one pattern I have seen work well that looks like this. In the library you have an API that looks like this:

module propertyvalue
type Tag // opaque
Unpack(PropertyValue) (Tag, []PropertyValue)
Pack(Tag, []PropertyValue) PropertyValue

This API lets you see the children of a PropertyValue, and then substitute them and get a similar-ish PropertyValue with different children. Note that this is a functional API (I'm borrowing from something I've seen in F# and Haskell), perhaps an imperative walker can be simpler but to be safe it'd need a deep clone function as well.

Continuing with this, if you have a complex transformation that's recursive, you write it like this:

func mytr(p PropettyValue) PropertyValue {
    // handle concrete cases you care about
    if pnext, ok := tr(p); ok {
       return p
    }
    // now recur for cases you don't care about, e.g. Secret, Output, etc, etc.
    tag, children := pv.Unpack(p)

    var trchildren []PropertyValue
    for _, c := range children {
        trchildren = append(trchildren, mytr(c))
    }
    return pv.Pack(tag, trchildren)
}

If you want to modify the recursion pattern like walking up or down or special casing, you then work within mytr function and don't need any more library edits.

@t0yv0
Copy link
Member

t0yv0 commented Dec 18, 2024

https://dl.acm.org/doi/10.1145/604174.604179 is a reference here. Realized in https://hackage.haskell.org/package/uniplate for example.

@t0yv0
Copy link
Member

t0yv0 commented Dec 18, 2024

https://hackage.haskell.org/package/uniplate-1.6.13/docs/Data-Generics-Uniplate-Operations.html in particular

Base automatically changed from vvm/move_sdkv2_set_detailed_diff_tests_to_correct_files to master December 18, 2024 15:18
@VenelinMartinov VenelinMartinov force-pushed the vvm/transform_property_value_limit_descent branch from d32594b to 5627094 Compare December 18, 2024 15:26
}

type TransformerDirectional = func(
resource.PropertyPath, resource.PropertyValue, bool,
Copy link
Member

Choose a reason for hiding this comment

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

Name the fields here.

@t0yv0
Copy link
Member

t0yv0 commented Dec 18, 2024

The latest version with entering is a bit complex but OK to reason about, I think I've seen such APIs in the wild, there is precedent. I'll let Ian review.

Copy link
Member

@iwahbe iwahbe left a comment

Choose a reason for hiding this comment

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

I agree with @t0yv0's comment, otherwise LGTM.

)
}

type SkipChildrenError struct{}
Copy link
Member

Choose a reason for hiding this comment

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

This needs a comment explaining what it does.

Comment on lines +81 to +85
// TransformPropertyValueDirectional is a variant of TransformPropertyValue that allows the transformer
// to visit nodes both on the way down and on the way up.
//
// The transformer can return a SkipChildrenError to indicate that the recursion should not descend into the value.
func TransformPropertyValueDirectional(
Copy link
Member

Choose a reason for hiding this comment

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

I'd rather this be called TransformPropertyValue, but we can change that in a subsequent PR.

@VenelinMartinov
Copy link
Contributor Author

superseded by #2780 as that matches the recursion pattern necessary in #2761 much better

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.

3 participants