Jezen Thomas

Jezen Thomas

CTO & Co-Founder at Supercede. Haskell programmer. Writing about business and software engineering. Working from anywhere.

Is it c? Or is it с?

Someone working on the team at Supercede asked the following question regarding some perplexing GHCi output.

I must have missed something obvious because I’ve been staring myself blind at this for the past few minutes. Isn’t it saying one thing is not in scope and then immediately suggesting that very same thing as a replacement?

ghci> :t API.Handler.V20201001.Types.tscaExcluded

<interactive>:1:1-40: error:
    Not in scope: 'API.Handler.V20201001.Types.tscaExcluded'
    Perhaps you meant one of these:
      'API.Handler.V20201001.Types.tsсaExcluded' (imported from API.Handler.V20201001.Types),

Can you spot the error?

The problem could have been related to some surprising behaviour in GHCi when references are held to old values after their names are shadowed. Or perhaps it was something related to the build cache since we use incremental compilation. But those would have been guesses two and three.

Based on experience — and some luck — here was my first guess.

You know, we had an interesting issue once where a c was substituted for a с. See the difference?

Lo and behold…

Emacs says one is a LATIN SMALL LETTER C and the other is a CYRILLIC SMALL LETTER ES but I would never have guessed. And would you believe – this is indeed what has happened. Wow!

Perhaps this deserves a new linting rule.

Kill on the Cover Letter, but Not Like That

A couple of years ago, I received the following email in response to a Haskell programming job I had advertised.

Life is cheap. I would kill someone for a Haskell job. Thanks for your consideration.

That was it. That was the entire email.

The applicant had attached their résumé to the email, and in fairness the résumé showed that this person would bring relevant and valuable experience to the team.

Now, I do love all sorts of humour, and I’m 99.9% certain that rather than being serious, this person was a little trigger happy with the hyperbole cannon.

But on the off chance that they were serious, how could I have it on my conscience that we ended up with a killer on the team even after they told me about it?! Could you imagine that tribunal?

I’m fortunate that when I do advertise a programming job on the internet, I’m inundated with strong applications. All of the best applications take the form of a brief couple of paragraphs in an email which describe why the applicant believes they’re a particularly good fit for the job. This is what I regard as the cover letter. The résumé should still be attached, but it’s the cover letter I’m reading first.

The cover letter sets the tone. It says “this is who I am, this is my understanding of what you’re looking for, and this is why I believe I’m the right choice.”

The cover letter is the perfect place to make the right introduction.

But, come on.

This ain’t it.

Stubbing I/O in Yesod

Here’s the scenario.

You’re writing a web application in Yesod. In one of your request handler functions, you need to run some IO action. This might be to make an HTTP request against an online weather service, or this might be to charge someone’s credit card, or even just to generate some random number.

Taking the latter as an example, imagine we want to generate a random number and then respond to the user’s request by reporting whether the randomly generated number is even or odd.

We might write code which looks like this.

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}

module Application where

import System.Random
import Yesod

data App = App

mkYesod "App" [parseRoutes|
/random RandomR GET
|]

instance Yesod App

getRandomR :: Handler Value
getRandomR = do
  n <- liftIO randomNumber
  returnJson $ isEven n
  where
    randomNumber :: IO Int
    randomNumber = randomRIO (1, 100)

    isEven :: Int -> String
    isEven n = if even n then "even" else "odd"

main :: IO ()
main = warp 3000 App

This is a complete Yesod application. We can run this locally and it will be listening for requests on port 3000. When we send requests there, we can see our application dutifully responding with whether or not the randomly generated number was even or odd.

$ curl http://localhost:3000/random
"even"

$ curl http://localhost:3000/random
"odd"

This is all well and good, but how do we write an automated test for this? We can’t control the randomness of our pseudo-random number generator. Similarly, if instead of generating a random number this were an HTTP request to attempt to charge someone’s credit card with some payment provider, e.g., Stripe, then we wouldn’t be able to write a reliable automated test for this because we don’t control Stripe’s servers.

What we need to do is to stub out this IO action. This means that instead of running the real implementation during the test, we swap it out for a fake version that we can control.

One simple approach for this is with dependency injection.

Instead of defining our randomNumber function alongside our request handler, we can declare it as part of our application’s foundational data type.

data App = App
  { appRandomNumber :: IO Int
  }

When we initialise our application, we construct our App value with the real implementation of our function.

main :: IO ()
main = warp 3000 $ App randomNumber
  where
    randomNumber = -- real implementation

Since our randomNumber function is no longer defined alongside our request handler, we’ll now need to ask for that function from within the handler instead.

getRandomR :: Handler Value
getRandomR = do
  n <- liftIO =<< getsYesod appRandomNumber
  returnJson $ isEven n
  where
    isEven :: Int -> String
    isEven n = -- …

This behaves exactly as it did before, but now we’re able to swap out our randomNumber function for a fake version in an automated test with testModifySite.

withApp :: SpecWith (TestApp App) -> Spec
withApp = before $ do
  pure (App randomNumber, id)

stub :: YesodDispatch a => (a -> a) -> YesodExample a ()
stub f = testModifySite (\app -> pure (f app, id))

spec :: Spec
spec = withApp $ do

  describe "GET /random" $ do

    it "works with even numbers" $ do
      stub (\a -> a { appRandomNumber = pure 66 })
      get RandomR
      statusIs 200
      bodyEquals "\"even\""

    it "works with odd numbers" $ do
      stub (\a -> a { appRandomNumber = pure 17 })
      get RandomR
      statusIs 200
      bodyEquals "\"odd\""

Of course, the usual warnings apply. There are problems that come along with stubbing out functions — if your stub doesn’t accurately reflect what that function actually does, then your test is only giving you false confidence.

People often say that stubbing is bad and that you shouldn’t do it. I don’t think this advice is useful. Yes, your tests and application logic should be pure as far as you can help it. But sometimes you really do need a stub.

A working example of this approach is available here.