2

I'm trying to port an algorithm from Python to Go. The central part of it is a tree built using dicts, which should stay this way since each node can have an arbitrary number of children. All leaves are at the same level, so up the the lowest level the dicts contain other dicts, while the lowest level ones contain floats. Like this:

tree = {}
insert(tree, ['a', 'b'], 1.0)
print tree['a']['b']

So while trying to port the code to Go while learning the language at the same time, this is what I started with to test the basic idea:

func main() {
    tree := make(map[string]interface{})
    tree["a"] = make(map[string]float32)
    tree["a"].(map[string]float32)["b"] = 1.0
    fmt.Println(tree["a"].(map[string]float32)["b"])
}

This works as expected, so the next step was to turn this into a routine that would take a "tree", a path, and a value. I chose the recursive approach and came up with this:

func insert(tree map[string]interface{}, path []string, value float32) {
    node := path[0]
    l := len(path)
    switch {
    case l > 1:
        if _, ok := tree[node]; !ok {
            if l > 2 {
                tree[node] = make(map[string]interface{})
            } else {
                tree[node] = make(map[string]float32)
            }
        }
        insert(tree[node], path[1:], value) //recursion
    case l == 1:
        leaf := tree
        leaf[node] = value
    }
}

This is how I imagine the routine should be structured, but I can't get the line marked with "recursion" to work. There is either a compiler error, or a runtime error if I try to perform a type assertion on tree[node]. What would be the correct way to do this?

elpres
  • 416
  • 5
  • 12
  • Usage of `interface{}` in Go? See https://stackoverflow.com/questions/23148812/whats-the-meaning-of-interface/62337836#62337836. –  Jul 10 '20 at 08:24

2 Answers2

7

Go is perhaps not the ideal solution for generic data structures like this. The type assertions make it possible, but manipulating data in it requires more work that you are used to from python and other scripting languages.

About your specific issue: You are missing a type assertion in the insert() call. The value of tree[node] is of type interface{} at that point. The function expects type map[string]interface{}. A type assertion will solve that.

Here's a working example:

package main

import "fmt"

type Tree map[string]interface{}

func main() {
    t := make(Tree)
    insert(t, []string{"a", "b"}, 1.0)

    v := t["a"].(Tree)["b"]
    fmt.Printf("%T %v\n", v, v)

    // This prints: float32 1
}

func insert(tree Tree, path []string, value float32) {
    node := path[0]
    len := len(path)

    switch {
    case len == 1:
        tree[node] = value

    case len > 1:
        if _, ok := tree[node]; !ok {
            tree[node] = make(Tree)
        }

        insert(tree[node].(Tree), path[1:], value) //recursion
    }
}

Note that I created a new type for the map. This makes the code a little easier to follow. I also use the same 'map[string]interface{}` for both tree nodes and leaves. If you want to get a float out of the resulting tree, another type assertion is needed:

leaf := t["a"].(Tree)["b"]  // leaf is of type 'interface{}`.
val := leaf.(float32)
jimt
  • 25,324
  • 8
  • 70
  • 60
  • Thank you very much! I don't seem to have understood the concept of `interface{}` yet, or the typing system in Go in general, but your example really helps illustrate the idea. – elpres Jun 05 '12 at 16:56
  • The code I need that for is right now in Python, but it uses lots of memory and could be easily parallelized, but then each process gets its own copy of the look-up tree, which can reach up to 1Gb. I hope Go with its easy concurrency and memory sharing between goroutines will help bring down memory usage as well as execution time, which is about 20 hours per run now. Besides, I've been waiting for an opportunity to try Go for a while now, and this seems like a good fit. – elpres Jun 05 '12 at 17:05
  • 2
    I would also consider the answer @jorelli provided. With some fixes for your particular use case, it is probably a better solution in Go. As he points out, it is often not a good idea to write a 1:1 translation of the solution in language X to language Y. Go has a different way of doing things. Digging a little deeper into it will give you other ideas on how to solve your problem. – jimt Jun 05 '12 at 21:51
6

well... the problem is that you're trying to code Go using Python idioms, and you're making a tree with... hashtables? Huh? Then you have to maintain that the keys are unique and do a bunch of otherstuff, when if you just made the set of children a slice, you get that sort of thing for free.

I wouldn't make a Tree an explicit map[string]interface{}. A tree and a node on a tree are really the same thing, since it's a recursive datatype.

type Tree struct {
    Children []*Tree
    Value    interface{}
}

func NewTree(v interface{}) *Tree {
    return &Tree{
        Children: []*Tree{},
        Value:    v,
    }
}

so to add a child...

func (t *Tree) AddChild(child interface{}) {
    switch c := child.(type) {
    case *Tree:
        t.Children = append(t.Children, c)
    default:
        t.Children = append(t.Children, NewTree(c))
    }
}

and if you wanted to implement some recursive function...

func (t *Tree) String() string {
    return fmt.Sprint(t.Value)
}

func (t *Tree) PrettyPrint(w io.Writer, prefix string) {
    var inner func(int, *Tree)
    inner = func(depth int, child *Tree) {
        for i := 0; i < depth; i++ {
            io.WriteString(w, prefix)
        }
        io.WriteString(w, child.String()+"\n") // you should really observe the return value here.
        for _, grandchild := range child.Children {
            inner(depth+1, grandchild)
        }
    }
    inner(0, t)
}

something like that. Any node can be made the root of some tree, since a subtree is just a tree itself. See here for a working example: http://play.golang.org/p/rEx43vOnXN

There are some articles out there like "Python is not Java" (http://dirtsimple.org/2004/12/python-is-not-java.html), and to that effect, Go is not Python.

jorelli
  • 8,064
  • 4
  • 36
  • 35
  • and to answer your question... every interface value also has an underlying type. You can use a type assertion to assert that an interface{} value is of specific type ("effective go" talks about this in detail; be sure to familiarize yourself with the "comma ok" idiom), or you can use a "type switch" to take action based on the interface{} value's underlying type. There's a more in-depth article on the topic here: http://research.swtch.com/interfaces – jorelli Jun 05 '12 at 21:35
  • Thanks. I used a hash table for the tree because each node may have a random amount of children (up to 20-ish), and hash tables would ensure constant look-up times instead of linear in case of a slice. What the tree is used for is a lot of look-ups (in the order of hundreds of millions), so bringing down the time needed for that is definitely a priority. In the end it's gonna depend on how fast a linear search in a slice is compared a hash table look-up. – elpres Jun 06 '12 at 07:29
  • you could also make the Children field of the Tree struct a map instead of a slice if the lookup time in the children proves to be a bottleneck. – Jeremy Wall Jun 07 '12 at 03:47