Functional reactive Twitter bots
The key idea of functional reactive programming (FRP) is time-varying values, which the Elm language calls Signals. In a discrete FRP system like Elm, you can think of a Signal as a stream of individual events. Rather than using a callback function to swing at every event that shows up, we use higher-level operators to manipulate a whole stream of events at once.
Signals fit modern "real-time" social data really well---like Twitter feeds. Because the abstraction is such a nice fit, we can write Twitter bots like @EveryMNLake with only a couple lines of code!
Let's think of a Twitter feed as an object of type Signal (StatusUpdate a)
. That is, it's a stream of status update events1. I've written an Elm library, Birdhouse, which lets you program a Twitter bot just by creating such a Signal object in an Elm program.
Elm gives us other Signals that we can use as input. We can filter, transform, and combine these Signals to create whatever Twitter stream we want.
Counter
Let's look at a simple example: a bot that will increment a number, then tweet it, every ten seconds.
Here's the code2:
module Bot where
import Birdhouse as BH
port updates : Signal { status : String } -- Signal (BH.StatusUpdate {})
port updates = BH.update . show <~ count (every (10 * second))
Can you spot the key line of code?
port updates = BH.update . show <~ count (every (10 * second))
Read this from right to left. It's a pipeline:
- Start from the signal
every (10 * second)
(which has a new value every ten seconds), - then run it through
count
(now we have a Signal of the number of ticks that have elapsed since the start of the program3), - then two "lifted" functions that transform what's inside of the Signal:
Now we have what we need---a Signal (BH.StatusUpdate {})
. Let's see a concrete example of what's inside each Signal in the pipeline at a given time.
We send our resulting Signal (BH.StatusUpdate {})
out through the updates
port. The Birdhouse library, running outside our bot, looks for a port by that name and tweets out whatever it sees there.
Now we can compile and run this bot, and look at our browser, and we'll see... nothing. There's no output in the browser, because we didn't give Elm a main
function telling it what to display. But, if the API access is working, we should see some tweets on Twitter!
How can we get some visual feedback from our bot about what it's tweeting? We can use Birdhouse's previewStream
function, which transforms a Signal (StatusUpdate a)
into a Signal Element
. Then we can show this preview on the screen by making it the value of main
. That'll provide a running preview of what we're tweeting.
main : Signal Element
main = BH.previewStream updates
Here's our bot, running. It's not hooked up to a Twitter account, but it should still spit out a new number every ten seconds:4
And here's our final source code:
module Bot where
import Birdhouse as BH
port updates : Signal { status : String } -- Signal (BH.StatusUpdate {})
port updates = BH.update . show <~ count (every (10 * second))
main : Signal Element
main = BH.previewStream updates
The Counter example is also on GitHub.
Building
We'll need to do some extra work to compile this bot with Birdhouse, but the setup should work for any bot. We need to:
- make an account for our bot and make a Twitter app in Twitter's developer console
- grant app read/write access, get developer API key and access token for app
- put our API keys in a Keys.elm file
- compile our Elm source files with the Birdhouse library
- write an
index.html
file that refers to the compiled Elm files, the Elm runtime, Birdhouse's JS component, and the Codebird JS library - host
index.html
and its dependencies on an HTTP server
and then we can run our bot by visiting index.html
.
For the Counter example, I've done all this and written a Makefile; if you want to write your own bot, you should probably start from those.
Inspiration
I've found Twitter bots interesting and entertaining for months, but a conversation I saw on Twitter inspired me to try this programming model:
. @ghost_things = @everyword.map { |thing| "ghost #{thing}" }
— ¬ ∀ \$_console\$\$ (@jcoglan) March 1, 2014
.@jcoglan @ghost_things @everyword i now *really want* twitter bot combinators.
— chrisamaphone (@chrisamaphone) March 1, 2014
The idea of Twitter feeds as first-class values, with operations like map
and filter
, seemed to match Elm perfectly; Elm also gives you the right inputs from which to build more complicated Signals---a Signal that ticks every hour, a Signal representing the response to your HTTP request... I could certainly have used another FRP library, like Rx or Netwire, but I knew Elm best. With another library, though, you could make a standalone desktop client and evade some of the browser restrictions Elm bots face.
Ironically, a Birdhouse bot can't read Twitter feeds right now, only post, so it doesn't fulfill the original goal. You can't map a function over another feed, or multiplex two feeds.5
In fact, Elm imposes severe restrictions on what your Twitter bot can do while it's running; these might make it surprisingly difficult to work with Twitter feeds. You can't create new Signal inputs during execution6, so you'd need to have everything defined upfront, or you'd need something like a single flexible input Signal which you dispatch on (and that seems unintuitive).
Further work
I'd like to talk about FRP and Twitter bots more later:
- How I wrote and hosted @EveryMNLake, the more complicated Birdhouse example (check it out!)
- Practical limitations and problems with Elm and the browser when doing this
- Implementing and using an API to read and operate over other Twitter feeds
Birdhouse is on GitHub. Pull requests, comments, and questions welcome (GitHub, Twitter, e-mail).
-
The
a
is a parameter which basically means that we can extend theStatusUpdate
with extra data sometimes if we want. Elm has extensible records, which are more powerful than Haskell's records. In fact, if Elm had rank-2 types and kind polymorphism, you could get the same level of power as ML's modules and Haskell's typeclasses using just Elm records.For the rest of the blog post, I'll be using
StatusUpdate {}
, meaning that there's no additional information (like location data) in this bot's tweets. ↩︎ - Why is `Signal (BH.StatusUpdate {})` in a comment? Isn't that what we said the type of `updates` should be? Elm currently [requires](https://github.com/evancz/Elm/issues/493) all port type signatures to be concrete (you have to specify each field); no type aliases are allowed, so we must substitute the definition in for `BH.StatusUpdate {}`. ↩︎
-
I was imprecise when I said this bot increments a counter and tweets it. Although there might be a counter hidden underneath, I don't see it, and we haven't got a name for it, so I'd rather forget about it. This bot really tweets how many ten-second intervals have passed since it started. That's the meaning which its code has, at least. ↩︎
-
I wanted to put a live code editor here, but it's a little involved. I'd need my own server running the Elm compiler. Share-Elm and elm-lang.org/try are great, but they only compile one file at a time, and they can't include external libraries and JavaScript like Birdhouse. Or I could compile the Elm compiler to JavaScript... ↩︎
-
Twitter has a separate streaming API which can "tell" you when someone tweets, instead of requiring you to poll periodically, so I wanted to do it properly that way. But I haven't built that part yet. ↩︎
- "During execution" means "when some other Signal changes," so you'd be creating a Signal inside a Signal, and there's no way to resolve `Signal (Signal a)` back into `Signal a`. That function `Signal (Signal a) -> Signal a` is just the `join` function in Haskell, and that would make `Signal` a [monad](http://hackage.haskell.org/package/base/docs/Control-Monad.html#v%3Ajoin)---which has its own problems. Elm has [intentionally avoided](https://github.com/evancz/Elm/issues/440) that route. ↩︎