Perl to go

I have been using Perl for more than 20 years now, seen Perl 4 bow out and Perl 5 come in and develop in that fantastic language that has helped me uncountable times in my professional life. During those years I’ve also considered learning another language, but I have been unable to take a stand for a long time.

And there came Go and the hype around Go, just like years ago there was a lot of hype around Java. But while whatever written in Java I came across was a big, heavy and slow memory eater, most of the tools I came across that were written in Go were actually good stuff — OK, still a bit bloated in size, but they actually worked. The opportunity came, and I finally gave Go a shot.

A false start

The opportunity arose when early this year it was announced that an introductory course in Go was being organised in our company. I immediately confirmed my interest and bought plane tickets when the date was confirmed.

When the day finally arrived I flew to Trondheim only to discover that the target of the course was silently changed and the course was way more specialistic than expected. Yes, you can say I was annoyed. In an attempt not to waste the trip and time, I started the Tour of Go there and then and went through the first few chapters and exercises. It looked great. For a while.

A tour of go

The tour of Go aims to be an introductory course, and to a point it actually is. But when you get far enough you’ll notice that some exercise assume more knowledge of the language than what was explained in previous chapters: whoever wrote the more advanced lessons was disconnected, either from the previous part of the course or from reality. I vented my frustrations in a tweet:

I was not liking Golang’s syntax nor the tour, frustration was growing, my learning experience was in danger. If I wanted to keep going I needed to find another way; for my particular mindset the best way is often a good book. It was time to find one.

Introducing Go

I did some research and finally set to buy Doxsey’s “Introducing Go” from O’Reilly, a simple book, so thin that Posten delivered it straight into my mailbox! (instead of the usual pick-up notice). The first chapters were simple indeed and I already knew most of their content from the tour of Go so I just skimmed through. Later chapters were still simple, but also informative and to the point, with exercises at the end that were well in line with the content.

I got to the end of the book reasonably quickly, and it was now time for a “final project”. Those who know me, know that I don’t like “Hello world!”-class exercises. To check my learning I want something that is easy enough to be possible for a beginner, but challenging enough to put my new knowledge to the test. I considered a few options and decided to reproduce hENC in Go.

hENC: a recap

For those who don’t know or don’t remember, hENC was a project of mine, the  radically simple hierarchical External Node Classifier (ENC) for CFEngine. It’s a very simple script written in pure Perl. In 79 lines of code (47 of actual code) it reads a list of files in a format similar to CFEngine’s module protocol and merges their content in a hierarchical fashion. The output is then used by CFEngine to classify the node and set variables according to the node’s role in the infrastructure.

hENC reads files, finds data through pattern matching, applies some logic, fills up data structures and prints out the result: no rocket science but still a real-world program. Reproducing hENC in Go had all that it takes for a decent exercise.

NB: hENC is silent on conditions that would otherwise be errors (e.g. missing input files). That is by design. Any output from the program (including errors!) is piped into CFEngine: if you don’t want to fill your CFEngine logs with tons of messages about non-compliant messages in the module’s output you need to make it as silent as possible. That’s why you don’t find any error logging in the Perl version, and all of the messages via the log package are commented out in the Go version.

hENC in Go

Perl and Go are built around very different philosophies: where Perl is redundant and allows for dozens of ways to express the same thing, Go chooses to allow only one or very few (think loops, think control structures…); where many frequently used functions are part of the core language in Perl (e.g. print() and file handling functions like open(), close() and so forth), Go has them in packages that, although part of the core, you have to import explicitly into the program. It’s a difference that is very visible by comparing the sources of the programs and from the very start: the Perl version starts by using two pragmata and then goes straight to the data structures:

use strict ;
use warnings ;

my %class ;    # classes container
my %variable ; # variables container

The Go version spends some lines to import before it gets to the same point:

package main

import "fmt"    // to print something...
import "os"     // to read command-line args, opening files...
import "bufio"  // to read files line by line, see https://golang.org/pkg/bufio/#example_Scanner_lines
// import "log"
import "regexp"


var class    = make(map[string]int)     // classes container
var variable = make(map[string]string)  // variables container

Perl takes advantage of the diamond construct to read from the files like they were a single one and without even explicitly open them:

while (my $line = ) {

Not so in Go:

var encfiles = os.Args[1:]

func main() {
	// prepare the regex for matching the ENC setting
	settingRe := regexp.MustCompile(`^\s*([=\@%+-/_!])(.+)\s*$`)

	// prepare the regex for matching a variable assignment
	varRe := regexp.MustCompile(`^(.+?)=`)
	
File:
	// iterate over files
	for _,filename := range encfiles {
		// try to open, fail silently if it doesn't exist
		file,err := os.Open(filename)
		if err != nil {
			// error opening this file, skip and...
			continue File
		}
		defer file.Close()

		// Read file line by line.
		// Dammit Go, isn't this something that one does often
		// enough to deserve the simplest way to do it???
		// Anyway, here we go with what one can find in
		// https://golang.org/pkg/bufio/#example_Scanner_lines
		scanner := bufio.NewScanner(file)
	Line:
		for scanner.Scan() {
			err := scanner.Err()
			if err != nil {
				// log.Printf("Error reading file %s: %s",filename,err)
				break Line
			}

			// no need to "chomp()" here, the newline is already gone
			line := scanner.Text()

The previous code snippet also includes the preparation of two regular expression patterns that will be used later in the game; this is a notable difference from Perl, where Perl is on the minimalistic side (one single instruction to do pattern matching, sub-match extraction and so forth), where Go introduces a number of functions and methods to do the same job. Regular expressions are an already complicated subject and don’t definitely need any additional mess: like many other languages, Go should take some lessons from Perl on this subject.

An area where Go tends to be cleaner than Perl is where you can use the built-in switch/case construct instead of if:

			case `+`:
				// add a class, assume id is a class name
				class[id] = 1

			case `-`:
				// undefine a class, assume id is a class name
				class[id] = -1

Perl’s equivalent given/when construct is still experimental; a Switch module is provided in CPAN and was in the core distribution in the past, but it’s use is discouraged in favour of the experimental given/when construct… uhm…

Switch/case aside, the Perl version of hENC was designed to run on any recent and not-so-recent Perl, so it uses the plain old if construct:

    if ($setting eq '+') {
	# $id is a class name, or should be.
	$class{$id} = 1 ;
    }

    # undefine a class
    if ($setting eq '-') {
	# $id is a class name, or should be.
	$class{$id} = -1 ;
    }

though there are still places where Perl is a bit clearer and more concise than Go:

    # reset the status of a class
    if ($setting eq '_') {
	# $id is a class name, or should be.
	delete $class{$id} if exists $class{$id} ;
    }

versus

			case `_`:
				// reset the class, if it's there
				_,ok := class[id]
				if ok {
					delete(class,id)
				}

You can find the full source of gohENC at the end of the post. By the way, the gohENC source is 140 lines with extensive comments (80 lines of actual code, nearly twice as Perl’s version).

Success!

I got gohENC completed through a few short sessions and it was time to test if it really worked. That was an easy task, since hENC comes with a test suite. All I had to do was to compile the Go source, replace the Perl version with the binary and run the tests:

$ prove --exec "sudo cf-agent -KC -f" ./henc_test.cf
./henc_test.cf .. ok   
All tests successful.
Files=1, Tests=8,  0 wallclock secs ( 0.05 usr  0.01 sys +  0.08 cusr  0.00 csys =  0.14 CPU)
Result: PASS

Success! gohENC is live!

Why not Java?

I have often considered Java, but never got to love it. I felt the language, the tools and the ecosystem were unnecessarily complicated and the Java programs I have used in the past didn’t make me love the language either.

Why not Python?

I have always been surrounded by “pythonists” and considered Python, too, but was kind of discouraged by the fact that Python 3’s popularity wasn’t really taking off, while learning Python 2.7 seemed like a waste of time because its successor was already there.

Why not Ruby?

The only time I touched Ruby was when I tried to write some Puppet facts: the code I saw at the time didn’t impress me and I tried to stay away from Ruby ever since.

Why not JavaScript?

Because I was unsure about how much I could use it to help me with my job, and outside of web pages anyway.

Why not PHP?

Ahahahahahahahahahah!

Why not Perl6?

Perl 6, the new kid on the block, seems great and powerful, but not really something that would add an edge in my CV unfortunately.

Source code for gohENC

package main

import "fmt"    // to print something...
import "os"     // to read command-line args, opening files...
import "bufio"  // to read files line by line, see https://golang.org/pkg/bufio/#example_Scanner_lines
// import "log"
import "regexp"


var class    = make(map[string]int)     // classes container
var variable = make(map[string]string)  // variables container

var encfiles = os.Args[1:]

func main() {
	// prepare the regex for matching the ENC setting
	settingRe := regexp.MustCompile(`^\s*([=\@%+-/_!])(.+)\s*$`)

	// prepare the regex for matching a variable assignment
	varRe := regexp.MustCompile(`^(.+?)=`)
	
File:
	// iterate over files
	for _,filename := range encfiles {
		// try to open, fail silently if it doesn't exist
		file,err := os.Open(filename)
		if err != nil {
			// error opening this file, skip and...
			continue File
		}
		defer file.Close()

		// Read file line by line.
		// Dammit Go, isn't this something that one does often
		// enough to deserve the simplest way to do it???
		// Anyway, here we go with what one can find in
		// https://golang.org/pkg/bufio/#example_Scanner_lines
		scanner := bufio.NewScanner(file)
	Line:
		for scanner.Scan() {
			err := scanner.Err()
			if err != nil {
				// log.Printf("Error reading file %s: %s",filename,err)
				break Line
			}

			// no need to "chomp()" here, the newline is already gone
			line := scanner.Text()

			// Dear Go, regular expression are already
			// complicated, there is absolutely NO need for you to
			// make them even more fucked up...
			// Sixteen functions to do pattern matching... so much
			// for your fucking minimalism!
			match := settingRe.FindStringSubmatch(line)

			setting,id := match[1],match[2]
			// log.Printf("setting: %s, value: %s",setting,id)

			switch setting {
			case `!`:
				// take a command
				switch id {
				case `RESET_ALL_CLASSES`:
					// flush the class cache
					// ...which means: kill all key/values
					// recorded in the classes map
					// In Go, you're better off overwriting the
					// new array, so...
					class = make(map[string]int)

				case `RESET_ACTIVE_CLASSES`:
					// remove active classes from the cache
					for k,v := range class {
						if v > 0 {
							delete(class,k)
						}
					}

				case `RESET_CANCELLED_CLASSES`:
					// remove cancelled classes from the cache
					for k,v := range class {
						if v < 0 { 							delete(class,k) 						} 					} 				} // switch id 			case `+`: 				// add a class, assume id is a class name 				class[id] = 1 			case `-`: 				// undefine a class, assume id is a class name 				class[id] = -1 			case `_`: 				// reset the class, if it's there 				_,ok := class[id] 				if ok { 					delete(class,id) 				} 			case `=`, `@`, `%`: 				// define a variable/list 				match := varRe.FindStringSubmatch(id) 				varname := match[1] // not necessary, just clearer 				variable[varname] = line 			case `/`: 				// reset a variable/list 				_,ok := variable[id] 				if ok { 					delete(variable,id) 				} 				 			} // switch setting 			// discard the rest 		} 	} 	// print out classes 	class[`henc_classification_completed`] = 1 	for classname,value := range class { 		switch { 		case value > 0:
			fmt.Printf("+%s\n",classname)

		case value < 0:
			fmt.Printf("-%s\n",classname)
		}
	}

	// print variable/list assignments, the last one wins
	for _,assignment := range variable {
		fmt.Println(assignment)
	}
}

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s