Go语言函数式编程实践指南

Go语言函数式编程实践指南 尝试用Go进行函数式编程,下面的代码对我来说运行良好,但希望与社区分享以获取审查、评论和反馈。

package main

import (
	"fmt"
	"log"
	"sort"
	"time"
)

const (
	layoutISO = "2006-01-02"
	layoutUS  = "January 2, 2006"
	custom    = "1/2/2006"
)

type Inventories []Inventory
type Inventory struct { //instead of: map[string]map[string]Pairs
	Warehouse string
	Item      string
	Batches   Lots
}
type Lots []Lot
type Lot struct {
	Date  time.Time
	Key   string
	Value float64
}

func main() {
	fmt.Println("Hello, 世界")
	date := "12/31/19" // "1999-12-31"
	t, _ := time.Parse(custom, date)
	var inventories = new(Inventories)
	var inventory = new(Inventory) // Or = Inventory{} both are working //warehouse[item[batch, qty]]
	inventory.Warehouse = "DMM"
	inventory.Item = "Helmet"
	inventory.Batches = append(inventory.Batches, Lot{t, "Jan", 10})
	inventory.InsertBatch(Lot{time.Now(), "Jan", 30})
	inventory.Batches.Insert(Lot{time.Now(), "Feb", 30})
	fmt.Printf("\nBefore grouping: %v %T\n", inventory, inventory)

	x := Inventory{
		Warehouse: "DMM",
		Item:      "Gloves",
		Batches: Lots{
			Lot{mustTime(time.Parse(custom, "1/7/2020")), "Jan", 50},
			Lot{mustTime(time.Parse(custom, "2/1/2020")), "Feb", 20},
			Lot{mustTime(time.Parse(custom, "1/5/2020")), "Jan", 40},
			Lot{mustTime(time.Parse(custom, "2/9/2020")), "Feb", 60},
		},
	}
	fmt.Printf("\nBefore grouping: %v %T\n", x, x)

	// Below can be used for grouping batches in each warehouse seperatly, this can be combined in the lines under
	//	inventory.Batches.Group()
	//	x.GroupBatches()
	// 	fmt.Printf("\nAfter grouping: %v %T\n", inventory, inventory)
	//  fmt.Printf("\nAfter grouping: %v %T\n", x, x)

	// Above can be replaced by below
	inventories.Insert(*inventory)
	inventories.Insert(x)
	inventories.GroupBatches()

	fmt.Printf("\nInventories after gouping batches: %v %T\n", inventories, inventories)

	inventories.SortBatches()

	fmt.Printf("\nInventories after sorting batches: %v %T\n", inventories, inventories)
}

func (i *Inventories) Insert(x Inventory) {
	*i = append(*i, x)
}
func (i *Inventories) GroupBatches() {
	inv := new(Inventories)
	for _, el := range *i {
		el.GroupBatches()
		inv.Insert(el)
	}
	(*i).ReplaceBy(inv)
}

func (i *Inventories) SortBatches() {
	inv := new(Inventories)
	for _, el := range *i {
		sort.Sort(Lots(el.Batches))
		inv.Insert(el)
	}
	(*i).ReplaceBy(inv)
}

func (i *Inventories) ReplaceBy(x *Inventories) {
	*i = *x
}

func (i *Inventory) InsertBatch(x Lot) {
	(*i).Batches = append((*i).Batches, x)
}

func (i *Inventory) GroupBatches() {
	(*i).Batches.Group()
}

func (p *Lots) Group() {
	lots := new(Lots)
	lots.FromMap(p.Map())
	p.ReplaceBy(lots)
}

func (p *Lots) FromMap(m map[string]float64) {
	for k, v := range m {
		(*p).Insert(Lot{time.Now(), k, v})
	}
}

// Below to enable sorting: sort.Sort(Lots(lots))
func (l Lots) Len() int           { return len(l) }
func (l Lots) Less(i, j int) bool { return (l[i].Date).Before(l[j].Date) } // { return l[i].Key < l[j].Key }
func (l Lots) Swap(i, j int)      { l[i], l[j] = l[j], l[i] }

func (p *Lots) Insert(x Lot) {
	*p = append(*p, x)
}

func (p *Lots) ReplaceBy(x *Lots) {
	*p = *x
}

func (p *Lots) Map() map[string]float64 {
	sum := make(map[string]float64)
	for _, el := range *p {
		sum[el.Key] = sum[el.Key] + el.Value
	}
	return sum
}

type Interface interface {
	// Len is the number of elements in the collection.
	Len() int
	// Less reports whether the element with
	// index i should sort before the element with index j.
	Less(i, j int) bool
	// Swap swaps the elements with indexes i and j.
	Swap(i, j int)
}

func mustTime(t time.Time, err error) time.Time {
	failOnError(err)
	return t
}

func failOnError(err error) {
	if err != nil {
		log.Fatal("Error:", err)
		panic(err)
	}
}

输出是:

[Running] go run "d:\goplay\grouping.go"
Hello, 世界

Before grouping: &{DMM Helmet [{0001-01-01 00:00:00 +0000 UTC Jan 10} {2020-06-05 00:26:16.9165066 +0300 +03 m=+0.006981101 Jan 30} {2020-06-05 00:26:16.9165066 +0300 +03 m=+0.006981101 Feb 30}]} *main.Inventory

Before grouping: {DMM Gloves [{2020-01-07 00:00:00 +0000 UTC Jan 50} {2020-02-01 00:00:00 +0000 UTC Feb 20} {2020-01-05 00:00:00 +0000 UTC Jan 40} {2020-02-09 00:00:00 +0000 UTC Feb 60}]} main.Inventory

Inventories after gouping batches: &[{DMM Helmet [{2020-06-05 00:26:16.9175045 +0300 +03 m=+0.007979001 Jan 40} {2020-06-05 00:26:16.9175045 +0300 +03 m=+0.007979001 Feb 30}]} {DMM Gloves [{2020-06-05 00:26:16.9175045 +0300 +03 m=+0.007979001 Jan 90} {2020-06-05 00:26:16.9175045 +0300 +03 m=+0.007979001 Feb 80}]}] *main.Inventories

Inventories after sorting batches: &[{DMM Helmet [{2020-06-05 00:26:16.9175045 +0300 +03 m=+0.007979001 Jan 40} {2020-06-05 00:26:16.9175045 +0300 +03 m=+0.007979001 Feb 30}]} {DMM Gloves [{2020-06-05 00:26:16.9175045 +0300 +03 m=+0.007979001 Jan 90} {2020-06-05 00:26:16.9175045 +0300 +03 m=+0.007979001 Feb 80}]}] *main.Inventories

[Done] exited with code=0 in 3.624 seconds

5 回复

你能解释一下这为什么是函数式编程吗?据我所知,函数式编程的一个重要方面是能够将一个 func 作为另一个 func 的参数来使用。


以下是我之前写的一些示例,可能会有所帮助。V1 是所有版本中最简单的。

V1: https://play.golang.org/p/xBgJUlz2uYX V2: https://play.golang.org/p/1_NXGwTtBHJ V3: https://play.golang.org/p/gHRO16rEEw7 V4: https://play.golang.org/p/2gdsQyvL0yi

如果你为函数参数指定了具体类型(例如 []string 等),这可能会有点棘手。有几种方法可以解决这个问题:

  1. 为每个函数定义一个严格的数据结构轮廓,以便相互传递,形成一种称为“责任链”的设计模式。更多信息:https://sites.google.com/view/chewkeanho/guides/software-design-patterns/patterns/behavioral/chain-of-responsibilities
  2. 与其进行类型定义,你可以“使用”空接口 interface{} 作为数据类型,并在函数内部重新识别数据类型。这会占用额外的 CPU 资源,但这是实现函数式编程最原始的方式。

你必须理解,在真正的函数式编程下,像 itermapForEach 这样的函数必须适用于任何数据类型。这就是为什么 Go 语言不是函数式编程的合适候选者,因为需要在数据类型识别上花费额外的资源。

如果你对函数式编程感兴趣,有空的时候可以试试 Haskell。

另外,你应该了解一下 UNIX 管道在终端中是如何工作的。那个管道是函数式编程的一个真实概念,其中源文件不会被改变,除非被管道传输到明确改变它们的函数中。

根据维基百科的定义,

函数式编程是一种编程范式——一种构建计算机程序结构和元素的风格——它将计算视为数学函数的求值,并避免改变状态和可变数据。

因此,在函数式编程中,有两个非常重要的规则:

  • 无数据突变:这意味着数据对象在创建后不应被更改。
  • 无隐式状态:应避免隐藏/隐式状态。在函数式编程中,状态并未被消除,而是使其可见和显式。

您提到的只是函数式编程的概念之一,即高阶函数,它允许您:

  1. 将函数赋值给变量,
  2. 将函数作为参数传递给另一个函数,
  3. 从一个函数返回另一个函数。

参考

以下是一个使用 Go 语言的示例:

package main
import "fmt"

func main() {
	var list = []string{"Orange", "Apple", "Banana", "Grape"}
	// 我们将数组和一个函数作为参数传递给 mapForEach 方法。
	var out = mapForEach(list, iter)
	fmt.Println(out) // [6, 5, 6, 5]

}

func iter(x string) int {
	return len(x)
}

// 高阶函数接受一个数组和一个函数作为参数
func mapForEach(arr []string, fn func(it string) int) []int {
	var newArray = []int{}
	for _, r := range arr {
		// 我们执行传入的方法 // 在这个例子中是 func iter
		newArray = append(newArray, fn(r))
	}
	return newArray
}

image

这是一个很好的函数式编程实践示例,展示了Go语言中如何通过方法组合和不可变操作来实现函数式风格。以下是一些专业评论:

代码优点

  1. 不可变操作Group()SortBatches()等方法返回新对象而不是修改原对象,符合函数式编程原则
  2. 方法链式调用:通过方法组合实现数据转换流水线
  3. 类型安全:使用具体类型而非interface{},保持类型安全

可改进的函数式编程实践

1. 使用高阶函数

// 添加Map和Filter高阶函数
func (p Lots) Map(f func(Lot) Lot) Lots {
    result := make(Lots, len(p))
    for i, lot := range p {
        result[i] = f(lot)
    }
    return result
}

func (p Lots) Filter(f func(Lot) bool) Lots {
    result := Lots{}
    for _, lot := range p {
        if f(lot) {
            result = append(result, lot)
        }
    }
    return result
}

// 使用示例
filtered := lots.Filter(func(l Lot) bool {
    return l.Value > 20
})

mapped := lots.Map(func(l Lot) Lot {
    return Lot{l.Date, l.Key, l.Value * 1.1}
})

2. 纯函数重构

// 将Group改为纯函数
func GroupLots(lots Lots) Lots {
    grouped := make(map[string]float64)
    
    for _, lot := range lots {
        grouped[lot.Key] += lot.Value
    }
    
    result := Lots{}
    for key, value := range grouped {
        result = append(result, Lot{time.Now(), key, value})
    }
    
    return result
}

// 使用函数组合
func ProcessInventories(inventories Inventories) Inventories {
    return inventories.Map(func(inv Inventory) Inventory {
        return Inventory{
            Warehouse: inv.Warehouse,
            Item:      inv.Item,
            Batches:   GroupLots(inv.Batches).Sort(),
        }
    })
}

3. 添加Reduce操作

func (p Lots) Reduce(initial float64, f func(float64, Lot) float64) float64 {
    result := initial
    for _, lot := range p {
        result = f(result, lot)
    }
    return result
}

// 使用示例
totalValue := lots.Reduce(0, func(acc float64, lot Lot) float64 {
    return acc + lot.Value
})

4. 简化现有代码

// 当前Group方法可以更函数式
func (p Lots) Group() Lots {
    return p.ReduceByKey(
        func(lot Lot) string { return lot.Key },
        func(acc, lot Lot) Lot {
            return Lot{lot.Date, lot.Key, acc.Value + lot.Value}
        },
    )
}

func (p Lots) ReduceByKey(
    keyFunc func(Lot) string,
    reduceFunc func(Lot, Lot) Lot,
) Lots {
    groups := make(map[string]Lot)
    
    for _, lot := range p {
        key := keyFunc(lot)
        if existing, found := groups[key]; found {
            groups[key] = reduceFunc(existing, lot)
        } else {
            groups[key] = lot
        }
    }
    
    result := make(Lots, 0, len(groups))
    for _, lot := range groups {
        result = append(result, lot)
    }
    
    return result
}

性能考虑

当前实现中多次创建新切片,对于大数据集可能影响性能。可以考虑:

// 使用builder模式优化
type LotsBuilder struct {
    lots Lots
}

func NewLotsBuilder() *LotsBuilder {
    return &LotsBuilder{make(Lots, 0)}
}

func (b *LotsBuilder) Add(lot Lot) *LotsBuilder {
    b.lots = append(b.lots, lot)
    return b
}

func (b *LotsBuilder) Build() Lots {
    return b.lots
}

测试友好性

纯函数更容易测试:

func TestGroupLots(t *testing.T) {
    lots := Lots{
        Lot{Date: time.Now(), Key: "Jan", Value: 10},
        Lot{Date: time.Now(), Key: "Jan", Value: 20},
        Lot{Date: time.Now(), Key: "Feb", Value: 30},
    }
    
    grouped := lots.Group()
    
    if len(grouped) != 2 {
        t.Errorf("Expected 2 groups, got %d", len(grouped))
    }
}

总体而言,这是一个很好的起点。通过添加更多高阶函数和保持操作不可变性,可以进一步强化函数式编程风格。

回到顶部