Teacher: Hello class! Welcome to your first day of functional programming. Today, we’re going to be talking about how to write the classic “Hello, World!” program in Haskell. It’ll be slightly more involved as we’ll ask for the user’s name and then greet them. I’m sure many of you have heard scary things about Haskell, but I promise you it’ll be fun.

Student: I heard we have to learn about IO. That sounds scary!

Teacher: What? No, there’s no need to worry about IO. In Haskell, when we want to perform an effect, we simply define a GADT to represent the capabilities of the effect.

Student: Uh, what?

Teacher: furiously typing

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

module Effects where

Teacher: So, as you can see, we start our program with just a few language extensions to lift (haw haw haw) Haskell up to the modern version we’ve come to know and love.

Student: Wait, we’re adding ten language extensions? Are you sure this is the normal way to write “Hello, world!”?

Teacher: Yes, of course. Next we import a few modules. We’ll be using the very nice effectful library for our effect system (along with effectful-th). Let’s do this!

import Effectful (Eff, Effect, IOE, MonadIO (liftIO), runEff, type (:>))
import Effectful.Dispatch.Dynamic (interpret)
import Effectful.TH (makeEffect)

Teacher: Now we’re cooking with gas!

Student: Wait, what is TH?

Teacher: Meta-programming.

Student: Why do we need meta-programming and an external effects library to write “Hello, world!”.

Teacher: Generality (turns head to screen, knowing wink at subreddit/HN/lobsters). furiously typing again

data Greeting :: Effect where
  GetName :: Greeting m String
  Greet :: String -> Greeting m ()

makeEffect ''Greeting

Teacher: Right. Our Greeting effect needs to do two things: one, accept the user’s name, returning a String and two, greet the user with their provided name.

Student: What does makeEffect ''Greeting do?

Teacher: Defines two fancy functions with the names getName and greet that correspond to Greeting’s constructors. NOW THEN, we can simply write our program! click clack click

program :: (Greeting :> es) => Eff es ()
program = do
  name <- getName

  greet name

Teacher: As you can see, for any set of effects that includes the Greeting effect, we can simply get the user’s name and then greet them with their name. We’re done! Class dismissed.

Student: Uh, wait, what? How do you actually run this program? What do the individual functions actually do here.

Teacher: Run the program? How pedestrian. The functions do anything we want! We aren’t coupled to their definitions! We’ve been set free from implementation constraints!

Student: I’m not messing around here, how do you run the program.

Teacher: Sigh. Ok, so in order to run the program we need to provide an interpreter for our Greeting effect in terms of some other effect. In this case we’ll do it in terms of IOE. IOE is like IO but is a built-in effect provided by effectful.

Student: YOU SAID WE WOULDN’T USE IO!

Teacher: How was I supposed to know you’d want to run this program? more typing

runGreeting :: (IOE :> es) => Eff (Greeting : es) a -> Eff es a
runGreeting = interpret $ const \case
  GetName -> liftIO getLine
  Greet name -> liftIO $ putStrLn $ "Hello, " <> name <> "!"

Teacher: There! For an Eff with the IOE and Greeting effects, we interpret the Greeting effect in terms of IOE. This removes Greeting from Eff’s list of effects and we’re left with only IOE! TA DA!

Student: But. How. Do. You. Run. The. Program.

Teacher: Right, right. effectful provides a runEff function with the signature Eff '[IOE] a -> IO a. That is, it discharges the final IOE effect giving you back a value in real IO!

runProgram :: Eff '[Greeting, IOE] a -> IO a
runProgram = runEff . runGreeting

main :: IO ()
main = runProgram program

Teacher: WATCH!

$ stack run
Drew
Hello, Drew!

Student: Did we just write an interpreter?

Teacher: Have you ever not done that?

Student: I … what? Why is this useful?

Teacher: LET’S WRITE A TEST!

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TypeOperators #-}

module EffectsSpec
  ( spec,
  )
where

import Effectful (Eff, runPureEff)
import Effectful.Dispatch.Dynamic (reinterpret)
import qualified Effectful.Writer.Dynamic as Writer
import Effects (Greeting (..), program)
import Test.Hspec (Spec, describe, it, shouldBe)

runGreeting :: String -> Eff (Greeting : es) a -> Eff es (a, String)
runGreeting name = reinterpret Writer.runWriterLocal $ const \case
  GetName -> pure name
  Greet n -> Writer.tell $ "greet: " <> n

runProgram :: String -> Eff '[Greeting] a -> (a, String)
runProgram name = runPureEff . runGreeting name

spec :: Spec
spec = do
  describe "program" do
    it "greets us" do
      let (result, greeting) = runProgram "Drew" program

      result `shouldBe` ()
      greeting `shouldBe` "greet: Drew"

Teacher: As you can see, we can provide a totally different interpreter when testing our program! In fact, in this case, we’ve reinterpreted Greeting in terms of Writer, another built-in effect. We provide a static name that is always returned by getName and greet appends our greeting to a String tracked by Writer. Finally, Writer.runWriterLocal returns both our resulting value () and the result of our greet calls. NOTICE HOW WE NO LONGER NEED IO? CHECK MATE STUDENTS.

$ stack test
effects> test (suite: effects-test)


Effects
  program
    greets us

Finished in 0.0006 seconds
1 example, 0 failures

effects> Test suite effects-test passed

Student: faints