February 20, 2018 at 05:20 Tags Go , Multiple dispatch

I've written about the expression problem here and later also here. Between them, these posts presented various solutions and non-solutions in C++, Haskell and Clojure.

Today I want to add another language into the mix - Go. It turns out that Go interfaces allow us to solve the expression problem, or at least a limited variant thereof.

The Go way Go's main vehicle of abstraction and polymorphism is interfaces. Go encourages the use of small interfaces, such that types could be easily implementing multiple interfaces if needed. It turns out this tool and philosophy is exactly suitable for tackling the expression problem in Go.

The expression problem in Go For a recap of the expression problem, please refer to the past posts linked at the top. Without further ado, here's a Go solution. type Expr interface { } // Types type Constant struct { value float64 } type BinaryPlus struct { left Expr right Expr } Note that the Expr interface is empty - any type implements it. We need something like this so that we can specify the types for the left and right members of BinaryPlus ; it really means left and right can be of any type. We'll get back to this discussion shortly. Now an interface for expressions we can evaluate: type Eval interface { Eval () float64 } And implementations for our existing types: func ( c * Constant ) Eval () float64 { return c . value } func ( bp * BinaryPlus ) Eval () float64 { return bp . left .( Eval ). Eval () + bp . right .( Eval ). Eval () } The instance for Constant is straightforward, but what's going on with BinaryPlus ? Recall that left and right can have arbitrary types at compile time. But at run-time, we need these to be Eval , hence the type casts. This code will fail with a cast error at run-time if the objects occupying b 's left or right slots don't, in fact, imlement Eval . Note that we could force Expr to have at least an Eval method - all expressions have to be evaluable, after all. This would make the Expr interface a bit less empty, and also would remove the need for casts in implementations of Eval . We'd still need casts for other interfaces though, as we'll see shortly. OK, now we have the basics. Let's add another operation, without modifying existing code: type Stringify interface { ToString () string } func ( c * Constant ) ToString () string { return strconv . FormatFloat ( c . value , 'f' , - 1 , 64 ) } func ( bp * BinaryPlus ) ToString () string { ls := bp . left .( Stringify ) rs := bp . right .( Stringify ) return fmt . Sprintf ( "(%s + %s)" , ls . ToString (), rs . ToString ()) } No surprises here. How about adding a new type: type BinaryMul struct { left Expr right Expr } func ( bm * BinaryMul ) Eval () float64 { return bm . left .( Eval ). Eval () * bm . right .( Eval ). Eval () } func ( bm * BinaryMul ) ToString () string { ls := bm . left .( Stringify ) rs := bm . right .( Stringify ) return fmt . Sprintf ( "(%s * %s)" , ls . ToString (), rs . ToString ()) } Again, very similar code to what we've written for other types. Finally, let's write some client code that uses these types and operations: func CreateNewExpr () Expr { c11 := Constant { value : 1.1 } c22 := Constant { value : 2.2 } c33 := Constant { value : 3.3 } bp := BinaryPlus { left : & BinaryPlus { left : & c11 , right : & c22 }, right : & c33 } return & bp } func main () { ne := CreateNewExpr () fmt . Printf ( "ne Eval = %g

" , ne .( Eval ). Eval ()) fmt . Printf ( "ne ToString = %s

" , ne .( Stringify ). ToString ()) } This snippet demonstrates how we can create new expressions at runtime (the return type of CreateNewExpr is simply Expr ) and observe their implementation of the interfaces we've defined. When run, it prints: ne Eval = 6.6 ne ToString = ((1.1 + 2.2) + 3.3)