Living Systems_

How I would like to write Go programs

Some time ago I got a post published on GopherAcademy , outlining in detail how I think a flow-based programming inspired syntax can strongly help to create clearer, easier-to-maintain, and more declarative Go programs.

These ideas have since became clearer, and we (Ola Spjuth ’s research group at pharmbio ) have successfully used them to make the workflow syntax for Luigi (Spotify’s great workflow engine by Erik Bernhardsson & co) workflows easier, as implemented in the SciLuigi helper library .

I even blogged the other day , about how I think this kind of syntax would help untangle the syntax used in most functional languages. But the final code example in that blog post was a toy example in python, that didn’t work, since python doesn’t have the required concurrent features to make it work.

So, the idea struck me tonight that it might be easy enough to implement the same example in Go, using the most up-to-date ideas for the syntax. One hour later, I have a working Go example!

Find it in this gist , golang play or in the pasted version just below! Watch out especially for those lines connecting the (asynchronous) processes together. That is, these lines:

split.In  = hisay.Out
lower.In  = split.OutLeft
upper.In  = split.OutRight
zippr.In1 = lower.Out
zippr.In2 = upper.Out
prntr.In  = zippr.Out

In summary: This is how I want to write composable Go programs! (Especially if I can find a nicer way to drive the network, than a hard-coded for-loop ;) - ideas welcome)

And so the full code example:

package main

import (
    "fmt"
    "math"
    "strings"
)

const (
    BUFSIZE = 16
)

// ======= Main =======

func main() {
    // Init processes
    hisay := NewHiSayer()
    split := NewStringSplitter()
    lower := NewLowerCaser()
    upper := NewUpperCaser()
    zippr := NewZipper()
    prntr := NewPrinter()

    // Network definition *** This is where to look! ***
    split.In = hisay.Out
    lower.In = split.OutLeft
    upper.In = split.OutRight
    zippr.In1 = lower.Out
    zippr.In2 = upper.Out
    prntr.In = zippr.Out

    // Set up processes for running (spawn go-routines)
    go hisay.Run()
    go split.Run()
    go lower.Run()
    go upper.Run()
    go zippr.Run()
    prntr.Run()

    println("Finished program!")
}

// ======= HiSayer =======

type hiSayer struct {
    Out chan string
}

func NewHiSayer() *hiSayer {
    return &hiSayer{Out: make(chan string, BUFSIZE)}
}

func (proc *hiSayer) Run() {
    defer close(proc.Out)
    for _, i := range []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} {
        proc.Out <- fmt.Sprintf("Hi for the %d:th time!", i)
    }
}

// ======= StringSplitter =======

type stringSplitter struct {
    In       chan string
    OutLeft  chan string
    OutRight chan string
}

func NewStringSplitter() *stringSplitter {
    return &stringSplitter{
        OutLeft:  make(chan string, BUFSIZE),
        OutRight: make(chan string, BUFSIZE),
    }
}

func (proc *stringSplitter) Run() {
    defer close(proc.OutLeft)
    defer close(proc.OutRight)
    for s := range proc.In {
        halfLen := int(math.Floor(float64(len(s)) / float64(2)))
        proc.OutLeft <- s[0:halfLen]
        proc.OutRight <- s[halfLen:len(s)]
    }
}

// ======= LowerCaser =======

type lowerCaser struct {
    In  chan string
    Out chan string
}

func NewLowerCaser() *lowerCaser {
    return &lowerCaser{Out: make(chan string, BUFSIZE)}
}

func (proc *lowerCaser) Run() {
    defer close(proc.Out)
    for s := range proc.In {
        proc.Out <- strings.ToLower(s)
    }
}

// ======= UpperCaser =======

type upperCaser struct {
    In  chan string
    Out chan string
}

func NewUpperCaser() *upperCaser {
    return &upperCaser{Out: make(chan string, BUFSIZE)}
}

func (proc *upperCaser) Run() {
    defer close(proc.Out)
    for s := range proc.In {
        proc.Out <- strings.ToUpper(s)
    }
}

// ======= Merger =======

type zipper struct {
    In1 chan string
    In2 chan string
    Out chan string
}

func NewZipper() *zipper {
    return &zipper{Out: make(chan string, BUFSIZE)}
}

func (proc *zipper) Run() {
    defer close(proc.Out)
    for {
        s1, ok1 := <-proc.In1
        s2, ok2 := <-proc.In2
        if !ok1 && !ok2 {
            break
        }
        proc.Out <- fmt.Sprint(s1, s2)
    }
}

// ======= Printer =======

type printer struct {
    In chan string
}

func NewPrinter() *printer {
    return &printer{}
}

func (proc *printer) Run() {
    for s := range proc.In {
        fmt.Println(s)
    }
}

And if we run it, we get:

$ go run dataflow_syntax_test.go
hi for the  1:TH TIME!
hi for the  2:TH TIME!
hi for the  3:TH TIME!
hi for the  4:TH TIME!
hi for the  5:TH TIME!
hi for the  6:TH TIME!
hi for the  7:TH TIME!
hi for the  8:TH TIME!
hi for the  9:TH TIME!
hi for the  10:TH TIME!
Finished program!