doc: libevm/pseudo package comments and improved readability

This commit is contained in:
Arran Schlosberg 2024-08-23 14:40:43 +01:00
parent 35c8988d4f
commit a796dcfeb0
No known key found for this signature in database
GPG key ID: 8A30F7E4344B4EF3
4 changed files with 134 additions and 71 deletions

View file

@ -1,4 +1,15 @@
// Package pseudo ...
// Package pseudo provides a bridge between generic and non-generic code via
// pseudo-types and pseudo-values. With careful usage, there is minimal
// reduction in type safety.
//
// Adding generic type parameters to anything (e.g. struct, function, etc)
// "pollutes" all code that uses the generic type. Refactoring all uses isn't
// always feasible, and a [Type] acts as an intermediate fix. Although their
// constructors are generic, they are not, and they are instead coupled with a
// generic [Value] that SHOULD be used for access.
//
// Packages typically SHOULD NOT expose a [Type] and SHOULD instead provide
// users with a type-safe [Value].
package pseudo
import (
@ -6,63 +17,101 @@ import (
"fmt"
)
// Zero ...
func Zero[T any]() (*Type, *Value[T]) {
var x T
return From[T](x)
}
// From ...
func From[T any](x T) (*Type, *Value[T]) {
t := &Type{
val: &concrete[T]{
val: x,
},
}
return t, NewValueUnsafe[T](t)
}
// OnlyType ...
func OnlyType[T any](t *Type, _ *Value[T]) *Type {
return t
}
// A Type ...
// A Type wraps a strongly-typed value without exposing information about its
// type. It can be used in lieu of a generic field / parameter.
type Type struct {
val value
}
func (t *Type) Interface() any { return t.val.get() }
// func (t *Type) Set(v any) error { return t.val.Set(v) }
// func (t *Type) MustSet(v any) { t.val.MustSet(v) }
func (t *Type) MarshalJSON() ([]byte, error) { return t.val.MarshalJSON() }
func (t *Type) UnmarshalJSON(b []byte) error { return t.val.UnmarshalJSON(b) }
var (
_ json.Marshaler = (*Type)(nil)
_ json.Unmarshaler = (*Type)(nil)
)
func NewValueUnsafe[T any](t *Type) *Value[T] {
return &Value[T]{t: t}
}
func NewValue[T any](t *Type) (*Value[T], error) {
var x T
if !t.val.canSetTo(x) {
return nil, fmt.Errorf("cannot create *Accessor[%T] with *Type carrying %T", x, t.val.get())
}
return NewValueUnsafe[T](t), nil
}
// A Value provides strongly-typed access to the payload carried by a [Type].
type Value[T any] struct {
t *Type
}
func (a *Value[T]) Get() T { return a.t.val.get().(T) }
// A Pseudo type couples a [Type] and a [Value]. If returned by a constructor
// from this package, both wrap the same payload.
type Pseudo[T any] struct {
Type *Type
Value *Value[T]
}
// TypeAndValue is a convenience function for splitting the contents of `p`,
// typically at construction.
func (p *Pseudo[T]) TypeAndValue() (*Type, *Value[T]) {
return p.Type, p.Value
}
// From returns a Pseudo[T] constructed from `v`.
func From[T any](v T) *Pseudo[T] {
t := &Type{
val: &concrete[T]{
val: v,
},
}
return &Pseudo[T]{t, MustNewValue[T](t)}
}
// Zero is equivalent to [From] called with the [zero value] of type `T`. Note
// that pointers, slices, maps, etc. will therefore be nil.
//
// [zero value]: https://go.dev/tour/basics/12
func Zero[T any]() *Pseudo[T] {
var x T
return From[T](x)
}
// Interface returns the wrapped value as an `any`, equivalent to
// [reflect.Value.Interface]. Prefer [Value.Get].
func (t *Type) Interface() any { return t.val.get() }
// NewValue constructs a [Value] from a [Type], first confirming that `t` wraps
// a payload of type `T`.
func NewValue[T any](t *Type) (*Value[T], error) {
var x T
if !t.val.canSetTo(x) {
return nil, fmt.Errorf("cannot create *Value[%T] with *Type carrying %T", x, t.val.get())
}
return &Value[T]{t}, nil
}
// MustNewValue is equivalent to [NewValue] except that it panics instead of
// returning an error.
func MustNewValue[T any](t *Type) *Value[T] {
v, err := NewValue[T](t)
if err != nil {
panic(err)
}
return v
}
// Get returns the value.
func (a *Value[T]) Get() T { return a.t.val.get().(T) }
// Set sets the value.
func (a *Value[T]) Set(v T) { a.t.val.mustSet(v) }
// MarshalJSON implements the [json.Marshaler] interface.
func (t *Type) MarshalJSON() ([]byte, error) { return t.val.MarshalJSON() }
// UnmarshalJSON implements the [json.Unmarshaler] interface.
func (t *Type) UnmarshalJSON(b []byte) error { return t.val.UnmarshalJSON(b) }
// MarshalJSON implements the [json.Marshaler] interface.
func (v *Value[T]) MarshalJSON() ([]byte, error) { return v.t.MarshalJSON() }
// UnmarshalJSON implements the [json.Unmarshaler] interface.
func (v *Value[T]) UnmarshalJSON(b []byte) error { return v.t.UnmarshalJSON(b) }
var _ = []interface {
json.Marshaler
json.Unmarshaler
}{
(*Type)(nil),
(*Value[struct{}])(nil),
(*concrete[struct{}])(nil),
}
// A value is a non-generic wrapper around a [concrete] struct.
type value interface {
get() any
canSetTo(any) bool
@ -84,11 +133,14 @@ func (c *concrete[T]) canSetTo(v any) bool {
return ok
}
type InvalidTypeError[T any] struct {
// An invalidTypeError is returned by [conrete.set] if the value is incompatible
// with its type. This should never leave this package and exists only to
// provide precise testing of unhappy paths.
type invalidTypeError[T any] struct {
SetTo any
}
func (e *InvalidTypeError[T]) Error() string {
func (e *invalidTypeError[T]) Error() string {
var t T
return fmt.Sprintf("cannot set %T to %T", t, e.SetTo)
}
@ -96,7 +148,9 @@ func (e *InvalidTypeError[T]) Error() string {
func (c *concrete[T]) set(v any) error {
vv, ok := v.(T)
if !ok {
return &InvalidTypeError[T]{SetTo: v}
// Other invariants in this implementation (aim to) guarantee that this
// will never happen.
return &invalidTypeError[T]{SetTo: v}
}
c.val = vv
return nil

View file

@ -15,7 +15,7 @@ func TestType(t *testing.T) {
testType(
t, "From[uint](314159)",
func() (*Type, *Value[uint]) {
func() *Pseudo[uint] {
return From[uint](314159)
},
314159, 0, struct{}{},
@ -24,20 +24,20 @@ func TestType(t *testing.T) {
testType(t, "nil pointer", Zero[*float64], (*float64)(nil), new(float64), 0)
}
func testType[T any](t *testing.T, name string, ctor func() (*Type, *Value[T]), init T, setTo T, invalid any) {
func testType[T any](t *testing.T, name string, ctor func() *Pseudo[T], init T, setTo T, invalid any) {
t.Run(name, func(t *testing.T) {
typ, val := ctor()
typ, val := ctor().TypeAndValue()
assert.Equal(t, init, val.Get())
val.Set(setTo)
assert.Equal(t, setTo, val.Get())
t.Run("set to invalid type", func(t *testing.T) {
wantErr := &InvalidTypeError[T]{SetTo: invalid}
wantErr := &invalidTypeError[T]{SetTo: invalid}
assertError := func(t *testing.T, err any) {
t.Helper()
switch err := err.(type) {
case *InvalidTypeError[T]:
case *invalidTypeError[T]:
assert.Equal(t, wantErr, err)
default:
t.Errorf("got error %v; want %v", err, wantErr)
@ -60,9 +60,20 @@ func testType[T any](t *testing.T, name string, ctor func() (*Type, *Value[T]),
buf, err := json.Marshal(typ)
require.NoError(t, err)
got, gotVal := Zero[T]()
got, gotVal := Zero[T]().TypeAndValue()
require.NoError(t, json.Unmarshal(buf, &got))
assert.Equal(t, val.Get(), gotVal.Get())
})
})
}
func ExamplePseudo_TypeAndValue() {
typ, val := From("hello").TypeAndValue()
// But, if only one is needed:
typ = From("world").Type
val = From("this isn't coupled to the Type").Value
_ = typ
_ = val
}

View file

@ -56,12 +56,12 @@ type ExtraPayloadGetter[C any, R any] struct{}
// FromChainConfig ...
func (ExtraPayloadGetter[C, R]) FromChainConfig(c *ChainConfig) *C {
return pseudo.NewValueUnsafe[*C](c.extraPayload()).Get()
return pseudo.MustNewValue[*C](c.extraPayload()).Get()
}
// FromRules ...
func (ExtraPayloadGetter[C, R]) FromRules(r *Rules) *R {
return pseudo.NewValueUnsafe[*R](r.extraPayload()).Get()
return pseudo.MustNewValue[*R](r.extraPayload()).Get()
}
func mustBeStruct[T any]() {
@ -160,19 +160,17 @@ func (r *Rules) extraPayload() *pseudo.Type {
return r.extra
}
func (Extras[C, R]) nilForChainConfig() *pseudo.Type { return pseudo.OnlyType(pseudo.Zero[*C]()) }
func (Extras[C, R]) nilForRules() *pseudo.Type { return pseudo.OnlyType(pseudo.Zero[*R]()) }
func (Extras[C, R]) nilForChainConfig() *pseudo.Type { return pseudo.Zero[*C]().Type }
func (Extras[C, R]) nilForRules() *pseudo.Type { return pseudo.Zero[*R]().Type }
func (*Extras[C, R]) newForChainConfig() *pseudo.Type {
var x C
return pseudo.OnlyType(pseudo.From(&x))
return pseudo.From(&x).Type
}
func (e *Extras[C, R]) newForRules(c *ChainConfig, r *Rules, blockNum *big.Int, isMerge bool, timestamp uint64) *pseudo.Type {
if e.NewForRules == nil {
return e.nilForRules()
}
return pseudo.OnlyType(
pseudo.From(e.NewForRules(c, r, c.extra.Interface().(*C), blockNum, isMerge, timestamp)),
)
return pseudo.From(e.NewForRules(c, r, c.extra.Interface().(*C), blockNum, isMerge, timestamp)).Type
}

View file

@ -56,9 +56,9 @@ func TestRegisterExtras(t *testing.T) {
},
})
},
ccExtra: pseudo.OnlyType(pseudo.From(&ccExtraA{
ccExtra: pseudo.From(&ccExtraA{
A: "hello",
})),
}).Type,
wantRulesExtra: &rulesExtraA{
A: "hello",
},
@ -68,9 +68,9 @@ func TestRegisterExtras(t *testing.T) {
register: func() {
RegisterExtras(Extras[ccExtraB, rulesExtraB]{})
},
ccExtra: pseudo.OnlyType(pseudo.From(&ccExtraB{
ccExtra: pseudo.From(&ccExtraB{
B: "world",
})),
}).Type,
wantRulesExtra: (*rulesExtraB)(nil),
},
{
@ -78,9 +78,9 @@ func TestRegisterExtras(t *testing.T) {
register: func() {
RegisterExtras(Extras[rawJSON, struct{}]{})
},
ccExtra: pseudo.OnlyType(pseudo.From(&rawJSON{
ccExtra: pseudo.From(&rawJSON{
RawMessage: []byte(`"hello, world"`),
})),
}).Type,
wantRulesExtra: (*struct{})(nil),
},
}