cli  
Article Series

This article series discuss more than 30 different programming languages. Please read overview before you read any of the details.

Playing with Records Related Articles.

Where to Discuss?

Local Group

Preface

Goal: Continue Part One


4: Approach in Solving Unique

A custom pattern matching example, that you can rewrite.

An Algorithm Case

Why making things complex?

My intention is writing a custom pattern matching example, so later I can rewrite other algorithm based on this example. You can read the detail on the first article. The record overview.

Consider going back to simple data. We need to discuss about solving unique list.

Head and Tail Pattern Matching

The original x:xs pattern matching from haskell shown as below:

exclude :: String -> ([String] -> [String])
exclude value = filter((/=) value)

unique :: [String] -> [String]
unique [] = []
unique (head:tail) = head:unique(exclude head tail)

tags :: [String]
tags = ["rock", "jazz", "rock", "pop", "pop"]

main = print (unique tags)

With the result of

["rock","jazz","pop"]

This x:xs pattern is made of two functions:

  1. Exclude
  2. Unique

Loop for Exclude

Based on

exclude :: String -> ([String] -> [String])
exclude value = filter((/=) value

We can rewrite twoliners above to this below:

package main

import "fmt"

func exclude(value string, tags []string) []string {
	var newTags []string

	for _, tag := range tags {
		if tag != value {
			newTags = append(newTags, tag)
		}
	}

	return newTags
}

func main() {
	var tags = []string{
		"rock", "jazz", "rock", "pop", "pop"}

	fmt.Println(exclude("rock", tags))
}

With the result similar as below slices:

$ go run 08-exclude.go
[jazz pop pop]

Loop for Unique

Based on

unique :: [String] -> [String]
unique [] = []
unique (head:tail) = head:unique(exclude head tail)

We can rewrite threeliners above to this below:

package main

import "fmt"

func exclude(value string, tags []string) []string {
	…
}

func unique(tags []string) []string {
	if len(tags) <= 1 {
		return tags
	} else {
		head := tags[0]
		tail := tags[1:len(tags)]

		newHeads := []string{head}
		newTails := unique(exclude(head, tail))

		return append(newHeads, newTails...)
	}
}

func main() {
	var tags = []string{
		"rock", "jazz", "rock", "pop", "pop"}

	fmt.Println(unique(tags))
}

With the result similar as below slices:

$ go run 09-unique.go
[rock jazz pop]

The Recursive

Based on

… = head:unique(exclude head tail)

Transformed into

	head := tags[0]
	tail := tags[1:len(tags)]

	newHeads := []string{head}
	newTails := unique(exclude(head, tail))

	return append(newHeads, newTails...)

I guess the code itself is self explanatory.

myutils Package

The code can be shown as below:

package myutils

func exclude(value string, tags []string) []string {
	…
}

func Unique(tags []string) []string {
	…
}

Go: The Utils Module

I keep the exclude function with lowercase, while changing unique function to uppercase.

Configuration

The go.mod file is simply as below setting

module mysongs

go 1.15

And the go.mod file for root directory has some settings:

module tutor

go 1.15

replace example.com/mysongs => ./mysongs
require example.com/mysongs v1.0.0

replace example.com/myutils => ./myutils
require example.com/myutils v1.0.0

Clean Up

Now we can have a short and tidy code.

package main

import (
	"example.com/mysongs"
	"example.com/myutils"
	"fmt"
)

func main() {
	var tags []string
	tags = mysongs.GetSongs().FlattenTags()
	tags = myutils.Unique(tags)
	fmt.Println(tags)
}

With the result similar as below slices:

$ go run 10-cleanup.go
[60s jazz rock 70s pop]

Go: Approach in Solving Unique

Now you can rewrite your own pattern matching algorithm to go.


5: Concurrency using Goroutine

Before we make a complex goroutine, we can utilize previous flatten example, as a show case to pass data between goroutine.

Passing by Reference

To use defer with array of string, I need to explain pointer a bit.

When we run a goroutine, we need to pass variable as parameter argument in function. In order to change the value of the parameter , we need to pass it by reference.

func flatten(songs []mysongs.Song, tags *[]string) {
	…
}

And change the value later by uisng pointer.

*tags = append(*tags, tag)

The complete code is as below:

package main

import (
	"example.com/mysongs"
	"fmt"
)

func flatten(songs []mysongs.Song, tags *[]string) {
	for _, song := range songs {
		if song.Tags != nil {
			for _, tag := range song.Tags {
				*tags = append(*tags, tag)
			}
		}
	}
}

func main() {
	var tags []string
	flatten(mysongs.GetSongs(), &tags)
	fmt.Println(tags)
}

_.

With the result similar as below slices:

$ go run 11-byref.go
[60s jazz 60s rock 70s rock 70s pop]

Waitgroup

Following the official documentation, we can apply waitgroup for this situation.

Consider start with defer.

package main

import (
	"example.com/mysongs"
	"fmt"
	"sync"
)

func flatten(wg *sync.WaitGroup,
	songs []mysongs.Song, tags *[]string) {
	defer wg.Done()

	for _, song := range songs {
		if song.Tags != nil {
			for _, tag := range song.Tags {
				*tags = append(*tags, tag)
			}
		}
	}
}

And apply the waitgroup pattern` in main function:

func main() {
	wg := &sync.WaitGroup{}
	var tags []string

	wg.Add(1)
	go flatten(wg, mysongs.GetSongs(), &tags)

	wg.Wait()
	fmt.Println(tags)
}

With exactly the same result as previous slices:

$ go run 12-defer.go
[60s jazz 60s rock 70s rock 70s pop]

Passing Data Between Channel

We need to create two functions. The flatten itself, and other function to walk the result.

The flatten function is very similar as previous code.

func flatten(message chan string, quit chan int) {
	songs := mysongs.GetSongs()

	for _, song := range songs {
		if song.Tags != nil {
			for _, tag := range song.Tags {
				message <- tag
			}
		}
	}

	quit <- 0
}

_.

On the other hand, the walk function is, following the example from official documentation.

func walk(message chan string, quit chan int) {
	var tags []string
	var tag string

	for {
		select {
		case tag = <-message:
			tags = append(tags, tag)
		case <-quit:
			fmt.Println(tags)
			return
		}
	}
}

Now we can call both functions, in main.

package main

import (
	"example.com/mysongs"
	"fmt"
)

func walk(message chan string, quit chan int) {…}
func flatten(message chan string, quit chan int) {…}

func main() {
	message := make(chan string)
	quit := make(chan int)

	go flatten(message, quit)
	walk(message, quit)
}

With exactly the same result as previous slices:

$ go run 13-channel.go
[60s jazz 60s rock 70s rock 70s pop]

Go: Passing Data between Channel in Goroutine


What is Next 🤔?

There is however other way to make our life simpler. We can query using Koazee, go-linq or go-funk.

Consider continue reading [ Go - Playing with Records - Part Three ].