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

x/shape supports generics #90

Merged
merged 14 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 4 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,53 +105,7 @@ result := MyReduceDepthFirstTree(tree, func (x, y int) int {
assert.Equal(t, 10, result)
```


### 2. Leverage generated default reduction with traversal strategies (depth first, breadth first)
You should use this approach
- When you need to traverse tree in different way than a depth first, like breadth first without writing your own code
- When you need to stop traversing of a tree at some point. For example, when you want to find a value in a tree, or meet some condition.

To demonstrate different traversal strategies, we will reduce a tree to a structure that will hold not only result of sum, but also order of nodes visited

```go
// This structure will hold order of nodes visited, and resulting sum
type orderAgg struct {
Order []int
Result int
}

// This is how we define reducer function for traversal of tree

var red TreeReducer[orderAgg] = &TreeDefaultReduction[orderAgg]{
PanicOnFallback: false,
DefaultStopReduction: false,
OnLeaf: func(x *Leaf, agg orderAgg) (orderAgg, bool) {
return orderAgg{
Order: append(agg.Order, x.Value),
Result: agg.Result + x.Value,
}, false
},
}

// Dept first traversal
result := ReduceTreeDepthFirst(red, tree, orderAgg{})
assert.Equal(t, 10, result.Result)
assert.Equal(t, []int{1, 2, 3, 4}, result.Order) // notice that order is different!

// Breadth first traversal
result = ReduceTreeBreadthFirst(red, tree, orderAgg{})
assert.Equal(t, 10, result.Result)
assert.Equal(t, []int{1, 4, 2, 3}, result.Order) // notice that order is different!
```

Note:
- You can see that generated code knows how to traverse union recursively.
- You can write flat code and don't worry about recursion.
- Generator assumes that if in structure is reference to union type `Tree`, then it's recursive.
- Such code can also work on slices. You can take a look at `example/where_predicate_example.go` to see something more complex


#### 3. Implement visitor interface
#### 2. Implement visitor interface
This is most open way to traverse tree.
- You have to implement `TreeVisitor` interface that was generated for you by `mkunion` tool.
- You have to define how traversal should happen
Expand Down Expand Up @@ -484,6 +438,9 @@ go test ./...
- [x] `mkunion` breaking change: remove -variants flag. Disable possibility to recompose union types from command line. In feature type aliases could be used for this purpose.

### V1.21.x
- [x] `mkunion` breaking change: remove depth_first and breadth_first reducers generation. No replacement is planned. Use MustMatch* functions instead to write your own traversals.

### V1.22.x
- [ ] Exhaustive pattern matching checks during generation
- [ ] Allow extending (embedding) base Visitor interface with external interface
- [ ] Schema Registry should reject registration of names that are already registered!
Expand Down
48 changes: 22 additions & 26 deletions cmd/mkunion/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func main() {
&cli.StringFlag{
Name: "skip-extension",
Aliases: []string{"skip-ext"},
Value: "reducer_bfs,reducer_dfs,default_visitor,default_reducer",
Value: "",
Required: false,
},
&cli.StringFlag{
Expand Down Expand Up @@ -109,37 +109,28 @@ func main() {

helper := generators.NewHelper(options...)
union := inferred.RetrieveUnion(unionName)
if union == nil {
return fmt.Errorf("union %s not found in %s", unionName, sourcePath)
}

jsonGenerator := generators.NewDeSerJSONGenerator(union, helper)
shapeGenerator := generators.NewShapeGenerator(union, helper)
visitor := generators.NewVisitorGenerator(union, helper)
schema := generators.NewSchemaGenerator(union, helper)
depthFirstGenerator := generators.NewReducerDepthFirstGenerator(union, helper)
breadthFirstGenerator := generators.NewReducerBreadthFirstGenerator(union, helper)
defaultReduction := generators.NewReducerDefaultReductionGenerator(union, helper)
defaultVisitor := generators.NewVisitorDefaultGenerator(union, helper)

// ensures that order of generators is always the same
generatorsList := []string{
"visitor",
"reducer_dfs",
"reducer_bfs",
"default_reducer",
"default_visitor",
"schema",
"shape",
"json",
}

generators := map[string]generators.Generator{
"visitor": visitor,
"reducer_dfs": depthFirstGenerator,
"reducer_bfs": breadthFirstGenerator,
"default_reducer": defaultReduction,
"default_visitor": defaultVisitor,
"schema": schema,
"shape": shapeGenerator,
"json": jsonGenerator,
"visitor": visitor,
"schema": schema,
"shape": shapeGenerator,
"json": jsonGenerator,
}

skipExtension := strings.Split(c.String("skip-extension"), ",")
Expand Down Expand Up @@ -169,15 +160,15 @@ func main() {

b, err := g.Generate()
if err != nil {
return err
return fmt.Errorf("failed to generate %s for %s in %s: %w", name, union.Name, sourcePath, err)
}

fileName := baseName + "_" + shared.Program + "_" + strings.ToLower(visitor.Name) + "_" + name + ".go"
fileName := baseName + "_" + shared.Program + "_" + strings.ToLower(union.Name) + "_" + name + ".go"
log.Infof("writing %s", fileName)

err = os.WriteFile(path.Join(cwd, fileName), b, 0644)
if err != nil {
return err
return fmt.Errorf("failed to write %s for %s in %s: %w", name, union.Name, sourcePath, err)
}
}
} else {
Expand All @@ -190,7 +181,7 @@ func main() {

b, err := g.Generate()
if err != nil {
return err
return fmt.Errorf("failed to generate %s for %s in %s: %w", name, union.Name, sourcePath, err)
}
body.WriteString(fmt.Sprintf("//mkunion-extension:%s\n", name))
body.Write(b)
Expand All @@ -202,14 +193,14 @@ func main() {
header.WriteString(helper.RenderBufferedImport())
log.Infof(helper.RenderBufferedImport())

fileName := baseName + "_" + strings.ToLower(visitor.Name) + "_gen.go"
fileName := baseName + "_" + strings.ToLower(union.Name) + "_gen.go"
log.Infof("writing %s", fileName)

header.Write(body.Bytes())

err = os.WriteFile(path.Join(cwd, fileName), header.Bytes(), 0644)
if err != nil {
return err
return fmt.Errorf("failed to write %s for %s in %s: %w", "gen", union.Name, sourcePath, err)
}
}
}
Expand Down Expand Up @@ -259,7 +250,7 @@ func main() {
cwd,
baseName+"_match_"+strings.ToLower(derived.MatchSpec.Name)+".go"), b, 0644)
if err != nil {
return err
return fmt.Errorf("failed to write %s for %s in %s: %w", "gen", derived.MatchSpec.Name, sourcePath, err)
}

return nil
Expand Down Expand Up @@ -310,12 +301,17 @@ func main() {
tsr.AddUnion(union)
}

for _, structLike := range inferred.RetrieveStruct() {
for _, structLike := range inferred.RetrieveStructs() {
tsr.AddStruct(structLike)
}
}

return tsr.WriteToDir(c.String("output-dir"))
err := tsr.WriteToDir(c.String("output-dir"))
if err != nil {
return fmt.Errorf("failed to write to dir %s: %w", c.String("output-dir"), err)
}

return nil
},
},
},
Expand Down
4 changes: 4 additions & 0 deletions example/my-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import * as schema from "./workflow/github_com_widmogrod_mkunion_x_schema";
import {Chat} from "./Chat";
import {GenerateImage, ListWorkflowsFn} from "./workflow/github_com_widmogrod_mkunion_exammple_my-app";

// type ListOf<T> = {}
// type ListOf2<T1,T2> = {}
// type B<T1,T2> = ListOf2<T1,T2>
// export type P = ListOf2<ListOf<any>, ListOf2<number, other>>
function flowCreate(flow: workflow.Flow) {
return fetch('http://localhost:8080/flow', {
method: 'POST',
Expand Down
118 changes: 1 addition & 117 deletions example/tree_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,7 @@ func TestTreeSumUsingReducer(t *testing.T) {
},
}

var red TreeReducer[orderAgg] = &TreeDefaultReduction[orderAgg]{
PanicOnFallback: false,
DefaultStopReduction: false,
OnLeaf: func(x *Leaf, agg orderAgg) (orderAgg, bool) {
return orderAgg{
Order: append(agg.Order, x.Value),
Result: agg.Result + x.Value,
}, false
},
}

result := ReduceTreeDepthFirst(red, tree, orderAgg{})
assert.Equal(t, 10, result.Result)
assert.Equal(t, []int{1, 2, 3, 4}, result.Order)

result = ReduceTreeBreadthFirst(red, tree, orderAgg{})
assert.Equal(t, 10, result.Result)
assert.Equal(t, []int{1, 4, 2, 3}, result.Order)

result = ReduceTreeF(tree, func(x int, agg orderAgg) orderAgg {
result := ReduceTreeF(tree, func(x int, agg orderAgg) orderAgg {
return orderAgg{
Order: append(agg.Order, x),
Result: agg.Result + x,
Expand All @@ -89,25 +70,6 @@ func ReduceTreeF[B any](x Tree, f func(int, B) B, init B) B {
)
}

func TestTreeNonExhaustive(t *testing.T) {
tree := &Branch{
L: &Leaf{Value: 1},
R: &Branch{
L: &Leaf{Value: 2},
R: &Leaf{Value: 3},
},
}

n := TreeDefaultVisitor[int]{
Default: 10,
OnLeaf: func(x *Leaf) int {
return x.Value
},
}

assert.Equal(t, 10, tree.AcceptTree(&n))
}

func TestTreeSchema(t *testing.T) {
tree := &Branch{
L: &Leaf{Value: 1},
Expand Down Expand Up @@ -140,81 +102,3 @@ func TestMyTriesMatchR0(t *testing.T) {
},
)
}

/*
Function reduction is faster than depth first, and depth first is faster than breadth first.
But function vs depth first is not that big difference.
BenchmarkReduceTreeBreadthFirst-8 1000000 1096 ns/op 1024 B/op 13 allocs/op
BenchmarkReduceTreeBreadthFirst-8 1000000 1090 ns/op 1024 B/op 13 allocs/op
BenchmarkReduceTreeBreadthFirst-8 1000000 1090 ns/op 1024 B/op 13 allocs/op
BenchmarkReduceTreeBreadthFirst-8 1000000 1086 ns/op 1024 B/op 13 allocs/op
BenchmarkReduceTreeBreadthFirst-8 1000000 1092 ns/op 1024 B/op 13 allocs/op
BenchmarkReduceTreeDepthFirst-8 20942499 57.10 ns/op 32 B/op 1 allocs/op
BenchmarkReduceTreeDepthFirst-8 20936881 57.75 ns/op 32 B/op 1 allocs/op
BenchmarkReduceTreeDepthFirst-8 20820109 57.58 ns/op 32 B/op 1 allocs/op
BenchmarkReduceTreeDepthFirst-8 20699052 57.60 ns/op 32 B/op 1 allocs/op
BenchmarkReduceTreeDepthFirst-8 20935268 57.53 ns/op 32 B/op 1 allocs/op
BenchmarkReduceTreeF-8 26585260 44.96 ns/op 0 B/op 0 allocs/op
BenchmarkReduceTreeF-8 26453421 45.33 ns/op 0 B/op 0 allocs/op
BenchmarkReduceTreeF-8 26363992 45.13 ns/op 0 B/op 0 allocs/op
BenchmarkReduceTreeF-8 26396396 48.38 ns/op 0 B/op 0 allocs/op
BenchmarkReduceTreeF-8 26441884 45.18 ns/op 0 B/op 0 allocs/op
*/
var (
benchTreeResult int
benchTreeExpected = 10
benchTree = &Branch{
L: &Leaf{Value: 1},
R: &Branch{
L: &Branch{
L: &Leaf{Value: 2},
R: &Leaf{Value: 3},
},
R: &Leaf{Value: 4},
},
}

benchTreeReducer TreeReducer[int] = &TreeDefaultReduction[int]{
PanicOnFallback: false,
DefaultStopReduction: false,
OnLeaf: func(x *Leaf, agg int) (int, bool) {
return agg + x.Value, false
},
}
)

func BenchmarkReduceTreeBreadthFirst(b *testing.B) {
var r int
for i := 0; i < b.N; i++ {
r = ReduceTreeBreadthFirst(benchTreeReducer, benchTree, 0)
if r != benchTreeExpected {
b.Fail()
}
}
benchTreeResult = r
}

func BenchmarkReduceTreeDepthFirst(b *testing.B) {
var r int
for i := 0; i < b.N; i++ {
r = ReduceTreeDepthFirst(benchTreeReducer, benchTree, 0)
if r != benchTreeExpected {
b.Fail()
}
}
benchTreeResult = r
}

func BenchmarkReduceTreeF(b *testing.B) {
var r int
for i := 0; i < b.N; i++ {
r = ReduceTreeF(benchTree, func(x int, agg int) int {
return agg + x
}, 0)
if r != benchTreeExpected {
b.Fail()
}
}
benchTreeResult = r
}
Loading