Let's assume we have two functions which will take some time (or http calls that naturally return Futures) and we need to aggregate their results.
The Scala way
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//assume these take time | |
def longComp1(): Int = 5 | |
def longComp2(): Int = 6 | |
def aggregate(i: Int, j: Int): Unit = { | |
println(s"i is $i, j is $j") | |
} | |
val longComp1Result = Future { longComp1() } | |
val longComp2Result = Future { longComp2() } | |
val aggregatedFuture: Future[Unit] = for { | |
r1 <- longComp1Result | |
r2 <- longComp1Result | |
} yield aggregate(r1, r2) |
The Go way
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//define a type for brevity below | |
type LongComp func() int | |
longComp1 := func() int { | |
return 5 | |
} | |
longComp2 := func() int { | |
return 6 | |
} | |
aggregate := func(is []int) { | |
fmt.Println(is) | |
} | |
f := func(fs []LongComp, agg func([]int)) { | |
channels := []chan int{} | |
//loop through the functions | |
for _, fun := range fs { | |
//create a channel for each and load it's result in | |
c := make(chan int) | |
channels = append(channels, c) | |
go func(f LongComp) { | |
c <- f() | |
close(c) | |
}(fun) | |
} | |
//get all the elements off the channel and apply the aggregate function | |
go func() { | |
results := []int{} | |
for _, c := range channels { | |
results = append(results, <-c) | |
} | |
agg(results) | |
}() | |
} | |
f([]LongComp{longComp1, longComp2}, aggregate) |
It's rather clunky compared to Scala's solution. But the main problem is that it's not polymorphic on the types. The same lump of code would need to be redefined every time the function signatures change.
Here is a more generic version below that uses explicit type casting.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
longComp1 := func() int { | |
return 5 | |
} | |
longComp2 := func() int { | |
return 6 | |
} | |
aggregate := func(is []int) { | |
fmt.Println(is) | |
} | |
type LongComp2 func() interface{} | |
//the "generic" function that works regardless of the underlying type | |
f2 := func(fs []LongComp2, agg func([]interface{})) { | |
channels := []chan interface{}{} | |
for _, fun := range fs { | |
c := make(chan interface{}) | |
channels = append(channels, c) | |
go func(f LongComp2) { | |
c <- f() | |
close(c) | |
}(fun) | |
} | |
go func() { | |
results := []interface{}{} | |
for _, c := range channels { | |
results = append(results, <-c) | |
} | |
agg(results) | |
}() | |
} | |
//functions needs to be transformed to accept and return interface{} | |
//transform a ()->int function to a ()->interface{} function | |
//without changing behaviour | |
wrap := func(f func() int) func() interface{} { | |
return func() interface{} { | |
return f() | |
} | |
} | |
f2([]LongComp2{ | |
wrap(longComp1), | |
wrap(longComp2)}, | |
//transform the aggregate function to accept []interface{} instead of []int | |
func(xs []interface{}) { | |
is := []int{} | |
for _, x := range xs { | |
is = append(is, x.(int)) | |
} | |
aggregate(is) | |
}) |
Not the nicest code I've ever written. If only Go had generics... Any Golang expert out there who could suggest improvements?
The Clojure Way
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns home.async-play | |
(:require [clojure.core.async :as async])) | |
(defn long-comp1 [] 5) | |
(defn long-comp2 [] 6) | |
(defn aggregate [xs] | |
(println xs)) | |
;;simple version, working with 2 functions | |
(let [c1 (async/go (long-comp1)) | |
c2 (async/go (long-comp2))] | |
(async/go | |
(aggregate [(async/<! c1) (async/<! c2)]))) | |
;;generalized version, working with a sequence of functions | |
(defn fs->chans | |
"Returns channels holding the result of each function execution" | |
[fs] | |
(for [f fs] (async/go (f)))) | |
(let [channels (fs->chans [long-comp1 long-comp2])] | |
(async/go | |
(aggregate (map async/<!! channels)))) |
Clojure borrowed its go-routine and channels idea from Golang. Yet the difference between the Clojure and Go versions in terms of brevity is striking. It's the result of 3 independent factors. One, Clojure is dynamically typed, hence there is no need to hassle with neither function signatures nor generics, or rather the lack of them. Two, it's a functional language. Maps and list comprehensions are way more terse than Go's for-loops. The third factor is async/go returns a channel containing the result of the function executed. Go needs to create a slice of channels first, loop through the slice of functions, create anonymous functions in go blocks where the function is executed and its result is put on the channel, ....lot's of hassle.
With generics many of Go's problems would go away. Rob Pike explicitly said, no generics, but hopefully someone will just force him or do it himself instead.
I'd recommend looking to manifold deferreds https://github.com/ztellman/manifold/blob/master/docs/deferred.md
ReplyDelete