blog.haardiek.org

Jan 07, 2017

Infinite Sets in Go

If you are too lazy to read, but interested in using finite and infinite sets in Go, just take a look at the documentation or directly at the source of the library which inspired this article.

Some time ago I was working with Go and was missing the concept of set-like objects like I had in Python. After some very very short investigation I found an article about go maps in action which was describing how to build such sets very simple by using maps.

Inspired by that, I was thinking about the concept of sets in programming languages and realized that all the sets were defined explicitly by their elements. This is, in general, a good idea, since it makes such sets very fast and leads to a very easy structure definition.

But I wanted to have an implementation of sets which was more like humans define sets, especially infinite sets. The goal was not to implement something that is fast, but easy to use them like real sets.

This article is about the steps that followed.

Finite Sets

To begin with a set data structure, I was first implementing a struct set defined explicitly by its elements. I followed the go maps in action article I mentioned earlier.

type elementSet struct {
    elements map[interface{}]bool
}

The most basic method defines which elements are contained in the set.

func (set elementSet) Contains(x interface{}) (bool, error) {
    return set.elements[x], nil
}

Why this function returns a tuple of boolean and error although this error is obviously never set, will be explained later.

Also, I implemented the possibility to count the elements of a set and a helper function to create an explicit list of a function to make iteration a lot easier.

func (set elementSet) Cardinality() (uint64, error) {

var number uint64
for _, contained := range set.elements {
    if contained {
        number++
    }
}

    return number, nil
}

func (set elementSet) List() ([]interface{}, error) {

    number, err := set.Cardinality()
    if err != nil {
        return []interface{}{}, err
    }

    var list = make([]interface{}, 0, number)
    for element, contained := range set.elements {
        if contained {
            list = append(list, element)
        }
    }
    return list, nil
}

Again here are some errors returned which are always nil. And again, this will be explained later.

Infinite Sets

After I had implemented the finite sets, I wanted to work with infinite sets, but defining infinite sets explicitly via the elements is obviously impossible. Mathematicians which are using pen and paper to define sets would, for example, define the set of all integers which are divisible by 10 like this

$$\{ n \in \Bbb{N} : n \mod 10 = 0 \}$$

So my idea was to define the infinite sets in an equal way and define an infinite set via a function which describes which elements are contained. So I defined them as followed

type functionSet struct {
    contains func(interface{}) (bool, error)
}

func (set functionSet) Contains(x interface{}) (bool, error) {
    return set.contains(x)
}

I have to admit that this definition is not very practical and very generic, but this is also the case by the definition of sets in math. So it fits.

Also, you can see that this struct can not only hold infinite sets but also finite sets as well. There are no real limits to the defining function at all.

The Interface

So now we have two different definitions of sets. To be able to use both of them the same way and be able for example to create a union from a finite set defined by elementSet and an infinite set defined by functionSet, I created an interface for these implementations. Also after defining an interface we can add other definitions of sets very easily as well. It's Go, right?

type Set interface {
    Contains(x interface{}) (bool, error)
    Cardinality() (uint64, error)
    List() ([]interface{}, error)
}

Now the problem is that we can not define Cardinality() and List() for functionSet that easy, since this information are very hard to extract from the defining function and for real infinite set it would be even impossible to define a list of elements explicitly. I solved this by implementing some dummy functions for these which only have err != nil.

To make things easier I added a function to the interface to see if you are working with a set implementing Cardinality() and List() or not. So, therefore, the distinction is not any longer between infinite and finite sets, but countable and uncountable sets.

type Set interface {
    Contains(x interface{}) (bool, error)
    Cardinality() (uint64, error)
    Countable() bool
    List() ([]interface{}, error)
}

Now it should be clear why there is returned some errors which are always nil in the previous sections. This is to fit the interface which is written much more generic than needed for the two existing implementations.

Adapt Structs to fit the Set Interface

To fulfill the interface Set I had to add some methods to the structs elementSet and functionSet.

For elementSet I only needed to add the Countable() function which simply says true.

func (set elementSet) Countable() bool {
    return true
}

For functionSet I had to add the dummy function I talked about previously for Cardinality() and List() and add a Countable() function which simply says false.

func (set functionSet) Countable() bool {
    return false
}

func (set functionSet) Cardinality() (uint64, error) {
    return 0, errors.New("Not countable by design")
}

func (set functionSet) List() ([]interface{}, error) {
    return []interface{}{}, errors.New("Not listable by design")
}

Now our structs should fit the interface Set and we can start defining functions on the sets.

Functions on Sets

You can define a lot of functions on the Set interface, but I will only describe a small amount of them. If you want to know more about those functions, look at the source or the documentation of the library.

Creating Sets

To be able to create sets I added two functions which simply takes either an explicit list or a function and create an elementSet or functionSet out of it.

func CreateFromArray(list []interface{}) Set {
    set := elementSet{make(map[interface{}]bool)}
    for _, element := range list {
        set.elements[element] = true
    }
    return set
}

func CreateFromFunc(f func(interface{}) (bool, error)) Set {
    return functionSet{f}
}

Intersecting Sets

One of the most basic operation on sets is to intersect them with each other. Due to the possibility to differ countable sets and not countable sets, we are able to distinct two different situations.

One countable Set

Obviously, if one of the sets to intersect is countable the intersection should also be countable as a subset of a countable set. The following function is to create such an intersection. Since intersections are commutative, let's assume that the set a is countable.

func countableIntersection(a Set, b Set) (Set, error) {
    // Create new countable set
    newSet := elementSet{make(map[interface{}]bool)}
    // Explicit list of the elements of a
    elements, err := a.List()
    if err != nil {
        return newSet, err
    }
    // Check if the elements are also in b and therefor in the intersection
    for _, element := range elements {
        yes, err := b.Contains(element)
        if err != nil {
            return newSet, err
        }
        if yes {
            newSet.elements[element] = true
        }
    }
    return newSet, nil
}

No countable Set

If both sets to intersect are not countable the intersection could be implemented as a countable set, but this is very hard to proof and therefore it is defined as the combination of the defining Contains(x interface{}) functions.

func notCountableIntersection(a Set, b Set) (Set, error) {
    // both not countable
    newContains := func(x interface{}) (bool, error) {
        if yes, err := a.Contains(x); err != nil {
            return false, err
        } else if !yes {
            return false, nil
        }
        // If here, x is in a
        if yes, err := b.Contains(x); err != nil {
            return false, err
        } else if !yes {
            return false, nil
        }
        // if here, x is in b
        return true, nil
    }
    return functionSet{newContains}, nil
}

Now we only need some glue code to call one of these functions.

func Intersection(a Set, b Set) (Set, error) {
    // a is countable
    if a.Countable() {
        return countableIntersection(a, b)
    }
    // b is countable
    if b.Countable() {
        return countableIntersection(b, a)
    }
    // Both not countable
    return notCountableIntersection(a, b)
}

Other functions are following the same chain of thoughts.

Examples

To show what you can do with this library here some examples.

Natural numbers

We can now, for example, define the set of natural numbers as a subset of all strings. It is a little bit like you see it as a human.

import (
    "regexp"

    "github.com/shaardie/set"
)

var NatualNumbers = set.CreateFromFunc(func(x interface{}) (bool, error) {

    // Is a string
    str, ok := x.(string)
    if !ok {
        return false, nil
    }

    // Match what you would "see" as a natural number
    return regexp.MatchString("0|[1-9][0-9]*", str)
})

Also, we define a set of very evil numbers.

var evilNumbers = set.CreateFromArray([]interface{}{"1", "3", "21", "999"}

Now we can create the subset of the natural numbers which are good by creating the difference.

goodNumbers, _ := set.Difference(NatualNumbers, evilNumbers)

Here the full example

package main

import (
    "bufio"
    "fmt"
    "os"
    "regexp"

    "github.com/shaardie/set"
)

func main() {
    var NatualNumbers = set.CreateFromFunc(func(x interface{}) (bool, error) {

        // Is a string
        str, ok := x.(string)
        if !ok {
            return false, nil
        }

        // Match what you would "see" as a natural number
        return regexp.MatchString("0|[1-9][0-9]*", str)
    })

    var evilNumbers = set.CreateFromArray([]interface{}{"1", "3", "21", "999"})

    goodNumbers, _ := set.Difference(NatualNumbers, evilNumbers)

    fmt.Println("Please type a natural number")
    b, _, _ := bufio.NewReader(os.Stdin).ReadLine()
    number := string(b)

    if in, _ := goodNumbers.Contains(number); in {
        fmt.Printf("%v is a good number\n", number)
    } else {
        fmt.Printf("%v is an evil number\n", number)
    }
}

Conclusion

Creating finite and infinite sets in Go was canonical and due to the interface logic it is easy to expand this further with more complex solutions as well.

Creating other math related packages, for example, a package of different number set is something I would definitely do. Also creating some kind of algebra on top of these sets is something very interesting.

Sven Haardiek  ·   ·  Golang