This package is inspired by Haskell, Scala, Shapeless, and Scala Cats.
Following features are provided by this package.
- Algebraic data types such as Tuple and Option
- A code generating tool
gombok
which is a lombok-like that can generate getter and builder methods.
Install gombok using:
go install github.com/csgura/fp/cmd/gombok
@fp.Value
annotation is used to generate getters and builder.
Example:
package docexample
//go:generate gombok
// @fp.Value
type Person struct {
name string
age int
}
go:generate only needs to be specified once in a package.
@fp.Value must be specified to each struct to generate getter method.
Getter method will not generated if fields name starts with an uppercase letter or starts with _ .
Run go generate
to generate the code. gombok creates {packname}_value_generated.go
file and adds the generated code in it.
The following types and methods are created in the generated file.
func (r Person) Name() string {
return r.name
}
func (r Person) Age() int {
return r.age
}
func (r Person) String() string {
return fmt.Sprintf("Person(name=%v, age=%v)", r.name, r.age)
}
func (r Person) WithName(v string) Person {
r.name = v
return r
}
func (r Person) WithAge(v int) Person {
r.age = v
return r
}
func (r Person) AsTuple() fp.Tuple2[string, int] {
return as.Tuple2(r.name, r.age)
}
func (r Person) AsMap() map[string]any {
return map[string]any{
"name": r.name,
"age": r.age,
}
}
type PersonBuilder Person
func (r PersonBuilder) Build() Person {
return Person(r)
}
func (r PersonBuilder) Name(v string) PersonBuilder {
r.name = v
return r
}
func (r PersonBuilder) Age(v int) PersonBuilder {
r.age = v
return r
}
func (r PersonBuilder) FromTuple(t fp.Tuple2[string, int]) PersonBuilder {
r.name = t.I1
r.age = t.I2
return r
}
func (r PersonBuilder) FromMap(m map[string]any) PersonBuilder {
if v, ok := m["name"].(string); ok {
r.name = v
}
if v, ok := m["age"].(int); ok {
r.age = v
}
return r
}
func (r Person) Builder() PersonBuilder {
return PersonBuilder(r)
}
type PersonMutable struct {
Name string
Age int
}
func (r PersonMutable) AsImmutable() Person {
return Person{
name: r.Name,
age: r.Age,
}
}
func (r Person) AsMutable() PersonMutable {
return PersonMutable{
Name: r.name,
Age: r.age,
}
}
Unlike lombok,
gombok doesn't generate equals and hashCode methods because they don't need.
If it required for some other reason,
You can use an instance of the fp.Eq or fp.Hashable typeclass to define equals and hashCode method, which can be genereted by gombok
// @fp.Derive
var _ hash.Derives[fp.Hashable[Person]]
// var HashablePerson fp.Hashable[Person]
// will be generated by above @fp.Derive annotation
// you can define Eq method by calling HasablePerson.Eqv
func (r Person) Eq(other Person) bool {
return HashablePerson.Eqv(r, other)
}
func (r Person) Hashcode() uint32 {
return HashablePerson.Hash(r)
}
// @fp.Value
type User struct {
name string
email fp.Option[string]
active bool
}
If field type is Option, the following Option-related methods are added.
func (r User) WithSomeEmail(v string) User {
r.email = option.Some(v)
return r
}
func (r User) WithNoneEmail() User {
r.email = option.None[string]()
return r
}
func (r UserBuilder) SomeEmail(v string) UserBuilder {
r.email = option.Some(v)
return r
}
func (r UserBuilder) NoneEmail() UserBuilder {
r.email = option.None[string]()
return r
}
// @fp.Value
// @fp.Json
type Address struct {
country string
city string
street string
}
type AddressMutable struct {
Country string `json:"country,omitempty"`
City string `json:"city,omitempty"`
Street string `json:"street,omitempty"`
}
Json tags will be added to the fields of mutable type.
If the json tag is already defined, it is just copied.
func (r Address) MarshalJSON() ([]byte, error) {
m := r.AsMutable()
return json.Marshal(m)
}
func (r *Address) UnmarshalJSON(b []byte) error {
if r == nil {
return fp.Error(http.StatusBadRequest, "target ptr is nil")
}
m := r.AsMutable()
err := json.Unmarshal(b, &m)
if err == nil {
*r = m.AsImmutable()
}
return err
}
As above, MarshalJSON and UnmarshalJSON methods are generated.
@fp.GenLabelled
annotation is required when deriving an instance of a typeclasse that use field names, such as json encoders and decoders.
// @fp.Value
// @fp.GenLabelled
type Car struct {
company string
model string
year int
}
func (r Car) AsLabelled() fp.Labelled3[NamedCompany[string], NamedModel[string], NamedYear[int]] {
return as.Labelled3(NamedCompany[string]{r.company}, NamedModel[string]{r.model}, NamedYear[int]{r.year})
}
func (r CarBuilder) FromLabelled(t fp.Labelled3[NamedCompany[string], NamedModel[string], NamedYear[int]]) CarBuilder {
r.company = t.I1.Value()
r.model = t.I2.Value()
r.year = t.I3.Value()
return r
}
type NamedCompany[T any] fp.Tuple1[T]
func (r NamedCompany[T]) Name() string {
return "company"
}
func (r NamedCompany[T]) Value() T {
return r.I1
}
func (r NamedCompany[T]) WithValue(v T) NamedCompany[T] {
r.I1 = v
return r
}
// @fp.Value
type Entry[A comparable, B any] struct {
key A
value B
}
type EntryBuilder[A comparable, B any] Entry[A, B]
type EntryMutable[A comparable, B any] struct {
Key A
Value B
}
func (r Entry[A, B]) Key() A {
return r.key
}
func (r Entry[A, B]) WithKey(v A) Entry[A, B] {
r.key = v
return r
}
gombok
can generates an instance of a typeclass automatically.
@fp.Derive
annotation is used.
Example:
// @fp.Derive
var _ eq.Derives[fp.Eq[Person]]
fp.Eq[Person]
is target type to be generated.
Person
type should have @fp.Value
annotation so that AsTuple
method and PersonBuilder
type are generated by gombok
eq.Derives
means eq package has typeclass instances of primitive types ( e.g. Tuple, String , HCons , HNil ) ,
and these instances will be used to generate fp.Eq[Person]
eq.Derives
is a phantom type and has no functionality.
It was just used to tell gombok about the eq package and target type.
package eq
type Derives[T any] interface {
}
The generated codes is like this:
var EqPerson fp.Eq[Person] = eq.ContraMap(
eq.Tuple2(eq.String, eq.Given[int]()),
Person.AsTuple,
)
In the above code, gombok
summons three typeclass instances from eq
package to make Eq[Tuple2[string,int]]
eq.Tuple2()
eq.String
eq.Given[int]()
And eq.ContrMap
is used to convert Eq[Tuple2[string,int]]
to Eq[Person]
eq.ContraMap
function requires a function which converts Person
to Tuple2[string,int]
, so Person.AsTuple
method is used which is generated by @fp.Value
annotation.
Instances of typeclasses are summoned by name.
Here are the rules for names.
Target Type |
Example | Local | Package of Type | Derive Package |
---|---|---|---|---|
Go named type | time.Duration |
EqTimeDuration , EqDuration |
time.EqDuration |
eq.TimeDuration , eq.Duration |
Tuple ( 1 ~ 21 ) | Tuple2 |
EqTuple2[A,B any)(Eq[A], Eq[B]) |
Tuple2[A,B any)(Eq[A], Eq[B]) |
|
hlist.Cons | hlist.Cons |
EqHCons[H any,T hlist.HList](Eq[H], Eq[T]) |
eq.HCons[H any,T hlist.HList](Eq[H], Eq[T]) |
|
hlist.Nil | hlist.Nil |
EqHNil |
eq.HNil |
|
Slice | []string |
EqSlice( Eq[string] ) |
eq.Slice(Eq[string)) |
|
Map | map[string]any |
EqGoMap(Eq[string], Eq[any]) |
eq.GoMap(Eq[string], Eq[any]) |
|
[]byte | []byte |
EqBytes , EqSlice(Eq[byte]) |
eq.Bytes , eq.Slice(Eq[byte]) |
|
Pointer | *int |
EqPtr(lazy.Eval[Eq[int]]) |
eq.Ptr(lazy.Eval[Eq[int]]) |
|
Basic | int |
EqInt |
eq.Int |
|
number | float64 |
EqNumber[~float64 | ~int]() |
eq.Number[~float64 | ~int]() |
|
comparable or any | comparable |
EqGiven[comparable]() |
eq.Given[comparable]() |
|
Labelled ( 1 ~ 21 ) | Labelled2 |
EqLabelled2[A,B fp.Named)(Eq[A], Eq[B]) |
eq.Labelled2[A,B fp.Named)(Eq[A], Eq[B]) |
|
hlist.Cons Labelled | hlist.Cons |
EqHConsLabelled[H fp.Named, T hlist.HList)(Eq[H], Eq[T]) |
eq.HConsLabelled[H fp.Named, T hlist.HList)(Eq[H], Eq[T]) |
|
fp.Named | fp.Named |
EqNamed[T fp.NamedField[A], A any)(Eq[A]) |
eq.Named[T fp.NamedField[A], A any)(Eq[A]) |
After creating an instance of a Tuple
or hlist.Cons
, gombok
converts it to an instance of that type.
In the case of the eq typeclass, ContraMap
was used for conversion, but the function used for conversion changes according to the typeclass's variant.
Because gombok doesn't know how to convert, one of ContraMap
, IMap
, Map
and Generic
functions must be provided.
typeclass variant | Function must be provided | Example typeclass |
---|---|---|
Covariant | Map or Generic |
Decoder, Read |
Contravariant | ContraMap or Generic |
Eq, Ord, Encoder, Show |
Invariant | IMap or Generic |
Monoid |
ContraMap should have the following form.
func ContraMap[A, B any](instance fp.Eq[A], fba func(B) A) fp.Eq[B] {
return New(func(a, b B) bool {
return instance.Eqv(fn(a), fba(b))
})
}
Map should have the following form.
func Map[A, B any](aread Read[A], fab func(A) B) Read[B] {
return New(func(s string) fp.Try[Result[B]] {
return try.Map(aread.Reads(s), func(r Result[A]) Result[B] {
return MapResult(r, fab)
})
})
}
IMap should have the following form.
func IMap[A, B any](instance fp.Monoid[A], fab func(A) B, fba func(B) A) fp.Monoid[B] {
return New(func() B {
return fab(instance.Empty())
}, func(a, b B) B {
return fab(instance.Combine(fba(a), fba(b)))
})
}
Generic should have the following form.
func Generic[A, Repr any](gen fp.Generic[A, Repr], reprShow fp.Show[Repr]) fp.Show[A] {
return New(func(a A) string {
return fmt.Sprintf("%s(%s)", gen.Type, reprShow.Show(gen.To(a)))
})
}
fp.Generic is following type.
package fp
type Generic[T, Repr any] struct {
Type string
To func(T) Repr
From func(Repr) T
}
The Ptr instance should use lazy.Eval to avoid infinite loops.
func Ptr[T any](eq lazy.Eval[fp.Eq[T]]) fp.Eq[*T] {
return New(func(a, b *T) bool {
if a == nil && b == nil {
return true
}
if a != nil && b != nil {
return eq.Get().Eqv(*a, *b)
}
return false
})
}
Example Recursive type :
// @fp.Value
type Node struct {
value string
left *Node
right *Node
}
// @fp.Derive
var _ eq.Derives[fp.Eq[Node]]
Generated Code :
func EqNode() fp.Eq[Node] {
return eq.ContraMap(
eq.Tuple3(eq.String, eq.Ptr(lazy.Call(func() fp.Eq[Node] {
return EqNode()
})), eq.Ptr(lazy.Call(func() fp.Eq[Node] {
return EqNode()
}))),
Node.AsTuple,
)
}
EqNode summoned as func because it calls itself recursively.
// @fp.ImportGiven
var _ ord.Derives[fp.Ord[any]]
You can import typeclass instances defined in other packages.
Example:
// @fp.ImportGiven
var _ ord.Derives[fp.Ord[any]]
// EqSeq is an overrided instance of eq.Seq
// as a result of importing, you can use fp.Ord while deriving fp.Eq[fp.Seq]
func EqSeq[T any](eqT fp.Eq[T], ordT fp.Ord[T]) fp.Eq[fp.Seq[T]] {
return eq.New(func(a, b fp.Seq[T]) bool {
asorted := seq.Sort(a, ordT)
bsorted := seq.Sort(b, ordT)
return eq.Seq(eqT).Eqv(asorted, bsorted)
})
}
// @fp.Value
type TestOrderedEq struct {
list fp.Seq[int]
tlist fp.Seq[fp.Tuple2[int, int]]
}
// @fp.Derive
var _ eq.Derives[fp.Eq[TestOrderedEq]]
Generated Code:
// as a result,
// EqSeq is summoned instead of eq.Seq,
// and Given func is summoned, which defined in ord package.
var EqTestOrderedEq = eq.ContraMap(
eq.Tuple2(EqSeq(eq.Given[int](), ord.Given[int]()), EqSeq(eq.Tuple2(eq.Given[int](), eq.Given[int]()), ord.Tuple2(ord.Given[int](), ord.Given[int]()))),
TestOrderedEq.AsTuple,
)
- Show : https://github.com/csgura/fp/blob/master/test/internal/show/show.go
- Read : https://github.com/csgura/fp/blob/master/test/internal/read/read.go
- Json Encoder : https://github.com/csgura/fp/blob/master/test/internal/js/encoder.go
- Json Decoder : https://github.com/csgura/fp/blob/master/test/internal/js/decoder.go
- Eq : https://github.com/csgura/fp/blob/master/eq/eq_op.go
- Ord : https://github.com/csgura/fp/blob/master/ord/ord_op.go
- Monoid : https://github.com/csgura/fp/blob/master/monoid/monoid_op.go
- Hashable : https://github.com/csgura/fp/blob/master/hash/hash_op.go