aichaos / rivescript-go

A RiveScript interpreter for Go. RiveScript is a scripting language for chatterbots.
https://www.rivescript.com/
MIT License
60 stars 16 forks source link

how did you find golang? #2

Open dcsan opened 8 years ago

dcsan commented 8 years ago

just wondering how you liked golang for this project? I'm a bit fedup wtih JS but not sure where to jump next. go seems much more C like and strict - for example parsing JSON you have to declare structs in advance... so maybe not as good for moving fast. but with pointers and other features it gives more fine grained control. presumably goroutines obviate the need for callback hell too... but for something as freeform a dialect as rivescript, how did you find writing the interpreter compared to say the python port?

kirsle commented 8 years ago

I really like Go so far. Writing this module in it was about as pleasant as writing it in the other languages (except Java). I used the CoffeeScript source as reference when writing this one, but due to the way Go packages work I could be a bit more liberal in breaking up functionality into multiple, more specifically named files, which I couldn't do in the other languages that care more about file names (e.g., where code from each file is isolated from the others unless explicitly imported).

For this RiveScript module, I like Go because it can link pretty well with C, at least in theory. This version should be the last port of RiveScript that I write by hand; for other languages I should be able to use a C extension binding to this module for them to use. I have the rsgb repo for experimenting with this, but the only thing there for now is a JavaScript "binding" generated with gopherjs.

A place where I'm really liking Go is actually in my Scarecrow chatbot. Goroutines are the best thing about the language; with them I can bolt anything onto anything, without worrying about how the things were written or if they'll play well with each other. For example, right now I'm playing with the idea of adding a web server front-end to Scarecrow using the standard HTTP library. In most programming languages and their frameworks (such as, say, Flask for Python), it would be very difficult to write a web server app that also does non-server things in the background, like manage Slack and XMPP bots. Most server frameworks are built around the purpose of serving stateless HTTP requests.

Most Go web servers you'll find code to have a main() function that ends up doing the http.ListenAndServe() to start serving requests. This call blocks forever and acts as a sort of main loop, like other web frameworks have. But, in Scarecrow's case, I can just write my own StartServer() function, and run it as a goroutine, so that it's off in its own corner doing its thing and has no impact on the Slack and XMPP bots.

When I worked on chatbots in Perl and Python I repeatedly ran into issues with event loop frameworks; I would write my bot to connect to a certain number of Instant Messenger platforms like AIM and IRC, and then want to add XMPP or something, but all of the modules available for it require an event loop framework because they themselves are heavily built around one. Trying to bolt it onto my existing codebase proves more trouble than it's worth, as I don't want to gut my entire program's core to refactor it around that event loop. I posted on my blog about this problem recently. With Go, any troublesome module I want to use I can just throw in a goroutine and let it do its own thing without getting in my way. It's pretty awesome.

dcsan commented 8 years ago

cool, thanks for the response. i'll read up on your event loop stuff. so overall you didn't feel like this go was a jump to a more C like environment where you have to be much more detailed, compared to a scripting language?

have you looked at elixir at all btw? on top of erlang it seems well suited for your event loop type problems, and phoenix web framework seems to be getting some buzz. ruby-ish syntax appeals to me too.

kirsle commented 8 years ago

Some parts of Go are a bit tedious; JSON parsing as you mentioned before, also for RiveScript I had to define structs for the data layout of the in-memory replies. The way that Go automagically decides what is exported or not based on whether the name begins with a capital letter can also be a pain, too. For example, all of my ast* types are lowercased, which means they can only be used from within the RiveScript package. I didn't want to do this originally, because I wanted those data types to be able to be given to an end user (think using the internal parse() function in your own code, and you get a nice struct tree that you can work with).

I decided to rename them to be lowercase for my own sanity in writing the rest of the library. My functions would have their own variables like topic and trigger, and I'd be writing code like this:

// in parser.go
ast.Topics[topic].Includes[field] = true

// in loading.go
rs.topics[topic].Triggers = append(rs.topics[topic].Triggers, trigger) // T or t

I'd have apparently random times that I use a capital letter vs. not, and so many places in the code touch parts of the AST structs that it was getting to be pretty annoying keeping track of when to capitalize and when not to. Each potentially user-facing layer of the structure was capitalized, but the indexed fields weren't because they were local variables, and the top level wasn't capitalized because rs.topics was a private variable holding the AST tree...

If I do add a Deparse() style function, it will mean basically duplicating the struct definitions for the AST tree to include capital letters, and copy everything over, but I decided this was the lesser of two evils. I'd rather the whole line-of-code be lowercased for the majority of the module and just deal with the code debt of having two copies of the AST struct.

Another example I remember being annoying is that reflection in Go should generally be avoided, so in sorting.go I had to write one line for each attribute because dynamically picking an attribute requires reflection:

// Sort each of the main kinds of triggers by their word counts.
running = sortByWords(running, track[ip].atomic)
running = sortByWords(running, track[ip].option)
running = sortByWords(running, track[ip].alpha)
running = sortByWords(running, track[ip].number)
running = sortByWords(running, track[ip].wild)

In something like Python, Perl, or CoffeeScript I could've done this:

for s in ["atomic", "option", "alpha", "number", "wild"]:
    running.extend(sort_by_words(track[s]))

And finally, the language being strongly typed made me have to do some things slightly differently in RiveScript-Go that I didn't have to do in other languages. For example in Perl, strings and numbers have no real differentiation and are dynamically cast depending on their context (if you add with + they get cast to numbers, if you concatenate with . they're strings), so for user variables I didn't have to think of what type to store and the <add>/<sub>/<mult>/<div> tags worked flawlessly, but for Go I had to make ALL user variables strings, and manually cast them to numbers to do those operations, before turning them back into strings).