15
Go, функциональное программирование
А помните вот эти вот все библиотеки для функционального Go, с монадами и левыми свёртками? А ведь в какой-то момент их было пилить популярнее, чем роутеры.
Я не думаю, что в обозримом будущем даже после появления type params в Go станет популярным функциональный подход к работе с коллекциями. Без итераторов в стандартной библиотеке, параметризированных методов, компактного синтаксиса лямбд и каррирования попытки натянуть map
, filter
или fold
на Go наткнутся на непропорцинально возросшее количество визуального шума.
Сравним два подхода подсчёта букв "а" в очищенных именах котиков
var cats = []string{"thusМамона", "thusАсмодей", "thusВельзевул"}
// функциональный
// никакого вам чейнинга!
var stripped = Map(cats, func(name string) string {
return strings.TrimPrefix(name, "thus")
})
var lowered = Map(stripped, strings.ToLower)
var counts = Map(counts, func(name string) int {
return strings.Count(name, "а")
})
// императивный
var counts = make([]int, 0, len(cats))
for _, name := range cats {
var stripped = strings.TrimPrefix(name, "thus")
var lowered = strings.ToLower(stripped)
counts = append(counts, strings.Count(name, "а"))
}
Для меня второй вариант кода выглядит несколько чище (я даже не поднимаю вопрос производительности). И думаю, разница станет ещё заметнее, если принести сюда обработку ошибок.
С другой стороны, пачка обобщённых функций для работы с коллекциями сильно упростила бы мне работу по маппингу различных DTO друг в друга. Типичная ситуация: есть слайс структур User{ID, Name, Labels}
, и для разных нужд из слайса нужно выудить срез по каждому из полей. Вот и пишешь кучу функций вида usersIDs
, usersNames
и usersLables
там, где можно было бы обойтись двумя строками дженерик кода. Опять же, только с хэш-мапами мне приходит с десяток функций вида func[K, V comparable] Reverse(map[K]V) map[V]K
, которые вроде бы и просто писать самому, но так надоедает!
Ещё можно вспомнить, что есть такой экзотический механизм в go - method expressions. Это что-то вроде обратного каррирования методов - если у типа Teapot
есть метод .Brew(water) error
, то выражение Teapot.Brew
будет иметь тип func(Teapot, water) error
.
В целом эта штука считается линтерами вредной, да и никем толком не используется. Но вот кажется мне, что с появлением дженериков она получит второе дыхание:
func main() {
var cats = []Cat{
{100},
{500},
{420},
}
fmt.Printf("%q\n", Map(cats, Cat.Mew))
// ["mew 100" "mew 500" "mew 420"]
}
type Cat struct{ index int }
func (cat Cat) Mew() string {
return fmt.Sprintf("mew %d", cat.index)
}
func Map[X, Y any](xx []X, fn func(X) Y) []Y {
var yy = make([]Y, 0, len(xx))
for _, x := range xx {
yy = append(yy, fn(x))
}
return yy
}