sdboyer / transducers-go

Transducers for Go
47 stars 1 forks source link

Transducers for Go

Build Status

This is an implementation of transducers, a concept from Clojure, for Go.

Transducers can be tricky to understand with just an abstract description, but here it is:

Transducers are a composable way to build reusable algorithmic transformations.

Transducers were introduced in Clojure for a sorta-similar reason that range exists in Go: having one way of writing element-wise operations on channels and other collection structures (though that's just the tip of the iceberg).

I'm honestly not sure if I these are a good idea for Go. I've written this library as an exploratory experiment in their utility for Go, and would love feedback.

UPDATE: After much reflection, I’m pretty sure they’re a bad idea. They’re just not worth departing the island of type-safety. Still open to being convinced otherwise :)

What Transducers are

There's a lot out there already, and I don't want to duplicate that here. Here are some bullets to quickly orient you:

Beyond that, here's some resources (mostly in Clojure):

Pudding <- Proof

I'm calling this proof of concept "done" because it can pretty much replicate (expand the ClojureParity example) a thorough demo case Rich Hickey put out there.

Here's some quick eye candy, though:

// dot import for brevity, remember this is a nono
import . "github.com/sdboyer/transducers-go"

func main() {
    // To make things work, we need four things (definitions in glossary):
    // 1) an input stream
    input := Range(4) // ValueStream containing [0 1 2 3]
    // 2) a stack of Transducers
    transducers := []Transducer{Map(Inc), Filter(Even)} // increment then filter odds
    // 3) a reducer to put at the bottom of the transducer stack
    reducer := Append() // very simple reducer - just appends values into a []interface{}
    // 4) a processor that puts it all together
    result := Transduce(input, reducer, transducers...)

    fmt.Println(result) // [2 4]

    // Or, we can use the Go processor, which does the work in a separate goroutine
    // and returns results through a channel.

    // Make an input chan, and stream each value from Range(4) into it
    in_chan := make(chan interface{}, 0)
    go StreamIntoChan(Range(4), in_chan)

    // Go provides its own bottom reducer (that's where it sends values out through
    // the return channel). So we don't provide one - just the input channel.
    out_chan := Go(in_chan, 0, transducers...)
    // Note that we reuse the transducer stack declared for the first example.
    // THIS. THIS is why transducers are cool.

    result2 := make([]interface{}, 0) // zero out the slice
    for v := range out_chan {
        result2 = append(result2, v)
    }

    fmt.Println(result) // [2 4]

}

Remember - what's important here is not the particular problem being solved, or the idiosyncracies of the Transduce or Go processors (you can always write your own). What's important is that we can reuse the Transducer stack we declared, and it works - regardless of eagerness vs laziness, parallelism, etc. That's what breaking down transformations into their smallest constituent parts gets us.

I also worked up another more sorta-real example in response to an idea on the mailing list: using transducers to decode signals from airplane transponders.

The Arguments

I figure there's pros and cons to something like this. Makes sense to put em up front.

Please feel free to send PRs with more things to put in this section :)

Cons

Pros

Glossary

Transducers have some jargon. Here's an attempt to cut it down. These go more or less in order.