gg: Go Generics Library

repository·main·Indexed 18 days ago

https://github.com/bytedance/gg

A high-performance library of generic data structures and utility functions for Go 1.18+. gg provides type-safe alternatives to standard library patterns, featuring functional programming utilities (gfunc, goption, gresult), generic data processing for slices (gslice), maps (gmap), and pointers (gptr), as well as concurrent-safe structures like skipset and skipmap. It includes lazy data processing via the iter and stream packages.

Tokens
11.5K
Snippets
40
Records
46
Agent score
63%

What's inside gg

  1. What is gg: Go Generics overview

    main

    gg is a high-performance, type-safe library of generic data structures and utility functions for Go (1.18+). It is designed to be modular, semantic, and easy to use without introducing third-party dependencies. Key features include:

    • Functional Programming Utilities: Tools like goption and gresult to handle common Go patterns.
    • Data Processing: Generic utilities for slices, maps, pointers, values, and JSON.
    • High-Performance Data Structures: Concurrent-safe structures like skipset and skipmap that can outperform the standard library's sync.Map.
    • Standard Library Wrappers: Generic wrappers for the sync package.
  2. Overview of gg: Go Generics

    main

    gg is a foundational Go generics library developed by ByteDance, built for Go 1.18+. It provides high-performance, type-safe generic data structures and utility functions.

    Key features include:

    • Modular Design: Functionality is split into subpackages (e.g., gslice, gmap, goption) for easy use.
    • High Performance: Includes high-performance concurrent data structures that can significantly outperform the standard library.
    • Zero Dependencies: The library does not introduce any third-party dependencies.
    • SemVer Compliant: Follows semantic versioning to ensure backward compatibility.
  3. How Partial Application works in gfunc

    main

    Partial application allows you to transform a function of $N$ arguments into a function of $N-1$ arguments by 'fixing' one or more parameters.

    Workflow Example:

    • Start with a function f(a, b) (Arity 2).
    • Use gfunc.Partial2(f) to create a wrapper.
    • Call .Partial(val) on that wrapper to bind the first argument. This returns a new function that only requires the remaining argument (Arity 1).
    • Call .Partial(val) again on the Arity 1 function to bind the last argument. This returns a function with no arguments (Arity 0).

    This pattern is useful for creating specialized versions of general functions for use in callbacks or specific logic flows.

    // Example of reducing arity from 2 -> 1 -> 0
    add := gfunc.Partial2(f)   // Arity 2
    add1 := add.Partial(1)    // Arity 1
    add1n2 := add1.Partial(2) // Arity 0
  4. How to implement a custom Iter

    main

    To use iter operations on custom data structures, implement the Iter interface. You must provide a Next(n int) []T method.

    Example: Implementing Iter for container/list:

    type listIter[T any] struct {
            e *list.Element
    }
    
    func FromList[T any](l *list.List) Iter[T] {
            return &listIter[T]{l.Front()}
    }
    
    func (i *listIter[T]) Next(n int) []T {
            var next []T
            j := 0
            for i.e != nil {
                    next = append(next, i.e.Value.(T))
                    i.e = i.e.Next()
                    j++
                    if n != ALL && j >= n {
                            break
                    }
            }
            return next
    }
    import (
            "container/list"
    )
    
    type listIter[T any] struct {
            e *list.Element
    }
    
    func FromList[T any](l *list.List) Iter[T] {
            return &listIter[T]{l.Front()}
    }
    
    func (i *listIter[T]) Next(n int) []T {
            var next []T
            j := 0
            for i.e != nil {
                    next = append(next, i.e.Value.(T))
                    i.e = i.e.Next()
                    j++
                    if n != ALL && j >= n {
                            break
                    }
            }
            return next
    }
  5. How stream variants work

    main

    The stream package provides different Stream variants based on the type constraints of the elements. These variants may include additional specialized sources, operations, or sinks that are not available on the base Stream[any] type.

    Element Type ConstraintVariant Name
    anyStream
    comparableComparable
    constraints.OrderedOrderable
    ~boolBool
    ~stringString
    map[comparable]anyKV
    map[constraints.Ordered]anyOrderableKV
  6. Quick Start with iter package

    main

    The iter package provides a generic Iter type and high-order functions for processing data. Most operations are lazy, meaning evaluation is delayed until a Sink (like ToSlice) is called.

    To use it:

    1. Import github.com/bytedance/gg/internal/iter.
    2. Create an iterator using a Source (e.g., FromSlice).
    3. Apply Operations (e.g., Filter, Map).
    4. Convert the result back to a data structure using a Sink (e.g., ToSlice).
    package main
    
    import (
            "fmt"
            "strconv"
            "github.com/bytedance/gg/gvalue"
            "github.com/bytedance/gg/internal/iter"
    )
    
    func main() {
            // Create iterator from slice -> Filter zeros -> Map to string -> Convert to slice
            s := iter.ToSlice(
                    iter.Map(strconv.Itoa, 
                            iter.Filter(gvalue.IsZero[int], 
                                    iter.FromSlice([]int{0, 1, 2, 3, 4}))))
            fmt.Printf("%q\n", s)
    
            // Output:
            // ["1" "2" "3" "4"]
    }
  7. Implement Partial Application with gfunc

    main

    The gfunc package implements Partial Application, which is the process of fixing a number of arguments to a function to produce a new function with a smaller arity (fewer parameters).

    To use it:

    1. Import github.com/bytedance/gg/gfunc.
    2. Use MakeN or PartialN functions (like Partial2 for a 2-parameter function) to wrap your target function.
    3. Use the .Partial() or .PartialR() methods on the resulting object to bind specific arguments.

    Note: While gfunc is useful for function manipulation, for common generic operations like addition, consider using gvalue instead.

    package main
    
    import (
            "fmt"
            "github.com/bytedance/gg/gfunc"
    )
    
    func main() {
            f := func(a, b int) int {
                    return a + b
            }
            add := gfunc.Partial2(f)   // Cast f to "partial application" type
            add1 := add.Partial(1)    // Bind argument a to 1
            fmt.Println(add1(0))      // 1 + 0 = 1
            fmt.Println(add1(1))      // 1 + 1 = 2
            add1n2 := add1.Partial(2) // Bind argument b to 2
            fmt.Println(add1n2())     // 1 + 2 = 3
    }
  8. Quick Start with stream processing

    main

    The stream package allows you to perform stream processing using method chaining. It is built as a wrapper around the iter package.

    To use it:

    1. Import github.com/bytedance/gg/internal/stream.
    2. Create a stream using a source function like FromSlice.
    3. Apply lazy operations like Filter or Map.
    4. Use a sink like ToSlice to evaluate the stream and convert it back to a concrete type.

    WARNING: This package is experimental and may change in the future.

    package main
    
    import (
            "fmt"
    
            "github.com/bytedance/gg/gvalue"
            "github.com/bytedance/gg/internal/stream"
    )
    
    func main() {
            s := stream.FromSlice([]int{0, 1, 2, 3, 4}).    // Construct a stream from int slice
                    Filter(gvalue.IsNotZero[int]).              // Filter zero value lazily
                    ToSlice()                               // Evaluate and convert back to slice
    
            fmt.Println(s)
            // Output:
            // [1 2 3 4]
    }
  9. Limitations of stream type transformations

    main

    Due to current Go language limitations regarding parameterized methods (Go issue #49085), you cannot transform a stream from one type to another using method chaining.

    While iter.Map can transform an iter.Iter[F] to iter.Iter[T], the Stream.Map method in this package cannot introduce a new type parameter T. Consequently, you cannot chain a Map operation that changes the underlying element type within a single method chain.

  10. Workaround for type inference limitations in gfunc

    main

    Due to Go 1.18 limitations regarding type inference for composite literals, you cannot always easily cast a function directly to a "partial application" type.

    Solution: Use the provided MakeN functions. These functions allow you to cast functions to partial-application-capable types without having to explicitly specify every type parameter manually.

  11. Simplify operations with Partial Application

    main

    You can use gfunc.Partial2 and .Partial() to bind arguments to higher-order functions, making them easier to pass into Map or other operations.

    Example: Creating a function that adds 1 to every element:

    add := gvalue.Add[int]                 // instantiate a int version of Add function
    add1 := gfunc.Partial2(add).Partial(1) // bind the first argument to 1
    s := ToSlice(
            Map(add1, 
                FromSlice([]int{1, 2, 3, 4})))
    
    // Output: [2 3 4 5]
    add := gvalue.Add[int]                 // instantiate a int version of Add function
    add1 := gfunc.Partial2(add).Partial(1) // bind the first argument to 1
    s := ToSlice(
            Map(add1, 
                FromSlice([]int{1, 2, 3, 4})))
    
    fmt.Println(s)
    // Output:
    // [2 3 4 5]