Building a random arts bot in F#

Since join­ing JUST EAT I have been much more active in attend­ing mee­tups because our office is walk­ing dis­tance to the Code Node. In par­tic­u­lar, I have been a reg­u­lar at Phil Trelford’s F#unctional Lon­don­ers group which meets twice a month.

In the last month alone, we’ve been spoiled with Phil’s Ran­dom Arts hands-on ses­sion and Mathias’s expe­ri­ence report on build­ing the World Bank Facts twit­ter bot. You see where this is going…

So, com­bin­ing the inspi­ra­tion (and code ) from both, I decid­ed to cre­ate a twit­ter bot for gen­er­at­ing ran­dom arts.

Design Goals

I had a few goals in mind when I sat down to write the bot:

  1. peri­od­i­cal­ly pub­lish ran­dom­ly gen­er­at­ed images (like what the LSys­tem Bot does for L-Sys­tems)
  2. sup­port a sim­ple DSL for express­ing the maths equa­tions used to gen­er­ate the images
  3. allow oth­ers to tweet to it in the above DSL and reply with the gen­er­at­ed image

DSL

Let’s start with the AST, the syn­tax of the DSL is just how we get to the AST. This is an extend­ed set of the expres­sions that Phil shared with us in his ses­sion:

rand_art_01

One approach would be to use a nat­ur­al maths syn­tax, e.g.

  • x + y
  • tan y
  • cos x * con­st
  • y + sqr x * sin y

The chal­lenge with this approach is to clear­ly define and cor­rect­ly sup­port the prece­dence rules.

Anoth­er approach I con­sid­ered is to use a LISP syn­tax, e.g.

  • (+ x y)
  • (tan y)
  • (* (cos x) con­st)
  • (+ y (* (sqr x) (sin y)))

This approach is slight­ly more ver­bose, but has the ben­e­fit of sim­plic­i­ty and prece­dence is very obvi­ous. It’s also very easy to write a S-Expres­sion pars­er, which also fac­tored into my deci­sion to go with this approach.

To go from the AST to the S-Expres­sion is super easy, just over­ride the ToString() method like this:

rand_art_02_v2

Now that the easy parts are done, let’s write a S-Expres­sion pars­er for our DSL.

Broad­ly speak­ing, the two com­mon approach­es for writ­ing parsers in F# is to use FPar­sec or vanil­la active pat­terns. Hav­ing imag­ined how I’d write the pars­er with both approach­es I decid­ed the FPar­sec route is sim­pler.

The fol­low­ing 25 lines of code would build the foun­da­tion for the whole DSL, spend a moment or two to see if you can work out what it’s doing.

rand_art_03

OK, going back to the AST, we can break things down into 4 cat­e­gories:

  1. prim­i­tives : x, y, and cost
  2. unary func­tions : sin, cos, tan, well, tent,  sqr and sqrt
  3. bina­ry func­tions : add, sub­tract, prod­uct, divide, max, min, aver­age and mod
  4. ternary func­tions : lev­el and mix

In the code snip­pet above, we first cre­at­ed parsers for ( and ), then parsers for x, y and con­st. But this line is inter­est­ing…

rand_art_04

This allows for a recur­sive pars­er for our recur­sive AST (expres­sion is defined in terms of func­tions, which are defined in terms of expres­sions, and so on).

For all the unary func­tions we have right now and in the future, their struc­ture is the same:

( func_name some_expr )

now see how we parse them:

rand_art_05

up until the oper­a­tor we have cap­tured the Expr object to invoke op with, what­ev­er op is.

rand_art_06

Inter­est­ing, so op is any func­tion that takes Expr as argu­ment. That’s con­ve­nient, because every clause of a union type is a func­tion in its own right. For exam­ple, in our AST, Sin is a func­tion that takes an Expr and returns an Expr..

rand_art_07

Aha, so we can stay DRY and use the unary­Op, bina­ry­Op and ternary­Op func­tions above to cre­ate cor­re­spond­ing pars­er for the unary, bina­ry and ternary func­tions in our AST 

rand_art_08

To wrap things up on the DSL, we have to define the imple­men­ta­tion for our Expr pars­er:

rand_art_09

The use of the attempt func­tion here is sig­nif­i­cant here, oth­er­wise, each failed attempt to parse an expres­sion would have con­sumed the lead­ing paren­the­sis ‘(‘ and cause all sub­se­quent parsers to also fail.

What else?

Since I have “bor­rowed” heav­i­ly from both Phil and Mathias’s work, much of the build­ing blocks I need­ed to write the bot was already there. But I also encoun­tered a few new chal­lenges along the way though.

Random is not threadsafe

When you use an instance of System.Random from mul­ti­ple threads you can mess up its inter­nal state, it’ll then always return 0 which is hard­ly ran­dom at all…

Since I decid­ed to go async from the start I was able to repro­duce this prob­lem reg­u­lar­ly. The solu­tion was to sim­ply instan­ti­ate a new instance of Ran­dom before I need to gen­er­ate a new for­mu­la or draw anoth­er image.

Formulae are usually too long to fit inside a tweet

The ran­dom­ly gen­er­at­ed for­mu­lae are almost always too long and couldn’t fit into a tweet. To increase the chance of gen­er­at­ing for­mu­lae that are tweet-sized, I lim­it­ed the max depth of the expres­sion gen­er­a­tion log­ic to 6. For exam­ple:

Now, rough­ly around half the gen­er­at­ed for­mu­lae can fit into the 140 char­ac­ters lim­it.

Many boring images

Anoth­er thing that quick­ly became obvi­ous was that, many of the gen­er­at­ed for­mu­lae would yield a sim­i­lar and kin­da bor­ing images (most­ly a black screen). So, to improve the gen­er­al qual­i­ty of the bot’s out­put, I added anoth­er step in the pipeline to inspect the gen­er­at­ed bitmap.

For now, this step would fil­ter out images that are most­ly a black screen.

Loopy conversation with another bot

It’s easy to get into a loop between two bots, and so far I have some rudi­men­ta­ry mea­sures in place to stop the con­ver­sa­tion. How­ev­er it is just as like­ly to pre­ma­ture­ly stop the con­ver­sa­tion with an inno­cent user who has made an error in his/her tweet.

There’s still work to be done to find the right bal­ance here, if you’ve got some ideas/suggestions, I’d love to hear about them in the com­ments below.

Auto-follow

If some­one tweets at the bot then they’re prob­a­bly show­ing an inter­est in it. Let’s auto-fol­low them, and maybe they’ll fol­low us back 

Supporting direct messages

Unsur­pris­ing­ly, after a few peo­ple became aware of the bot’s exis­tence I found some­one sent direct mes­sages to the bot to see if they can get an image back via DM as well as tweets. Sad­ly it’s not yet sup­port­ed, but it’s a triv­ial effort to add so expect to see this in the near future.

 

So that’s it folks, a quick update on what I did last week­end, if you’d like to give the bot a try then feel free to send tweets to @RandomArtsBot. You can find the doc­u­men­ta­tion for the DSL syn­tax here, or just tweet “help” at the bot.

There are also a num­ber of oth­er sim­i­lar bots that you might like to play around with, for instance, @fsibot, @worldbankfacts, @SpirographBot and @LSystemBot.