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

Introduce 'quip' data building helpers. #134

Merged
merged 3 commits into from
Jan 8, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
152 changes: 152 additions & 0 deletions fluent/quip/quip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// quip is a package of quick ipld patterns.
//
// Most quip functions take a pointer to an error as their first argument.
// This has two purposes: if there's an error there, the quip function will do nothing;
// and if the quip function does something and creates an error, it puts it there.
// The effect of this is that most logic can be written very linearly.
//
// quip functions can be used to increase brevity without worrying about performance costs.
// None of the quip functions cause additional allocations in the course of their work.
// Benchmarks indicate no measurable speed penalties versus longhand manual error checking.
//
// This package is currently considered experimental, and may change.
// Feel free to use it, but be advised that code using it may require frequent
// updating until things settle; naming conventions in this package may be
// revised with relatively little warning.
package quip

import (
"github.com/ipld/go-ipld-prime"
)

// TODO:REVIEW: a few things about this outline and its symbol naming:
// - consistency/nomenclature check:
// - fluent package uses "BuildMap" at package scope. And then "CreateMap" on its NB facade (which takes a callback param). It's calling "NodeBuilder.BeginMap" internally, of course. (is fluent's current choice odd and deserving of revising?)
// - the "Build" vs "Create" prefixes here indicate methods that start with a builder or prototype and return a full node, vs methods that work on assemblers. (the fluent package doesn't have any methods that *don't* take callbacks, so there's no name component to talk about that.)
// - here currently we've used "BeginMap" for something that returns a MapAssembler (consistent with ipld.NB), and "BuildMap" takes a callback.
// - ditto the above bullet tree for List instead of Map.
// - many of these methods could have varying parameter types: ipld.Node forms, interface{} and dtrt forms, and callback-taking-assembler forms. They all need unique names.
// - this is the case for values, and similarly for keys (though for keys the most important ones are probably string | ipld.Node | pathSegment). The crossproduct there is sizable.
// - do we want to fill out this matrix completely?
// - what naming convention can we use to make this consistent?
// - do we actually want the non-callback/returns-MapAssembler BeginMap style to be something this package bothers to export?
// - hard to imagine actually wanting to use these.
// - since it turns out that the callbacks *do* get optimized quite reasonably by the compiler in common cases, there's no reason to avoid them.

// TODO: a few notable gaps in what's provided, which should improve terseness even more:
// - we don't have top-level functions for doing a full Build returning a Node (and saving you the np.NewBuilder preamble and nb.Build postamble). see naming issues discussion.
// - we don't have great shorthand for scalar assignment at the leaves. current best is a composition like `quip.AbsorbError(&err, na.AssignString(x))`.
// - do we want to make an `quip.Assign{Kind}(*error, *ipld.NodeAssembler, {kindPrimitive})` for each kind? seems like a lot of symbols.
// - there's also generally a whole `quip.MapEntry(... {...})` or `ListEntry` clause around *that*, with the single Absorb+Assign line in the middle. that's boilerplate that needs trimming as well.
// - so... twice as many Assign helpers, because it's kinda necessary to specialize them to map and list assemblers too? uff.

func AbsorbError(e *error, err error) {
if *e != nil {
return
}
if err != nil {
*e = err
}
}

func BeginMap(e *error, na ipld.NodeAssembler, sizeHint int64) ipld.MapAssembler {
if *e != nil {
return nil
}
ma, err := na.BeginMap(sizeHint)
if err != nil {
*e = err
return nil
}
return ma
}

func BuildMap(e *error, na ipld.NodeAssembler, sizeHint int64, fn func(ma ipld.MapAssembler)) {
if *e != nil {
return
}
ma, err := na.BeginMap(sizeHint)
if err != nil {
*e = err
return
}
fn(ma)
if *e != nil {
return
}
*e = ma.Finish()
}

func MapEntry(e *error, ma ipld.MapAssembler, k string, fn func(va ipld.NodeAssembler)) {
if *e != nil {
return
}
va, err := ma.AssembleEntry(k)
if err != nil {
*e = err
return
}
fn(va)
}

func BeginList(e *error, na ipld.NodeAssembler, sizeHint int64) ipld.ListAssembler {
if *e != nil {
return nil
}
la, err := na.BeginList(sizeHint)
if err != nil {
*e = err
return nil
}
return la
}

func BuildList(e *error, na ipld.NodeAssembler, sizeHint int64, fn func(la ipld.ListAssembler)) {
if *e != nil {
return
}
la, err := na.BeginList(sizeHint)
if err != nil {
*e = err
return
}
fn(la)
if *e != nil {
return
}
*e = la.Finish()
}

func ListEntry(e *error, la ipld.ListAssembler, fn func(va ipld.NodeAssembler)) {
if *e != nil {
return
}
fn(la.AssembleValue())
}

func CopyRange(e *error, la ipld.ListAssembler, src ipld.Node, start, end int64) {
if *e != nil {
return
}
if start >= src.Length() {
return
}
if end < 0 {
end = src.Length()
}
if end < start {
return
}
for i := start; i < end; i++ {
n, err := src.LookupByIndex(i)
if err != nil {
*e = err
return
}
if err := la.AssembleValue().AssignNode(n); err != nil {
*e = err
return
}
}
return
}
245 changes: 245 additions & 0 deletions fluent/quip/quip_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package quip_test

import (
"strings"
"testing"

"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/ipld/go-ipld-prime/fluent"
"github.com/ipld/go-ipld-prime/fluent/quip"
basicnode "github.com/ipld/go-ipld-prime/node/basic"
)

func BenchmarkQuip(b *testing.B) {
var n ipld.Node
var err error
for i := 0; i < b.N; i++ {
n, err = f1()
}
_ = n
if err != nil {
b.Fatal(err)
}
}

func BenchmarkUnmarshal(b *testing.B) {
var n ipld.Node
var err error
serial := `[{
"destination": "/",
"type": "overlay",
"source": "none",
"options": [
"lowerdir=/",
"upperdir=/tmp/overlay-root/upper",
"workdir=/tmp/overlay-root/work"
]
}]`
r := strings.NewReader(serial)
for i := 0; i < b.N; i++ {
nb := basicnode.Prototype.Any.NewBuilder()
err = dagjson.Decoder(nb, r)
n = nb.Build()
r.Reset(serial)
}
_ = n
if err != nil {
b.Fatal(err)
}
}

func BenchmarkFluent(b *testing.B) {
var n ipld.Node
var err error
for i := 0; i < b.N; i++ {
n, err = fluent.BuildList(basicnode.Prototype.Any, -1, func(la fluent.ListAssembler) {
la.AssembleValue().CreateMap(4, func(ma fluent.MapAssembler) {
ma.AssembleEntry("destination").AssignString("/")
ma.AssembleEntry("type").AssignString("overlay")
ma.AssembleEntry("source").AssignString("none")
ma.AssembleEntry("options").CreateList(-1, func(la fluent.ListAssembler) {
la.AssembleValue().AssignString("lowerdir=" + "/")
la.AssembleValue().AssignString("upperdir=" + "/tmp/overlay-root/upper")
la.AssembleValue().AssignString("workdir=" + "/tmp/overlay-root/work")
})
})
})
}
_ = n
if err != nil {
b.Fatal(err)
}
}

func BenchmarkReflect(b *testing.B) {
var n ipld.Node
var err error
val := []interface{}{
map[string]interface{}{
"destination": "/",
"type": "overlay",
"source": "none",
"options": []string{
"lowerdir=/",
"upperdir=/tmp/overlay-root/upper",
"workdir=/tmp/overlay-root/work",
},
},
}
for i := 0; i < b.N; i++ {
n, err = fluent.Reflect(basicnode.Prototype.Any, val)
}
_ = n
if err != nil {
b.Fatal(err)
}
}

func BenchmarkReflectIncludingInitialization(b *testing.B) {
var n ipld.Node
var err error
for i := 0; i < b.N; i++ {
n, err = fluent.Reflect(basicnode.Prototype.Any, []interface{}{
map[string]interface{}{
"destination": "/",
"type": "overlay",
"source": "none",
"options": []string{
"lowerdir=/",
"upperdir=/tmp/overlay-root/upper",
"workdir=/tmp/overlay-root/work",
},
},
})
}
_ = n
if err != nil {
b.Fatal(err)
}
}

func BenchmarkAgonizinglyBare(b *testing.B) {
var n ipld.Node
var err error
for i := 0; i < b.N; i++ {
n, err = fab()
}
_ = n
if err != nil {
b.Fatal(err)
}
}

func fab() (ipld.Node, error) {
nb := basicnode.Prototype.Any.NewBuilder()
la1, err := nb.BeginList(-1)
if err != nil {
return nil, err
}
ma, err := la1.AssembleValue().BeginMap(4)
if err != nil {
return nil, err
}
va, err := ma.AssembleEntry("destination")
if err != nil {
return nil, err
}
err = va.AssignString("/")
if err != nil {
return nil, err
}
va, err = ma.AssembleEntry("type")
if err != nil {
return nil, err
}
err = va.AssignString("overlay")
if err != nil {
return nil, err
}
va, err = ma.AssembleEntry("source")
if err != nil {
return nil, err
}
err = va.AssignString("none")
if err != nil {
return nil, err
}
va, err = ma.AssembleEntry("options")
if err != nil {
return nil, err
}
la2, err := va.BeginList(-4)
if err != nil {
return nil, err
}
err = la2.AssembleValue().AssignString("lowerdir=" + "/")
if err != nil {
return nil, err
}
err = la2.AssembleValue().AssignString("upperdir=" + "/tmp/overlay-root/upper")
if err != nil {
return nil, err
}
err = la2.AssembleValue().AssignString("workdir=" + "/tmp/overlay-root/work")
if err != nil {
return nil, err
}
err = la2.Finish()
if err != nil {
return nil, err
}
err = ma.Finish()
if err != nil {
return nil, err
}
err = la1.Finish()
if err != nil {
return nil, err
}
return nb.Build(), nil
}

func f1() (_ ipld.Node, err error) {
nb := basicnode.Prototype.Any.NewBuilder()
quip.BuildList(&err, nb, -1, func(la ipld.ListAssembler) {
f2(la.AssembleValue(),
"/",
"overlay",
"none",
[]string{
"lowerdir=" + "/",
"upperdir=" + "/tmp/overlay-root/upper",
"workdir=" + "/tmp/overlay-root/work",
},
)
})
if err != nil {
return nil, err
}
return nb.Build(), nil
}

func f2(na ipld.NodeAssembler, a string, b string, c string, d []string) (err error) {
quip.BuildMap(&err, na, 4, func(ma ipld.MapAssembler) {
quip.MapEntry(&err, ma, "destination", func(va ipld.NodeAssembler) {
quip.AbsorbError(&err, va.AssignString(a))
})
quip.MapEntry(&err, ma, "type", func(va ipld.NodeAssembler) {
quip.AbsorbError(&err, va.AssignString(b))
})
quip.MapEntry(&err, ma, "source", func(va ipld.NodeAssembler) {
quip.AbsorbError(&err, va.AssignString(c))
})
quip.MapEntry(&err, ma, "options", func(va ipld.NodeAssembler) {
quip.BuildList(&err, va, int64(len(d)), func(la ipld.ListAssembler) {
for i := range d {
quip.ListEntry(&err, la, func(va ipld.NodeAssembler) {
quip.AbsorbError(&err, va.AssignString(d[i]))
})
}
})
})
})
return
}
Loading