Generics: A Boon for Strongly Typed Languages

Get to know when generics are useful and how Golang uses it…
Jul 1 2024 · 7 min read

Background

Consider a scenario, You have two glasses one with milk and the other with buttermilk. Your task is to identify between milk and buttermilk before drinking it(of course without tasting and touching!). 

Hard to admit that, right? Just because they seem similar doesn’t mean they are the same.

The same happens with loosely typed languages. When we provide arguments without mentioning types (as they don’t expect), the identification relies on runtime whether it will be a number or string(milk and buttermilk in the above example).

For example, You have a function that accepts a variable as input, but unless we specify its type, we can’t admit that it will always be a number or a string. Because it can be either one or completely a different one — boolean!

There come the strictly/strongly typed languages in the picture. No doubt loosely typed languages come with more freedom but with the cost of robustness! 
In languages like JavaScript and PHP, variables dance to the beat of their assigned values, morphing from numbers to strings with nary a complaint. 

But for those who’ve migrated to Golang, the world of strict typing can feel…well, a bit rigid at first. 

Two separate functions for int and string
Even though having the same computation logic? 
Where's the flexibility?

But why to fear, when generics are here?

Generics are important for writing reusable and expressive code. In strongly typed languages, generics are a way to go. However, Golang had no support for generics until Go1.17, it started supporting from Go1.18 and later. 

In this blog, we will explore what are the disadvantages of loosely typed languages and how generics bridges a gap between the expressiveness of loose typing and strongly typed languages(Golang).

Sponsored

We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!

Why choose strongly typed language over loosely typed?

Loose Typing: Case of the Miscalculated Discount (PHP)

Imagine an e-commerce website built with PHP, a loosely typed language. A function calculates a discount based on a user’s loyalty points stored in a variable $points.

function calculateDiscount($points) {
  if ($points > 100) {
    $discount = $points * 0.1; // Assuming points are integers
  } else {
    $discount = 0;
  }
  return $discount;
}

$userPoints = "Gold"; // User with "Gold" loyalty tier

$discount = calculateDiscount($userPoints); // Unexpected behavior

// This might result in a runtime error or unexpected discount calculation 
// due to the string value in $userPoints.

Here, the function expects an integer for $points to calculate the discount. However, due to loose typing, a string value ("Gold") is passed. 

This might lead to a runtime error(as it’s not pre-compiled) or an unexpected discount calculation, causing confusion for the user and requiring additional debugging efforts.

Strict Typing: Catching the Discount Error Early (Go)

Now, let’s consider the same scenario in Go, a strictly typed language.

func calculateDiscount(points int) float64 {
  if points > 100 {
    return float64(points) * 0.1
  }
  return 0
}

var userPoints int = 150 // User's loyalty points stored as an integer

discount := calculateDiscount(userPoints)

// This code will not compile due to the incompatible type of "userPoints"
// if it's not declared as an integer initially.

In Go, the function calculateDiscount explicitly requires an integer for points

If we attempt to pass the string value "Gold", the code won't even compile. This early error detection prevents unexpected behavior at runtime and ensures data integrity.

However, while strictly typed languages don’t provide as much flexibility as loosely typed it ensures the robustness of our code. While not being so rigid, strictly typed languages provide support for generics to ensure code reusability and cleanliness. 

What are generics?

Generics are a powerful programming concept that allows you to write functions and data structures that can work with a variety of different data types without sacrificing type safety.

Practical use case

Let’s consider a simple example,

A function that takes an array as input be it []int64 or []float64 and returns the sum of all its elements.

Ideally, we would need to define two different functions for each.

// SumInts adds together the values of m.
func SumInts(m []int64) int64 {
 var s int64
 for _, v := range m {
  s += v
 }
 return s
}

// SumFloats adds together the values of m.
func SumFloats(m []float64) float64 {
 var s float64
 for _, v := range m {
  s += v
 }
 return s
}

With generics, it’s possible to use a single function that behaves the same for different data types.

// SumIntsOrFloats sums the values of map m. It supports both int64 and float64
// as types for array values.
func SumIntsOrFloats[T int64 | float64](m []T) T{
 var s T
 for _, t:= range m {
  s += t
 }
 return s
}

This function takes []T as input and give T as output, it can be any of the two we mentioned int64 or float64.

The magic lies in the T placeholder. It represents a generic type that can be anything int64 or float64. This eliminates the need for duplicate functions and keeps our code clean and concise.

Type Constraints in Go Generics

Go generics introduce type parameters, allowing functions and data structures to work with various types. However, sometimes, we need to ensure specific properties for those types. This is where type constraints come in.

The comparable Constraint: Not for Ordering

The comparable constraint is a natural choice for ordering elements. 

After all, it guarantees types can be compared for equality using the == operator. However, comparable doesn't imply the ability to use comparison operators like <, >, <=, and >=.

Example:

func IntArrayContains(arr []int, target int) bool {
  for _, element := range arr {
    if element == target {
      return true
    }
  }
  return false
}

func StringArrayContains(arr []string, target string) bool {
  for _, element := range arr {
    if element == target {
      return true
    }
  }
  return false
}

The above function takes int and string respectively and checks whether an element exists in an array or not.

func ArrayContains[T comparable](arr []T, target T) bool {
  for _, element := range arr {
    if element == target {
      return true
    }
  }
  return false
}

The above generic function is a replacement if the IntArrayContains() and StringArrayContains() . It can also be used for float(Try providing ([]float64{2.3,4.5,1.2}, 4.5) as argument!

Where,

[T comparable] —defines a generic type parameter T with comparable constraint. This ensures the elements in the slice can be compared using the == operator.

 — The ordered Constraint: For Ordering

For ordering elements within a generic function, Go offers the Ordered constraint from the cmp package.

The ordered constraint ensures the type parameter can be used with comparison operators like <, >, <=, and >=, making functions like Min or Maxpossible.

Example:

A function to find the minimum value in a slice. Traditionally, we might have written separate functions for different types like int and string .

func minInt(s []int) int {
  min := s[0]
  for _, val := range s {
    if val < min {
      min = val
    }
  }
  return min
}

func minString(s []string) string {
  min := s[0]
  for _, val := range s {
    if val < min { // String comparison might not be intuitive
      min = val
    }
  }
  return min
}

With generics, we can define a single generic function Min that works with any comparable type.

import "cmp"

func Min[T cmp.Ordered](s []T) T {
 min := s[0]
 for _, val := range s {
  if val < min {
   min = val
  }
 }
 return min
}

[T cmp.Ordered] — It defines a generic type parameter T. The cmp.Ordered part specifies a constraint on T. It must be a type that implements the Ordered interface (meaning it supports comparison operators like <, > etc).

Using Generics with Struct Methods

Have you faced a situation when you need to implement two separate functions with the same computational logic, just to process two different structs? Sounds relevant right?

While using generics, we can bind a method and a property with a struct that supports any datatype.

Example:

package main

import "fmt"

type Stack[T any] struct { // define struct with type T(int64/string/float64 etc)
 elements []T
}

func (s *Stack[T]) Push(val T) {
 s.elements = append(s.elements, val)
}

func main() {
 // Stack of integers
 intStack := Stack[int]{}
 intStack.Push(10)
 intStack.Push(20)
 fmt.Println("intStack : ", intStack)

 // Stack of strings
 stringStack := Stack[string]{}
 stringStack.Push("Hello")
 stringStack.Push("World")

 fmt.Println("stringStack : ", stringStack)
}

Here,

  • Stack[T any] is a generic struct and acts as a placeholder for any type.
  • The [Stack T any] has elements property that will hold the elements of type T .
  • The Push(val T) method is defined on the Stack struct. It takes a value of type T and appends it to the elements slice.

Key points

  • The generic type T allows the Stack struct and its Push method to work with various data types.
  • This eliminates the need for separate stack implementations for different types.

Benefits of Generics

  • Reduced Code Duplication — Write one function/data structure that works with various types.
  • Improved Type Safety — The compiler enforces type constraints, preventing errors at compile time.
  • Enhanced Readability — Clearer code that expresses the intent more effectively.
  • Increased Reusability — Generic code can be reused across different parts of your application.

Conclusion

For those of us who’ve embraced the freedom (and occasional chaos) of loose typing, generics offer a compelling middle ground. 

They provide the flexibility we’re used to but with the added benefit of type safety and code clarity. 

So, the next time you find yourself yearning for the days of “anything goes,” remember the power of generics in Golang. They’ll help you write cleaner, more maintainable code, all while keeping the type safe and happy.

Similar Blogs


nidhi-d image
Nidhi Davra
Web developer@canopas | Gravitated towards Web | Eager to assist


nidhi-d image
Nidhi Davra
Web developer@canopas | Gravitated towards Web | Eager to assist

contact-footer
Say Hello!
footer
Subscribe Here!
Follow us on
2024 Canopas Software LLP. All rights reserved.