Generics: A Boon for Strongly Typed Languages

Nandani Sharma - Jul 3 - - Dev Community

Exciting News! Our blog has a new home!🚀

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. Still, 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).

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 golang.org/x/exp/constraints 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
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

This blog post was originally published on canopas.com.

To read the full version, please visit this blog.

That’s it for today. Keep exploring for the best!!


If you like what you read, be sure to hit 💖 button! — as a writer it means the world!

I encourage you to share your thoughts in the comments section below. Your input not only enriches our content but also fuels our motivation to create more valuable and informative articles for you.

Happy coding! đź‘‹

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player