Logo

Functional Programming: Where Bugs Become Elegant Puzzles (Part 3)

The IBM/fp-go package provides a set of utilities to write fully functional code in Go. Let's see how we can use this package.

avatar
Hung VoPublished on May 13, 2024
loading

Part 1
Part 2

Example 4: Fully Functional using IBM/fp-go package

The IBM/fp-go package provides a set of utilities to write fully functional code in Go. Let's see how we can use this package to implement the following example:

type User struct {
	ID     string `json:"id"`
	Name   string `json:"name"`
	Phone  string `json:"phone"`
	RoleID int    `json:"role_id"`

	Role string `json:"role"`
}

type Role struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

const APIEnpoint = "https://api.eazymock.net/mock/5abdcf75-2345-4df6-ba62-1fb040f632eb/126/"

// getUsers fetches users from the API
func getUsers(endpoint string) ([]User, error) {
	resp, err := http.Get(endpoint + "users")
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var users []User
	if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
		return nil, err
	}

	return users, nil
}

// getRoles fetches roles from the API
func getRoles(endpoint string) ([]Role, error) {
	resp, err := http.Get(endpoint + "roles")
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var roles []Role
	if err := json.NewDecoder(resp.Body).Decode(&roles); err != nil {
		return nil, err
	}

	return roles, nil
}

func main() {
	// Get users from API
	users, err := getUsers(APIEnpoint)
	if err != nil {
		fmt.Println(err)
		return
	}

	// Get roles from API
	roles, err := getRoles(APIEnpoint)
	if err != nil {
		fmt.Println(err)
		return
	}

    // Add role to users
	roleMap := make(map[int]string)
	for _, role := range roles {
		roleMap[role.ID] = role.Name
	}

	var newUsers []User
	for _, user := range users {
		user.Role = roleMap[user.RoleID]
		newUsers = append(newUsers, user)
	}

	// print the result
	data, _ := json.MarshalIndent(newUsers, "", "  ")
	fmt.Println(string(data))
}

In this example, we fetch users and roles from an API, map roles to users, and print the result. I'm using EazyMock to simulate the API responses. EazyMock is a free online tool that allows you to mock APIs for testing and development purposes. It allows you to define API endpoints and responses, making it easy to test your code without relying on real APIs. It also provides AI-powered mock generation, making it easy to create mock APIs quickly.

Now, let's rewrite the same logic using the IBM/fp-go package:

type User struct {
	ID     string `json:"id"`
	Name   string `json:"name"`
	Phone  string `json:"phone"`
	RoleID int    `json:"role_id"`

	Role string `json:"role"`
}

type Role struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

const APIEnpoint = "https://api.eazymock.net/mock/5abdcf75-2345-4df6-ba62-1fb040f632eb/126/"

func main() {
    // Create a new HTTP client
	client := H.MakeClient(http.DefaultClient)

    // 5-step pipeline to fetch users, roles, map roles to users, and print the result
	data := F.Pipe5(
        // Create a tuple of API endpoints
		T.MakeTuple2(APIEnpoint+"users", APIEnpoint+"roles"),
        // Make GET requests to fetch users and roles
		T.Map2(H.MakeGetRequest, H.MakeGetRequest),
        // Read JSON responses
		R.TraverseTuple2(
			H.ReadJSON[[]User](client),
			H.ReadJSON[[]Role](client),
		),
        // Chain to add roles to users
		R.Chain(func(t T.Tuple2[[]User, []Role]) R.ReaderIOEither[T.Tuple1[[]User]] {
			return F.Pipe2(
                // Create a map of role IDs to role names
				T.Of(getUserRoleMap(t.F2)),
                // Map roles to users
				T.Map1(func(roleMap map[int]string) []User {
					return addRoleToUsers(t.F1, roleMap)
				}),
				R.Of,
			)
		}),
        // Marshal the result to JSON
		R.Chain(func(t T.Tuple1[[]User]) R.ReaderIOEither[string] {
			data, _ := json.MarshalIndent(t.F1, "", "  ")
			return R.Of(string(data))
		}),
        // Print the result
		R.ChainFirstIOK(IO.Printf[string]("%v\n")),
	)

	result := data(context.Background())
	result()

}

// getUserRoleMap creates a map of role IDs to role names
func getUserRoleMap(roles []Role) map[int]string {
	roleMap := make(map[int]string)
	for _, role := range roles {
		roleMap[role.ID] = role.Name
	}
	return roleMap
}

// addRoleToUsers adds roles to users
func addRoleToUsers(users []User, roleMap map[int]string) []User {
	var newUsers []User
	for _, user := range users {
		user.Role = roleMap[user.RoleID]
		newUsers = append(newUsers, user)
	}
	return newUsers
}

The code may looks a bit complex at first, but it's more functional and composable. If you're familiar with the fp-ts library in TypeScript, you'll find the IBM/fp-go package quite similar. It provides a set of utilities to write fully functional code in Go, making it easier to manage side effects, error handling, and data transformations in a predictable and composable way.

[Try it on Go playground]

Why Functional Programming?

Functional Programming offers several benefits that make it an attractive paradigm for software development:

  • Semantic Clarity: FP emphasizes writing code that is more declarative and expressive, making it easier to understand and maintain.

  • Safer Programs: By minimizing side effects and mutable state, FP reduces the likelihood of bugs and unintended consequences in your code.

  • Concise Code: FP encourages writing code that is more declarative and expressive, leading to shorter, more readable programs.

  • Easier Debugging & Testing: Pure functions and immutability make it easier to reason about and test your code, leading to more robust and reliable software.

  • Concurrency: FP simplifies concurrent programming by eliminating shared mutable state, making it easier to write parallel and distributed systems.

By embracing Functional Programming principles, developers can write code that is more elegant, maintainable, and scalable, ultimately leading to more efficient and reliable software systems.

Drawbacks

While Functional Programming offers many benefits, it also has some drawbacks that developers should be aware of:

  • Performance: Functional Programming can sometimes lead to performance issues due to the overhead of creating new data structures and the lack of in-place updates.

  • Not 100% Functional: It's challenging to write fully functional code in languages like Go, which are not purely functional. You may need to compromise on some functional principles to work within the language's constraints.

  • Idiomatic Go: Functional Programming may not always align with idiomatic Go practices. Writing purely functional code in Go can sometimes feel unnatural or verbose.

  • Lack of Syntactic Sugar: Functional Programming can be more verbose than imperative programming, especially in languages like Go that lack syntactic sugar for functional constructs.

When to Use Functional Programming?

Functional Programming is well-suited for a wide range of scenarios, including:

  • When Performance is Not Critical: If performance is not a primary concern, Functional Programming can be an excellent choice for writing clear, maintainable, and reliable code.

  • Whenever Possible: Functional Programming can be used whenever possible, even in languages like Go that are not purely functional. Leveraging functional constructs can lead to more elegant and efficient code.

  • Utilize Libraries: Functional Programming libraries and utilities can help you write more functional code in languages like Go. By leveraging existing tools and libraries, you can embrace FP principles without reinventing the wheel. Some popular libraries for Go include IBM/fp-go, samber/lo, and samber/mo.

Misconceptions

Functional Programming is often misunderstood due to several misconceptions:

  • Syntactic Sugar: Functional Programming is sometimes dismissed as mere syntactic sugar or a coding style. In reality, FP is a paradigm that fundamentally alters how we approach software development.

  • Complicated: Functional Programming is often perceived as complex and difficult to learn. While it may require a shift in mindset, the core concepts of FP are intuitive and can lead to more elegant and maintainable code.

  • Never Side-Effects: Functional Programming does not eliminate side effects entirely. Instead, it minimizes side effects and isolates them to specific parts of the codebase, making it easier to reason about and manage.

  • Academic: Functional Programming is sometimes seen as an academic pursuit with limited real-world applications. In reality, FP is widely used in industry and can lead to more efficient, reliable, and scalable software systems.

Conclusion

Functional Programming is more than just a coding style; it's a paradigm shift that transforms the way we think about writing software. By embracing FP principles such as pure functions, immutability, recursion, higher-order functions, and monads, developers can write elegant, efficient, and maintainable code. While FP may not be a one-size-fits-all solution, it offers many benefits that make it an attractive choice for a wide range of scenarios. By understanding the core concepts of Functional Programming and leveraging the right tools and libraries, developers can unlock the full potential of FP and embark on a journey to solve programming puzzles with elegance and efficiency.

0
0
87
share post

Comments

avatar