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

fluent: add qp, a different spin on quip #138

Merged
merged 1 commit into from
Jan 18, 2021
Merged

fluent: add qp, a different spin on quip #138

merged 1 commit into from
Jan 18, 2021

Conversation

mvdan
Copy link
Contributor

@mvdan mvdan commented Jan 11, 2021

This is what I came up with, building on top of Eric's quip. I don't
want to waste too much time naming this, and I like two-letter package
names in place of dot-imports, so "qp" seems good enough for now. They
are the "strong" consonants when one says "Quick iPld".

First, move the benchmarks comparing all fluent packages to the root
fluent package, to keep things a bit more tidy.

Second, make all the benchmarks report their allocation stats, without
having to always remember to use the -benchmem flag.

Third, add a qp benchmark.

Fourth, notice a couple of potential bugs in the quip benchmarks, and
add TODOs for them.

Finally, add the qp API. It differs from quip in a few external ways:

  1. No error pointers. Instead, it uses panics which are recovered at the
    top-level API layer. This reduces verbosity, removes the "forgot to
    handle an error" type of mistake, and does not affect performance
    thanks to the defers being statically allocated in the stack.

  2. Supposed better composition. For example, one can use MapEntry along
    with Map to have a map inside another map. In contrast, quip requires
    either an extra layer of func literals, or extra API like
    AssignMapEntryString.

  3. Thanks to the points above, the API is significantly smaller. Note
    that some helper APIs like Bool are missing, but even when added, qp
    should expose about half the API funcs taht quip does.

This is the first proof of concept. I'll probably finish adding the rest
of the API helpers when I find the first use case for qp.

Benchmark numbers, with perflock and benchstat on my i5-8350u laptop:

name                              time/op
Quip-8                            1.39µs ± 1%
QuipWithoutScalarFuncs-8          1.42µs ± 2%
Qp-8                              1.46µs ± 2%

name                              alloc/op
Quip-8                              912B ± 0%
QuipWithoutScalarFuncs-8            912B ± 0%
Qp-8                                912B ± 0%

name                              allocs/op
Quip-8                              18.0 ± 0%
QuipWithoutScalarFuncs-8            18.0 ± 0%
Qp-8                                18.0 ± 0%

This is what I came up with, building on top of Eric's quip. I don't
want to waste too much time naming this, and I like two-letter package
names in place of dot-imports, so "qp" seems good enough for now. They
are the "strong" consonants when one says "Quick iPld".

First, move the benchmarks comparing all fluent packages to the root
fluent package, to keep things a bit more tidy.

Second, make all the benchmarks report their allocation stats, without
having to always remember to use the -benchmem flag.

Third, add a qp benchmark.

Fourth, notice a couple of potential bugs in the quip benchmarks, and
add TODOs for them.

Finally, add the qp API. It differs from quip in a few external ways:

1) No error pointers. Instead, it uses panics which are recovered at the
   top-level API layer. This reduces verbosity, removes the "forgot to
   handle an error" type of mistake, and does not affect performance
   thanks to the defers being statically allocated in the stack.

2) Supposed better composition. For example, one can use MapEntry along
   with Map to have a map inside another map. In contrast, quip requires
   either an extra layer of func literals, or extra API like
   AssignMapEntryString.

3) Thanks to the points above, the API is significantly smaller. Note
   that some helper APIs like Bool are missing, but even when added, qp
   should expose about half the API funcs taht quip does.

This is the first proof of concept. I'll probably finish adding the rest
of the API helpers when I find the first use case for qp.

Benchmark numbers, with perflock and benchstat on my i5-8350u laptop:

	name                              time/op
	Quip-8                            1.39µs ± 1%
	QuipWithoutScalarFuncs-8          1.42µs ± 2%
	Qp-8                              1.46µs ± 2%

	name                              alloc/op
	Quip-8                              912B ± 0%
	QuipWithoutScalarFuncs-8            912B ± 0%
	Qp-8                                912B ± 0%

	name                              allocs/op
	Quip-8                              18.0 ± 0%
	QuipWithoutScalarFuncs-8            18.0 ± 0%
	Qp-8                                18.0 ± 0%
@mvdan mvdan requested a review from warpfork January 11, 2021 16:56
@mvdan
Copy link
Contributor Author

mvdan commented Jan 11, 2021

Here is the diff between quip's example and qp's, to get an idea of how the differences look like in practice:

 func Example() {
-	var err error
-	n := quip.BuildMap(&err, basicnode.Prototype.Any, 4, func(ma ipld.MapAssembler) {
-		quip.AssignMapEntryString(&err, ma, "some key", "some value")
-		quip.AssignMapEntryString(&err, ma, "another key", "another value")
-		quip.AssembleMapEntry(&err, ma, "nested map", func(na ipld.NodeAssembler) {
-			quip.AssembleMap(&err, na, 2, func(ma ipld.MapAssembler) {
-				quip.AssignMapEntryString(&err, ma, "deeper entries", "deeper values")
-				quip.AssignMapEntryString(&err, ma, "more deeper entries", "more deeper values")
-			})
-		})
-		quip.AssembleMapEntry(&err, ma, "nested list", func(na ipld.NodeAssembler) {
-			quip.AssembleList(&err, na, 2, func(la ipld.ListAssembler) {
-				quip.AssignListEntryInt(&err, la, 1)
-				quip.AssignListEntryInt(&err, la, 2)
-			})
-		})
+	n, err := qp.BuildMap(basicnode.Prototype.Any, 4, func(ma ipld.MapAssembler) {
+		qp.MapEntry(ma, "some key", qp.String("some value"))
+		qp.MapEntry(ma, "another key", qp.String("another value"))
+		qp.MapEntry(ma, "nested map", qp.Map(2, func(ma ipld.MapAssembler) {
+			qp.MapEntry(ma, "deeper entries", qp.String("deeper values"))
+			qp.MapEntry(ma, "more deeper entries", qp.String("more deeper values"))
+		}))
+		qp.MapEntry(ma, "nested list", qp.List(2, func(la ipld.ListAssembler) {
+			qp.ListEntry(la, qp.Int(1))
+			qp.ListEntry(la, qp.Int(2))
+		}))
 	})

@mvdan
Copy link
Contributor Author

mvdan commented Jan 11, 2021

Something that's also a product of not using error pointers is that the top-level API is a bit more Go-like, returning a node, err instead of just node and the error via the previous var err error.

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.

2 participants