Introduction to PureScript: Twitter Search API

rabraham

Rajiv Abraham

Posted on April 1, 2020

Introduction to PureScript: Twitter Search API

This post is an ported, edited version of the original

TLDR: I wrote this in a fiction format for fun. The actual code is in the repo. Also, I'm new to FP so this is newbie code. I can refactor it to be elegant but I want to keep this simple for beginners.

Twitter Storm

Kim Kardashian felt uneasy as soon as she woke up. She had just used the r-word yesterday and suffered a huge backlash. She felt vulnerable about her twitter following and needed to be reassured. She had to do something different. Yes, she could just type her name in the Twitter App and see what people were saying about her. But she had secretaries for that. No, she had to do what no other celeb had done before. She would code!

What language though? A language which is nice and clean and pure. So she googles around and discovers PureScript! She installs it in a breeze while wondering about this Mr. Java Script guy who was always complaining online on how difficult it was. Sigh. Ok, what next?

Reading Twitter credentials

First, she has to read her Twitter credentials from a file. Yes, she could hard code the passwords in the program but she's a celeb. She knows Security.

So, she got her credentials from Twitter and created a file like below at config/twitter_credentials.json

{
  "consumer_key": "KimMama",
  "consumer_secret": "KimLikesToCode",
  "access_token": "KimDoesNotKnowWhatThisIsFor",
  "access_token_secret": "KimThinksTwitterHasGoneMad"
}

Enter fullscreen mode Exit fullscreen mode

She built a JavaScript like object in PureScript(called records) using type:

type TwitterCredentials =
  { consumer_key :: String
  , consumer_secret :: String
  , access_token :: String
  , access_token_secret :: String
  }

Enter fullscreen mode Exit fullscreen mode

How do we read this file?

import Node.Encoding (Encoding(..))
import Node.FS.Aff (readTextFile)

readConfigStr :: String -> Aff String
readConfigStr path =  readTextFile UTF8 path

Enter fullscreen mode Exit fullscreen mode

import Node.Encoding (Encoding(..)) meant import the type constructor Encoding and the .. meant import all it's data constructors as well, one of which is UTF8. Since she is a celeb and she is never wrong, type constructors are like abstract base types and data constructors are like normal OOP constructors but fancier. You can have data constructors with different names and you can even treat them like Enumerations in switch/case like statements(Kim's BFF liked to call them pattern matching).

Aff stands Asynchronous Effect(the synchronous effect is called Effect). These effects represent an action that the program would like to take, but not executed yet. Whaaa?

If Kim wanted to call Khloe for lunch, buy flowers for her mother and type her next tweet... She wouldn't be the person doing it, would she? It would be her secretary! All, she would do is text her secretary commands to do this thing but it wouldn't happen until her secretary actually executed the commands at a later time!

In the same way, Aff(and Effect) were like texts by Kim to her new secretary PureScript. It was a way of telling PureScript that she wanted them to be done but it was just a representation of a command, not the actual execution of the command. By representation, it just meant it was a value, just like the way number 3 or "a_string" or a JavaScript object were values.

For e.g., imagine the following pseudocode in an imperative language(e.g. Python):

1: x = print("A String")
2: x
3: x

Enter fullscreen mode Exit fullscreen mode

The output would be

A string
Enter fullscreen mode Exit fullscreen mode

The execution and evaluation of the print statement both happen at line 1.

But in a functional language, the above pseudocode would be something like

1: run(
2: let x = print("A String")
3: x
4: x
5: )
Enter fullscreen mode Exit fullscreen mode

And the output would be

A String
A String
Enter fullscreen mode Exit fullscreen mode

let is like the variable assignment in imperative code.

Only the evaluation happens at lines 2-4 but not the execution. The execution happens inside run. So before the program is given to run, x replaced at lines 3 and 4 to be print("A String"). Note, the print has different interpretations. In the imperative setting, it executes a command, but in the functional setting, it executes nothing, just returns back a value representing an action for future execution by the run procedure.

Another viewpoint is that most applications always start with the main function. In PureScript, perhaps the simplest program one could write is.

import Effect.Console (log)
main:: Effect Unit
main = log "Product Placement Here. ;)"
Enter fullscreen mode Exit fullscreen mode

The signature for log is log :: String -> Effect Unit. Unit stands for nothing, as in, we don't expect anything back from the console.

And like the pseudocode above, what happens within PureScript code, unseen by the programmer is something like

run(main)
Enter fullscreen mode Exit fullscreen mode

Kim felt a chill through her spine. She regretted not taking programming seriously in school.

Ok, readConfigStr returned a Aff String but she needed to convert it to our TwitterCredentials record. She asked her secretary for technology to find a library for her and she found PureScript-Simple-JSON by a guy called Justin Woo.

import Simple.JSON as SimpleJSON
import Data.Either (Either(..))

parseConfig :: String -> Either String TwitterCredentials
parseConfig s =
  case SimpleJSON.readJSON s of
    Left error -> Left (show error)
    Right (creds :: TwitterCredentials) -> Right creds
Enter fullscreen mode Exit fullscreen mode

parseConfig has an Either String TwitterCredentials in it's signature. It's like an union type. The result could either be a String(an error string) or the actual credentials. PureScript defines Either as

data Either a b = Left a | Right b
Enter fullscreen mode Exit fullscreen mode

So if we want to return a string, we return Left "my error string", the actual credentials as Right creds. That way, the person calling parseConfig knows which is which.

In parseConfig, SimpleJSON.readJSON returned an Either but Kim didn't want to deal with the complex Left type, so she just converted that to a string using show.

Now it was just a matter of calling readConfigStr and passing the value to parseConfig. Something like this pseudocode

cStr = readConfigStr path
return parseConfig cStr
Enter fullscreen mode Exit fullscreen mode

But she couldn't make it compile! She started panicking and thought of what would happen if the word got out and Taylor Swift found out. The Shame

"Try the do notation", said a voice from behind.

Kim swivelled back and her mouth opened with surprise.

"Kanye! I didn't know you knew PureScript!"

"Nah, PureScript is for hipsters. I'm old school. I like my Haskell."

He continued, "The do notation allows you to extract the String from Aff String and gives you the illusion of the pseudocode above."

readConfig :: String -> Aff (Either String TwitterCredentials)
readConfig path = do
  cStr <- readConfigStr path
  pure $ parseConfig cStr
Enter fullscreen mode Exit fullscreen mode

"What's pure $ for?", asked Kim?

Kanye sighed. He knew the author of this post was in a hurry to move on to doing cooler stuff and didn't want to get into monads in this post. So he bailed too.

First $. That's just a simple way of saying consider everything after as one value. For e.g.
show $ SimpleJSON.readJSON s meant show (SimpleJSON.readJSON s) instead of (show SimpleJSON.readJSON) s. Kim approved. She liked $ signs.

Kanye then braced himself for his 'simplification' of pure.

"You noticed that it was cStr <- readConfigStr path and not let cStr = readConfigStr path. The <- is syntax sugar which make it look like an =. But what is really happening underneath is something very similar to callbacks. The Aff String type has to be given a function to work on the String value within it. But this function can't just be cStr -> parseConfig cStr. The function has to return back an Aff something. pure is a constructor. In this context of Aff, when we say pure something, it's like saying new Aff(something) or in our case, it's like saying new Aff(parseConfig(cStr))"

Kim beamed at Kanye. He looked so hot right now. She wanted him so bad.

Bearer Token from Twitter.

Great, that gave her the credentials but she needed a bearer token from Twitter which she would then use to get the results. How does one call the Twitter endpoint in PureScript? She beckoned her secretary for technology to find her a library. Her secretary came back running.

"I found a library called Milkis... again by Justin Woo!"

Kim's eyes sharpened with intent. She wondered out aloud, "Do you think this Justin guy is a celebrity in the PureScript world? Hmmmm make my agent call his agent. Let's do a reality show together."

Kim first created a method to construct the authorization string from the credentials and encode it in Base64. The <> was like an append operator.

import Data.String.Base64 as S
authorizationStr :: TwitterCredentials -> String
authorizationStr credentials =
  S.encode $ credentials.consumer_key <> ":" <> credentials.consumer_secret

Enter fullscreen mode Exit fullscreen mode

She then made a simple fetch helper method from Milkis.

import Milkis as M
import Milkis.Impl.Node (nodeFetch)


fetch :: M.Fetch
fetch = M.fetch nodeFetch
Enter fullscreen mode Exit fullscreen mode

She then created a method to get the bearer token string or return a string as error(in the Left part of the code).

import Milkis as M
import Effect.Aff (Aff, attempt)

getTokenCredentialsStr :: String -> Aff (Either String String)
getTokenCredentialsStr basicAuthorizationStr = do
    let
      opts =
        { body: "grant_type=client_credentials"
        , method: M.postMethod
        , headers: M.makeHeaders { "Authorization": basicAuthorizationStr
                                 , "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
                                 }

        }
    _response <- attempt $ fetch (M.URL "https://api.twitter.com/oauth2/token") opts
    case _response of
      Left e -> do
        pure (Left $ show e)
      Right response -> do
        theText <- M.text response
        pure (Right theText)

Enter fullscreen mode Exit fullscreen mode

Now to bring it all together.


type BearerAuthorization =
  { token_type :: String
  , access_token :: String
  }

basicHeader :: String -> String
basicHeader base64EncodedStr = "Basic " <> base64EncodedStr

toBearerAuthorization :: String -> Either String BearerAuthorization
toBearerAuthorization tokenString = do
  case SimpleJSON.readJSON tokenString of
    Left e -> do
      Left $ show e
    Right (result :: BearerAuthorization) -> do
      Right result

getTokenCredentials :: TwitterCredentials -> Aff (Either String BearerAuthorization)
getTokenCredentials credentials = do
  tokenCredentialsStrE <- getTokenCredentialsStr $ basicHeader $ authorizationStr credentials
  case tokenCredentialsStrE of
    Left error -> do
      pure (Left error)
    Right tokenCredentialsStr -> do
      let tokenCredentialsE = toBearerAuthorization(tokenCredentialsStr)
      case tokenCredentialsE of
        Left error -> do
          pure (Left error)
        Right authResult -> do
          pure (Right authResult)
Enter fullscreen mode Exit fullscreen mode

Great, we had the bearer token. It's finally time to search for Kim Kardashian!
PureScript had this interesting signature format though. What it was saying below was that showResults took as input a BearerAuthorization and a String and returned an Aff (Either String SearchResults)

Also, the SearchResults and Status had lots of fields but she just wanted the basic stuff.


type Status =
  { created_at :: String
  , id_str :: String
  , text :: String
  }

type SearchResults =
  { statuses :: Array Status
  }

twitterURL :: String -> M.URL
twitterURL singleSearchTerm = M.URL $ "https://api.twitter.com/1.1/search/tweets.json?q=" <> singleSearchTerm

showResults :: BearerAuthorization -> String -> Aff (Either String SearchResults)
showResults credentials singleSearchTerm = do
  let
    opts =
      { method: M.getMethod
      , headers: M.makeHeaders { "Authorization": "Bearer " <> credentials.access_token}

      }
  _response <- attempt $ fetch (twitterURL singleSearchTerm) opts
  case _response of
    Left e -> do
      pure (Left $ show e)
    Right response -> do
      stuff <- M.text response
      let aJson = SimpleJSON.readJSON stuff
      case  aJson of
        Left e -> do
          pure $ Left $ show e
        Right (result :: SearchResults) -> do
          pure (Right result)
Enter fullscreen mode Exit fullscreen mode

Finally, reaching the very end to the main command!

import Effect.Class.Console (errorShow, log)
import Effect.Aff (Aff, launchAff_)

main :: Effect Unit
main = launchAff_ do
  let searchTerm = "Kim Kardashian"
  config <- readConfig "./config/twitter_credentials.json"
  case config of
    Left errorStr -> errorShow errorStr
    Right credentials -> do
      tokenCredentialsE <- getTokenCredentials credentials
      case tokenCredentialsE of
        Left error ->
          errorShow error
        Right tokenCredentials -> do
          resultsE <- showResults tokenCredentials searchTerm
          case resultsE of
            Left error ->
              errorShow error
            Right result ->
              log $ show $ "Response:" <> (show result.statuses)

Enter fullscreen mode Exit fullscreen mode

launchAff_ was required because the entire computation returned Aff something but main was of type Effect Unit. So launchAff_ just converted Aff something to Effect Unit

As Kim beamed with pride at her code, she flashed her eyes at Kanye and asked him, "Isn't the code beautiful?"

Kanye gazed into her eyes and said, "Actually, it sucks. There are so many case statements in that code that I feel cross eyed."

And the next thing Kanye knew, was that he was flat on the ground, his jaw felt like it had been displaced and he was seeing double.

For there are three things you don't tell your wife:

1) Honey, you have gained weight
2) Your code sucks
3) I miss my mother's cooking.

As Kanye massaged his jaw, he muttered, ".. I guess she does not want to know about the ExceptT Monad.."

💖 💪 🙅 🚩
rabraham
Rajiv Abraham

Posted on April 1, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Dependent Types in PureScript
functional Dependent Types in PureScript

August 23, 2024

Testing Bank Kata in PureScript
functional Testing Bank Kata in PureScript

April 14, 2019

Bank Kata in PureScript
functional Bank Kata in PureScript

April 14, 2019