DEV Community

Cover image for Go for Java developers
Nicola Apicella
Nicola Apicella

Posted on • Edited on

Go for Java developers

Last day I had to implement a task in go.

As you might already know from this post, I've recently started programming in Go, so quite often the first question which pops up in my mind is: How would I do it in Java?

I am not sure if it's good or not, but I find somehow beneficial for me to look at the same thing from multiple perspectives. It is not about using Go or Java, using functional or imperative style, it's in mixing different concepts in which I see the real value.

Similarly to the other post, in this one I'll document how looking at the problem from a different angle shaped the solution.

The problem

I am going to simply a bit the task to highlight the interesting bit in this post. Part of the task involved loading in memory a section of a file: a super simple parser.

The file looks like this:

// a comment here 
#HEADER 
Key1=value1 
//another comment 
Key2=value2 
Key3=value3 
Key4=value4 
Enter fullscreen mode Exit fullscreen mode

We are interested in two keys which belong to a section.
The section starts with an header, which begins with an hashtag.
We are given the name of the section and the name of two keys.
i.e. section: "person", keys : ["name", "surname"]
So we want to find the "person "section in the file and load the values for name and surname in memory.

Couples of gotchas about the file:

  • The Header might not be in the file
  • name and/or surname might not be present in the file
  • name always precedes surname in the file
  • It might contain comments (//) or other markers

One of the possible solution is simply to read the file line by line and:

  • if the line starts with # then this is the beginning of a section, prepare to read the key pair for this header
  • Read name and surname
  • Ignore comments and the rest of the keys

So, let's put on for a second the hat of a Java programmer here.

So reading a file, line by line, filtering some lines…

Alright this looks like a job for java.nio.file.Files, a java.util.stream.Stream and a custom java.util.function.Predicate to filter the stream.
I say custom, meaning that we need to keep some state within our predicate.
The state is simply what string the predicate should match next time it is called, i.e. if it already matched the header then should match name next.

This basically means two things: Stream and Lambda functions.

Let's make it short, there is no such a thing like a stream in Golang.
Let me make a little diversion here, the whole point of this mental exercise is not implementing something which goes against the language itself, but get the best out of it. So no stream. We are going to live without it, for loops to the rescue.

But what about lambdas?
Well, functions in Go are first class citizen. We can assign them to variables, create array of functions, pass them around like any other argument, so I guess we have our lambdas :)


Ok enough talk, let's see the code…
Wait before looking at the code, let's recap what we want to do.
So we expect to have a for loop in which we read the content from a source line by line and for each line we check if it should be written somewhere.
The bit which checks if the line should be written is a filter, which internally keeps a state.

All right so let's see this bit:

func copy(profileName string, in io.Reader, out io.Writer) error {
    var (
        line       string
        readError  error
        writeError error
    )

    profileFilter := newProfileFilter(profileName)
    reader := bufio.NewReader(in)

    for {
        line, readError = reader.ReadString('\n')

        if profileFilter.match(line) {
            _, writeError = io.WriteString(out, line)
        }

        if readError != nil || writeError != nil {
            break
        }
    }

     if writeError != nil {
        return writeError
     }

     if readError != io.EOF {
        return readError
     }

     return nil
}
Enter fullscreen mode Exit fullscreen mode

This is pretty much "equivalent" to the Java

Files.lines(resource())
             .filter(profileFilter)
             .forEach(ProfileWriter::write); 
Enter fullscreen mode Exit fullscreen mode

Let's break it down a bit:

  • Files.lines is the for loop
  • The filter is the if condition profileFilter.match(line)
  • The forEach is simply the body of the if condition io.WriteString(out, line)

So let's look at the filter:

func newProfileFilter(profileName string) *profileFilter {
    var matchers [](func(line string) bool)

    matchers = append(matchers,
        matcher(startsWith).apply("#"+profileName),
        matcher(startsWith).apply("name="),
        matcher(startsWith).apply("surname="),
    )

    return &profileFilter{matchers, profileName}
}

func startsWith(line string, toMatch string) bool {
    return strings.HasPrefix(
        strings.ToLower(strings.TrimSpace(line)),
        strings.ToLower(toMatch),
    )
}
Enter fullscreen mode Exit fullscreen mode

This is the constructor of profileFilter, responsible to detect the header of the section and the keys. It's basically an array of functions or lambdas if you will. Every time the filter matches a line in the file, we pop an element from the array, so in the next iteration the filter is going to match on a different string.
What I really like from this code is the way we construct the lambdas:

matcher(startsWith).apply("name=")

Enter fullscreen mode Exit fullscreen mode

So we are building a matcher which matches a line that startsWith "name="
What's really a matcher?

type matcher func(line string, toMatch string) bool

Enter fullscreen mode Exit fullscreen mode

It's a type alias for a function. So it's just a function which gets as input a line and the string to match and return a boolean. That's it.
The syntax matcher(startsWith) is just a cast, we are casting the function startsWith to a matcher. Sure enough we can do that, since matcher is a function!

when I wrote that, I was like...well, functions in go are pretty damn cool

What's with that apply method?

func (f matcher) apply(toMatch string) func(line string) bool {
    return func(line string) bool {
        return f(line, toMatch)
    }
}
Enter fullscreen mode Exit fullscreen mode

It's just a way to curry the function. Long story short, we have a function of two arguments, and we really want a function with one argument in which the other one is already set.

So here, we apply the string we want to match, like "name=" and we get back a function which has as parameter only the line.
We attached the apply method to the matcher, so the end result is that by calling:

matcher(startsWith).apply("name=") 
Enter fullscreen mode Exit fullscreen mode

We get back a function which returns a boolean (a Predicate in java land) which is going to be true if the line starts with name.

This is the rest of the code here just for the sake of completeness.

type profileFilter struct {
    matchers    []func(line string) bool
    profileName string
}

func (p *profileFilter) match(text string) bool {
    if len(p.matchers) == 0 {
        return false
    }

    shouldFilter := p.matchers[0](text)

    if shouldFilter {
        p.matchers = p.matchers[1:len(p.matchers)]
    }

    return shouldFilter
}
Enter fullscreen mode Exit fullscreen mode

Java version

While writing the article I decided that for fun I could actually write the same program in Java.
Here is a quick implementation:

    public static void main(String[] args) throws IOException, URISyntaxException{
        Matcher startsWith = (line, toMatch) -> startsWith(line, toMatch);

        ProfileFilter profileFilter =
            new ProfileFilter(
                startsWith.apply("#some-profile"),
                startsWith.apply("key2="),
                startsWith.apply("key3=")
            );

        Files.lines(resource())
             .filter(profileFilter)
             .forEach(System.out::println);
    }

    static class ProfileFilter implements Predicate<String> {
        private LinkedList<Predicate<String>> predicates;

        ProfileFilter(Predicate<String>...predicates) {
            this.predicates = new LinkedList<>(Arrays.asList(predicates));
        }

        @Override
        public boolean test(String s) {
            if (predicates.size() == 0) {
                return false;
            }

            boolean shouldFilter = predicates.getFirst().test(s);

            if (shouldFilter) {
                predicates.removeFirst();
            }

            return shouldFilter;
        }
    }

    interface Matcher extends BiPredicate<String, String> {

        default Predicate<String> apply(String applied) {
            return s -> this.test(s, applied);
        }
    }

    static Boolean startsWith(String line, String toMatch) {
        return line.toLowerCase().trim().startsWith(toMatch.toLowerCase());
    }
Enter fullscreen mode Exit fullscreen mode

The two versions are obviously really similar, except for the fact that in the java version I use stream instead of the for loop and I print the lines on the console instead of writing it to a Writer.

A side note here: Stream are really cool, but they do not get along with Exceptions. Writing to a File for example is one of those operations which throws an exception, so in a real use case I think I would either use a for loop instead of a stream or RxJava Observable.

Conclusions

All right, this turned out to be longer than I expected, considering I wanted just to say that probably Golang isn't that bad XD.
Joke aside, hope you liked it.

Thanks!

Resources

Code for the article: github
I found two interesting talks about functions in Golang.
Totally worth it looking at them:
Closures are the generics of go
Do not feat the first class function

Top comments (2)

Collapse
 
theodesp profile image
Theofanis Despoudis

Great job. However I'm curious about one thing. Why does the match need to modify the internal matchers state? If you call it 2 times with the same reference object and text it will return different results. For example

pf := newProfileFilter("Theo")
fmt.Println(pf.match("#Theo")) // True
fmt.Println(pf.match("#Theo")) // False ??? Side effects???

Collapse
 
napicella profile image
Nicola Apicella

Hello! Thank you :)

So yes it modifies the internal state. The idea is to keep a state within the profileFilter so it matches the header, name and surname in the order.
i.e. after matching the header, the filter must transition in a state in which is going to match 'name' next, and so on.
It has side side effects.

We can transform it in a pure function(no side effects) by passing the filter state itself as additional argument.

Something along those lines:
pf := newProfileFilter("Theo")
isAMatch, pf = pf.match("#Theo", pf)

The match function returns two values, a boolean as before and the profileFilter itself.
With this change the match function is pure because given the same input returns the same output.

Hope this answer your question.