logic-building/functional-go

Support fluent programming style (enable method chaining)

vst-bit opened this issue · 4 comments

Hi all,

Many thanks for providing such a great code, first of all.

Would it be a lot of hassle for you to enhance the existing code generation a bit so we can all benefit from method chaining

For instance, in case of Employee type, there would be just two more types introduced (EmployeeSliceand EmployeeSlicePtr) as well as all their relevant methods like Map, MapPtr etc.:

type EmployeeSlice []Employee

// New method to generate
func (slice EmployeeSlice) Map(f func(Employee) Employee) EmployeeSlice {
	return Map(f, slice) // reusing existing function
}

// further EmployeeSlice methods follow...

type EmployeeSlicePtr []*Employee

// New method to generate
func (slicePtr EmployeeSlicePtr) MapPtr(f func(*Employee) *Employee) EmployeeSlicePtr {
	return MapPtr(f, slicePtr) // reusing existing function
}

// further EmployeeSlicePtr methods follow...

This would allow writing code in fluent programming style, something like:

es := EmployeeSlicePtr{&Employee{...}}

es = es.MapPtr(...).MapPtr(...).FilterPtr(...)

Furthermore, by simply rewriting the mapper function MapPtr (in the monadic fashion using functors):

type EmployeeFunctorPtr    func(*Employee)                            *Employee

type EmployeeMapFunctorPtr func(EmployeeFunctorPtr, EmployeeSlicePtr)  EmployeeSlicePtr

// MapPtr function rewritten using functors
func MapPtr(f EmployeeFunctorPtr, slicePtr EmployeeSlicePtr) EmployeeSlicePtr {
	if f == nil {
		return EmployeeSlicePtr{}
	}
	newSlice := make(EmployeeSlicePtr, len(slicePtr))
	for i, v := range slicePtr {
		newSlice[i] = f(v)
	}
	return newSlice
}

...and by adding another (pipeline) method:

// Another method (to generate) 
func (slicePtr EmployeeSlicePtr) MapPipelinePtr(mapper EmployeeMapFunctorPtr, functors ...EmployeeFunctorPtr) EmployeeSlicePtr {

    tmpSlicePtr := slicePtr 
    for _, f := range functors {
        tmpSlicePtr = mapper(f, tmpSlicePtr)
    }
    return tmpSlicePtr
}

...we would then also be able to simply chain any number of mapping transformations:

slice := &EmployeeSlicePtr{&Employee{1,"Alfa",...}, &Employee{2,"Beta",...}}
funct1 := func(e *Employee) *Employee {
    ...
    return e
}
funct2 := func(e *Employee) *Employee {
    ...
    return e
}
slice.MapPipelinePtr(MapPtr, funct1, funct2)

Thanks in advance for taking this into consideration.

Hi
I really appreciate you for taking the time to review and giving the ideas with examples.
I am thinking of adding your idea as a new feature. I am thinking something like this.


// https://play.golang.org/p/TvLbbx1CX83

package main

import "fmt"
func main() {
	mySlice := Int32([]int32{1, 2, 3})  

	fmt.Println(
		mySlice.
		Filter(even).
		Map(square).
		Get())
}


type Int32 []int32

func(slice Int32) Map(f func(int32) int32) Int32 {

	var newList Int32
	for _, v := range slice {
		r := f(int32(v))
		newList = append(newList, r)
	}
	return newList
}

func(slice Int32) Filter(f func(int32) bool) Int32 {

	var newList Int32
	for _, v := range slice {
		if f(int32(v)) {
			newList = append(newList, v)
		}
	}
	return newList
}

func (slice Int32) Get() []int32 {
	return slice
}

func square(a int32) int32 {
	return a * a;
}

func even(a int32) bool {
	return a % 2 == 0
}

Let me know your feedback. It would be also great if you can put a running example that can help me to analyze and writing a template to generate code.

Hey, thanks! You're definitely on a good/right track! 👍

Here's a running example (for MapPtr method): https://goplay.tools/snippet/qZodFKHXUqO

package main

import "fmt"

// The source type to use for code generation
type Employee struct {
	Id     int
	Name   string
	Salary float64
	//CreationDate time.Time
}

// BEGIN OF CODE GENERATION

// Generate all required types (based on Employee)
type EmployeeSlicePtr []*Employee
type EmployeeFunctorPtr func(*Employee) *Employee

// The only explicit (and idempotent) constructor for the EmployeeSlicePtr type
func MakeEmployeeSlicePtr(employees ...*Employee) EmployeeSlicePtr {
	newSlice := make(EmployeeSlicePtr, len(employees))
	for i, e := range employees {
		newSlice[i] = &Employee{Id: e.Id, Name: e.Name, Salary: e.Salary}
	}
	return newSlice
}

// A new helper using functors (analog existing MapPtr helper function)
func mapPtr(f EmployeeFunctorPtr, slicePtr EmployeeSlicePtr) EmployeeSlicePtr {
	if f == nil {
		return EmployeeSlicePtr{}
	}
	newSlice := make(EmployeeSlicePtr, len(slicePtr))
	for i, v := range slicePtr {
		newSlice[i] = f(v)
	}
	return newSlice
}

// The new mapping method (to generate)
func (slicePtr EmployeeSlicePtr) MapPtr(functors ...EmployeeFunctorPtr) EmployeeSlicePtr {

	tmpSlicePtr := slicePtr
	for _, f := range functors {
		tmpSlicePtr = mapPtr(f, tmpSlicePtr)
	}
	return tmpSlicePtr
}

// END OF CODE GENERATION

// Various functors (to be used within transformation)
var (
	tenPercentRaiseFunct EmployeeFunctorPtr = func(e *Employee) *Employee {
		e.Salary *= 1.1
		return e
	}
	incomeTaxFunct EmployeeFunctorPtr = func(e *Employee) *Employee {
		e.Salary *= .95
		return e
	}
)

func main() {

	emp1 := &Employee{1, "Alfa", 100.} // an object under transformation
	emp2 := &Employee{2, "Beta", 200.} // yet another object under transformation

	newSlice := MakeEmployeeSlicePtr(emp1, emp2).MapPtr(tenPercentRaiseFunct).MapPtr(incomeTaxFunct) // facilitate and run the transformation
	// Equivalent to: newSlice := MakeEmployeeSlicePtr(emp1, emp2).MapPtr(tenPercentRaiseFunct, incomeTaxFunct)

	fmt.Printf("Old salary (before raise) = %f => New salary (after raise) %f", emp1.Salary, newSlice[0].Salary)
	// This should produce output: Old salary (before raise) = 100.000000 => New salary (after raise) 104.500000
}

Hi,
I completely agree with your idea. I will include this as a new feature in the next release sometime in March. Thanks a lot.

Hi!

Deeply grateful for adopting the idea 👍 And looking forward to the March release!

Here's the previous example rewritten to demonstrate some major benefits of using monads: https://goplay.tools/snippet/MrWJ-MXb5tF

package main

import "fmt"

// The source type to use for code generation
type Employee struct {
	Id     int
	Name   string
	Salary float64
	//CreationDate time.Time
}

//// BEGIN OF CODE GENERATION ////

//// TYPES

type EmployeeSlicePtr []*Employee                                          // new (slice) type to generate methods for
type EmployeeFunctorPtr func(e *Employee, index int, length int) *Employee // EmployeeSlicePtr metadata-aware (inde/length) functor
type EmployeeMonadPtr func(...interface{}) EmployeeFunctorPtr              // generic monad for EmployeeFunctorPtr

//// MONADS (field-specific)

// A type field-specific monadic helper (field: Salary)
var EmployeeSalaryMultiplicatorPtr EmployeeMonadPtr = func(flt64 ...interface{}) EmployeeFunctorPtr {
	return func(e *Employee, index, length int) *Employee {
		for _, f := range flt64 {
			e.Salary *= f.(float64)
		}
		return e
	}
}

// Yet another type field-specific monadic helper (field: Salary)
var EmployeeSalaryIncreaserPtr EmployeeMonadPtr = func(flt64 ...interface{}) EmployeeFunctorPtr {
	return func(e *Employee, index, length int) *Employee {
		for _, f := range flt64 {
			e.Salary += f.(float64)
		}
		return e
	}
}

//// MONADS (field-agnostic)

// A field-agnostic monadic helper demonstrating trivial usage of index/length
var EmployeeNilFirstPtr EmployeeMonadPtr = func(...interface{}) EmployeeFunctorPtr {
	return func(e *Employee, index, length int) *Employee {
		if index == 0 {
			return nil
		}
		return e
	}
}

// Yet another field-agnostic monadic helper demonstrating a trivial usage of index/length
var EmployeeNilLastPtr EmployeeMonadPtr = func(...interface{}) EmployeeFunctorPtr {
	return func(e *Employee, index, length int) *Employee {
		if index == length-1 {
			return nil
		}
		return e
	}
}

// Yet another field-agnostic monadic helper demonstrating a trivial usage of index/length
var EmployeeNilOddsPtr EmployeeMonadPtr = func(...interface{}) EmployeeFunctorPtr {
	return func(e *Employee, index, length int) *Employee {
		if index%2 == 0 {
			return nil
		}
		return e
	}
}

// Yet another field-agnostic monadic helper demonstrating a trivial usage of index/length
var EmployeeNilEvensPtr EmployeeMonadPtr = func(...interface{}) EmployeeFunctorPtr {
	return func(e *Employee, index, length int) *Employee {
		if index%2 == 0 {
			return e
		}
		return nil
	}
}

//// CONSTRUCTORS

// The only explicit (and idempotent) constructor of EmployeeSlicePtr
func NewEmployeeSlicePtr(employees ...*Employee) EmployeeSlicePtr {
	newSlice := make(EmployeeSlicePtr, len(employees))
	for i, e := range employees {
		newSlice[i] = &Employee{Id: e.Id, Name: e.Name, Salary: e.Salary}
	}
	return newSlice
}

// The copy constructor of EmployeeSlicePtr
func CopyFromEmployeeSlicePtr(employees EmployeeSlicePtr) EmployeeSlicePtr {
	return NewEmployeeSlicePtr(employees...)
}

//// METHODS (of EmployeeSlicePtr)

// A new helper using functors (analog existing MapPtr helper function)
func mapPtr(f EmployeeFunctorPtr, slicePtr EmployeeSlicePtr) EmployeeSlicePtr {
	if f == nil {
		return EmployeeSlicePtr{}
	}
	newSlice := make(EmployeeSlicePtr, len(slicePtr))
	for i, v := range slicePtr {
		if v != nil {
			if emp := f(v, i, len(slicePtr)); emp != nil {
				newSlice[i] = emp
			}
		}
	}
	return newSlice
}

// The new mapping method (to generate)
func (slicePtr EmployeeSlicePtr) MapPtr(functors ...EmployeeFunctorPtr) EmployeeSlicePtr {

	tmpSlicePtr := slicePtr
	for _, f := range functors {
		tmpSlicePtr = mapPtr(f, tmpSlicePtr)
	}
	return tmpSlicePtr
}

// A monadic version of the mapping method
func (slicePtr EmployeeSlicePtr) MapMonadicPtr(m EmployeeMonadPtr, values ...interface{}) EmployeeSlicePtr {

	tmpSlicePtr := slicePtr
	f := m(values...)
	tmpSlicePtr = mapPtr(f, tmpSlicePtr)
	return tmpSlicePtr
}

//// END OF CODE GENERATION ////

//// USAGE EXAMPLE

// Various *Employee functors (to be used when mapping)
var (
	raiseSalaryTenPercent                   = EmployeeSalaryMultiplicatorPtr(1.1)
	deduceIncomeTax                         = EmployeeSalaryMultiplicatorPtr(.95)
	raiseSalaryTenPercentAndDeduceIncomeTax = EmployeeSalaryMultiplicatorPtr(1.1, .95) // previous two functors combined/chained
	deduceSyndicalMembership                = EmployeeSalaryIncreaserPtr(-2.)
)

func main() {

	emp1 := &Employee{1, "Alfa", 100.} // an object under transformation
	emp2 := &Employee{2, "Beta", 200.} // yet another object under transformation

	origin := NewEmployeeSlicePtr(emp1, emp2) // just for the sake of comparison (in the final report)
	CopyFromEmployeeSlicePtr(origin).         // a whole new slice of pointers
		// MapPtr(raiseSalaryTenPercentAndDeduceIncomeTax).      // equivalent to: MapPtr(raiseSalaryTenPercent, deduceIncomeTax)
		MapMonadicPtr(EmployeeSalaryMultiplicatorPtr, 1.1, .95). // equivalent to: MapPtr(EmployeeSalaryMultiplicatorPtr(1.1, .95))
		MapPtr(deduceSyndicalMembership).                        // equivalent to: MapMonadicPtr(EmployeeSalaryIncreaserPtr, -2.)
		//MapPtr(EmployeeNilFirstPtr()).                         // just to demo it, no any reason to call it at all
		MapPtr(func(e *Employee, index, length int) *Employee { // short report
			fmt.Printf("%s: Old gross salary (before raise) = %f => New net salary (after raise) = %f \n",
				e.Name, origin[index].Salary, e.Salary)
			return e
		})
	// This should produce output:
	// Alfa: Old gross salary (before raise) = 100.000000 => New net salary (after raise) = 102.500000
	// Beta: Old gross salary (before raise) = 200.000000 => New net salary (after raise) = 207.000000
}