“This is the first day of my participation in the First Challenge 2022. For details: First Challenge 2022.”
Today let’s use some real macros in the build process! This brings us back to the command line tools of the ’90s, and some of the things that those versed in C language preprocessors might remember.
First, let’s create a context for the question:
We’ll revisit functional programming favorites: MAP, FILTERFOLDL. We will choose two simple operations: map/filter. We will be using C language preprocessors, so our macros will look like C macros. Of course, you are welcome to use M4 or other more generic preprocessors, perhaps you can write your own.
start
The preparation for this article is straightforward: Start a Go project and write a Makefile. We’ll start with this directory structure.
. ├ ─ ─ app. Cgo ├ ─ ─. Mod ├ ─ ─ macros │ └ ─ ─ functions provides. H └ ─ ─ ─ a makefileCopy the code
Take a look at app.cgo:
package main
import (
"fmt"
"math/rand"
)
type Demo struct {
A int
}
func main(a) {
as := []Demo{}
for i := 0; i < 1000; i++ {
as = append(as, demo.Demo{A: rand.Intn(100)}}}Copy the code
Note: Normally we would use the traditional for I := range array. We didn’t do this above because we could better test our macros:
bs := []Demo{}
for i := range as {
if as[i].A < 50 {
bs = append(bs, as[i])
}
}
fmt.Printf("bs=%d demos\n".len(bs))
Copy the code
On the machine I ran, 506 elements were eventually inserted into the BS. Your results may be different, but should be consistent across multiple runs (RAND isn’t really random).
But before we start the experiment, there’s one more thing to do: add to our Makefile
all:
cat app.cgo | perl -np -e 's{^\t*//(\#.+)$$}{$$1}' | gcc -P -traditional -E - 2>/dev/null | cat -s | goimports 1>app.go
Copy the code
Note: If you are new to makefiles, commands under the all command must be preceded by a TAB character (\t), not a string of consecutive Spaces.
The makefile describes: The C preprocessor runs our app.cgo file and enters the processing results into app.go.
perl -np ...
→ allows us to run gofmt or goimports on cGO files without reporting them#include ""
Is an invalid GO syntax (but it is in GO). This will remove the GO comment (//) and place the macro at the top of the line to avoid any preprocessor issues.gcc -P ...
→ Run C language preprocessorcat -s
→ Remove long lines of codegoimports 1>app.go
To write inapp.go
It ran beforegoimports
So what’s the result? First take a look at:
package main
import (
"fmt"
"math/rand"
)
type Demo struct {
A int
}
func main(a) {
as := []Demo{}
for i := 0; i < 1000; i++ {
as = append(as, Demo{A: rand.Intn(100)})
}
bs := []Demo{}
for i := range as {
if as[i].A < 50 {
bs = append(bs, as[i])
}
}
fmt.Printf("bs=%d demos\n".len(bs))
}
Copy the code
Pretty much what we expected above. So that’s it. Here’s what looks like magic.
MACROS!
As you may have noticed above, we have a folder called Macros that contains a function.h file. Add this file to your editor.
Filter
Then let’s write a FILTER macro. We’ll follow the C convention and make the macro fully uppercase.
#define FILTER(arr, condition, type) \
func(arr []type) []type { \
zs := []type{}; \
for i := range arr {; \
ifcondition { \ zs = append(zs, arr[i]); \} \}; \ return zs; \ }(arr)
Copy the code
You’ll notice two things:
- Lots of semicolons. This is because C macros do not extend to new lines (though you can use \u000A to get new lines, depending on you).
- We are using function macros, of course you can use other options, but stick to that syntax for this article.
Then we use this macro:
// #include "macros/functions.h"
func main(a) {
as := []Demo{}
for i := 0; i < 1000; i++ {
as = append(as, Demo{A: rand.Intn(100)})
}
bs := []Demo{}
for i := range as {
if as[i].A < 50 {
bs = append(bs, as[i])
}
}
fmt.Printf("bs=%d demos\n".len(bs))
cs := FILTER(as, as[i].A < 50, Demo)
fmt.Printf("cs=%d demos\n".len(cs))
}
Copy the code
Then you run make and get the new app.go:
func main(a) {
as := []Demo{}
for i := 0; i < 1000; i++ {
as = append(as, Demo{A: rand.Intn(100)})
}
bs := []Demo{}
for i := range as {
if as[i].A < 50 {
bs = append(bs, as[i])
}
}
fmt.Printf("bs=%d demos\n".len(bs))
cs := func(as []Demo) []Demo {
zs := []Demo{}
for i := range as {
if as[i].A < 50 {
zs = append(zs, as[i])
}
}
return zs
}(as)
fmt.Printf("cs=%d demos\n".len(cs))
}
Copy the code
Of course, this can also be done without closures, but it protects us from being confused with other variables in scope. When you run the application, you get this result (depending on the machine):
bs=506 demos
cs=506 demos
Copy the code
But we’re not sure if this is actually true. A check needs to be made:
if len(cs) ! =len(bs) {
panic("cs and bs differ")}for i := range bs {
ifbs[i] ! = cs[i] { fmt.Printf("index %d differs, b=%d,c=%d\n", i, bs[i], cs[i])
errors++
}
}
Copy the code
Map
Then let’s see: MAP.
#define MAP(arr, condition, member, type, returnType) \
func(arr []type) []returnType { \
zs := []returnType{}; \
for i := range arr {; \
ifcondition { \ zs = append(zs, member); \} \}; \ return zs; \ }(arr)
Copy the code
You’ll see more parameters in this macro. This is so that we can generate code that properly describes the type. Then insert the code that uses the MAP macro into app.cgo:
js := MAP(as, as[i].A < 50, as[i].A, Demo, int)
fmt.Println("js=%d ints\n".len(js))
Copy the code
Running make generates additional code:
js := func(as []Demo) []int {
zs := []int{}
for i := range as {
if as[i].A < 50 {
zs = append(zs, as[i].A)
}
}
return zs
}(as)
fmt.Printf("js=%d ints\n".len(js))
Copy the code
The makefile option
So far our makefile is pretty basic, there are plenty of articles on the web that teach you how to iterate over all *.cgo files in the repo in a Makefile, so you don’t have to list every CGO file in your Makefile, and you don’t have to worry about keeping it up to date.
While writing this article, it’s helpful to have two rules, make Run and make Debug, to build and run the application, respectively, and to look at the preprocessor output:
debug:
cat app.cgo | perl -np -e 's{^\t*//(\#.+)$$}{$$1}' | gcc -P -CC -traditional -E - 2>/dev/null | cat -s | tee /dev/tty | goimports
run: all
if [[ -f app.go ]]; then go run app.go; rm app.go; else echo "app.go failed to generate"; fi
Copy the code
It’s much cleaner than using these types of code generation, and if a bug is found or something slightly different is needed, it’s also easier to fix, with a lot fewer template files and just focus on how the functionality is done.
Ok, so that’s macro! The whole story. You might want to try adapting the current operations of the collection data structure to the macro operations described above.