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