Skip to content

Commit

Permalink
Merge pull request #90 from widmogrod/feature/december2023
Browse files Browse the repository at this point in the history
x/shape supports generics
  • Loading branch information
widmogrod authored Dec 15, 2023
2 parents 3580794 + 96b44fc commit cc8d491
Show file tree
Hide file tree
Showing 47 changed files with 1,540 additions and 2,552 deletions.
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

0 comments on commit cc8d491

Please sign in to comment.