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 package pseudo
import ( import (
@ -6,63 +17,101 @@ import (
"fmt" "fmt"
) )
// Zero ... // A Type wraps a strongly-typed value without exposing information about its
func Zero[T any]() (*Type, *Value[T]) { // type. It can be used in lieu of a generic field / parameter.
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 ...
type Type struct { type Type struct {
val value val value
} }
func (t *Type) Interface() any { return t.val.get() } // A Value provides strongly-typed access to the payload carried by a [Type].
// 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
}
type Value[T any] struct { type Value[T any] struct {
t *Type 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) } 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 { type value interface {
get() any get() any
canSetTo(any) bool canSetTo(any) bool
@ -84,11 +133,14 @@ func (c *concrete[T]) canSetTo(v any) bool {
return ok 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 SetTo any
} }
func (e *InvalidTypeError[T]) Error() string { func (e *invalidTypeError[T]) Error() string {
var t T var t T
return fmt.Sprintf("cannot set %T to %T", t, e.SetTo) 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 { func (c *concrete[T]) set(v any) error {
vv, ok := v.(T) vv, ok := v.(T)
if !ok { 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 c.val = vv
return nil return nil

View file

@ -15,7 +15,7 @@ func TestType(t *testing.T) {
testType( testType(
t, "From[uint](314159)", t, "From[uint](314159)",
func() (*Type, *Value[uint]) { func() *Pseudo[uint] {
return From[uint](314159) return From[uint](314159)
}, },
314159, 0, struct{}{}, 314159, 0, struct{}{},
@ -24,20 +24,20 @@ func TestType(t *testing.T) {
testType(t, "nil pointer", Zero[*float64], (*float64)(nil), new(float64), 0) 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) { t.Run(name, func(t *testing.T) {
typ, val := ctor() typ, val := ctor().TypeAndValue()
assert.Equal(t, init, val.Get()) assert.Equal(t, init, val.Get())
val.Set(setTo) val.Set(setTo)
assert.Equal(t, setTo, val.Get()) assert.Equal(t, setTo, val.Get())
t.Run("set to invalid type", func(t *testing.T) { 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) { assertError := func(t *testing.T, err any) {
t.Helper() t.Helper()
switch err := err.(type) { switch err := err.(type) {
case *InvalidTypeError[T]: case *invalidTypeError[T]:
assert.Equal(t, wantErr, err) assert.Equal(t, wantErr, err)
default: default:
t.Errorf("got error %v; want %v", err, wantErr) 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) buf, err := json.Marshal(typ)
require.NoError(t, err) require.NoError(t, err)
got, gotVal := Zero[T]() got, gotVal := Zero[T]().TypeAndValue()
require.NoError(t, json.Unmarshal(buf, &got)) require.NoError(t, json.Unmarshal(buf, &got))
assert.Equal(t, val.Get(), gotVal.Get()) 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 ... // FromChainConfig ...
func (ExtraPayloadGetter[C, R]) FromChainConfig(c *ChainConfig) *C { func (ExtraPayloadGetter[C, R]) FromChainConfig(c *ChainConfig) *C {
return pseudo.NewValueUnsafe[*C](c.extraPayload()).Get() return pseudo.MustNewValue[*C](c.extraPayload()).Get()
} }
// FromRules ... // FromRules ...
func (ExtraPayloadGetter[C, R]) FromRules(r *Rules) *R { 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]() { func mustBeStruct[T any]() {
@ -160,19 +160,17 @@ func (r *Rules) extraPayload() *pseudo.Type {
return r.extra return r.extra
} }
func (Extras[C, R]) nilForChainConfig() *pseudo.Type { return pseudo.OnlyType(pseudo.Zero[*C]()) } func (Extras[C, R]) nilForChainConfig() *pseudo.Type { return pseudo.Zero[*C]().Type }
func (Extras[C, R]) nilForRules() *pseudo.Type { return pseudo.OnlyType(pseudo.Zero[*R]()) } func (Extras[C, R]) nilForRules() *pseudo.Type { return pseudo.Zero[*R]().Type }
func (*Extras[C, R]) newForChainConfig() *pseudo.Type { func (*Extras[C, R]) newForChainConfig() *pseudo.Type {
var x C 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 { func (e *Extras[C, R]) newForRules(c *ChainConfig, r *Rules, blockNum *big.Int, isMerge bool, timestamp uint64) *pseudo.Type {
if e.NewForRules == nil { if e.NewForRules == nil {
return e.nilForRules() return e.nilForRules()
} }
return pseudo.OnlyType( return pseudo.From(e.NewForRules(c, r, c.extra.Interface().(*C), blockNum, isMerge, timestamp)).Type
pseudo.From(e.NewForRules(c, r, c.extra.Interface().(*C), blockNum, isMerge, timestamp)),
)
} }

View file

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