class: center, middle, title-slide count: false  .less-line-height[ Alejandro Serrano @ ZuriHac 2022 .grey[๐ฆ @trupill - ๐โโฌ serras - ๐จโ๐ป Tweag] ] --- # ๐ฅ Overall goal ### .grey[How do we build software with Haskell?] --- # ๐ฅ Overall goal ### .grey[How do we build software with Haskell?] 1. Domain-specific languages
Representing actions and scripts
Property-based testing 2. Communicating over the network
Serialization (without boilerplate)
Error handling
Concurrency across threads --- # ๐ Overall goal ### .grey[Build an interactive card game] 1. Represent the cards and the actions 2. Communicate different clients --- # ๐ Overall goal ### .grey[Build an interactive card game] 1. Represent the cards and the actions 2. Communicate different clients
This is
ZuriHac
!
Collaborate
Discuss
Enjoy
--- #
Pokรฉmon Trading Card Game Players take turns drawing and playing cards
Goal: knock out 6 of your opponent's Pokรฉmon
For this your use
attacks
Those attacks cost
energy
Each attack does
damage
HP
define the maximum damage before knock-out
--- # ๐๏ธ Our approach ### .grey[Explanations interleaved with tasks] 1. Representing cards 2. Representing actions 3. Testing actions --- # ๐๏ธ Our approach ### .grey[Explanations interleaved with tasks] 1. Representing cards 2. Representing actions 3. Testing actions ## .grey[Domain-specific Language (DSL)] Implementation of the _Ubiquitous Language_ idea from DDD, the code speaks the domain --- #
Representing cards
Each card comes with...
Name:
Pikachu
Type:
HP: 70
Attack(s)
(forget about the rest for now)
--- # โ๏ธ Algebraic Data Types (ADTs)
Each card comes with...
Name:
Pikachu
Type:
HP: 70
Attack(s)
Straightforward translation of the description .code70[ ```haskell data Card = Card { name :: Text , typ :: Energy , hp :: Natural , attacks :: [Attack] } ``` ] --- # โ๏ธ Algebraic Data Types (ADTs)
Each card comes with...
Name:
Pikachu
Type:
HP: 70
Attack(s)
.code70[ ```haskell data Card = Card { name :: Text , typ :: Energy , hp :: HP , attacks :: [Attack] } newtype HP = HP Natural deriving (Eq, Show, Num) ``` ] --- # โฏ๏ธ Energies There are 10 types of energy in the game, - 9 regular energies
- Colorless energy
- Any card providing a regular energy may also provide colorless energy
= 2
+ 1 of any other --- # โฏ๏ธ Energies There are 10 types of energy in the game, - 9 regular energies
- Colorless energy
.code70[ ```haskell data Energy = Colorless | Grass | Fire | Water | Lightning | Fighting | Psychic | Darkness | Metal | Dragon data Card = PokemonCard { ... } | EnergyCard { typ :: Energy } ``` ] --- # โ๏ธ Attacks We consider only "simple" attacks for now
.code70[ ```haskell data Attack = Attack { attackName :: Text , cost :: [Energy] , damage :: Natural } ``` ] --- # ๐งโ๐ป Time for practice! .very-little-margin-top[ ### `serras.github.io/zurihac-workshop` ] Define values for the following cards
--- # ๐งโ๐ป Time for practice! .very-little-margin-top[ ### `serras.github.io/zurihac-workshop` ] Check whether some energy cards are enough to "pay" for the cost of an attack ```haskell enoughEnergy :: [Energy] -> [Card] -> Bool ``` Then, refine it to return the missing energy ```haskell missingEnergy :: [Energy] -> [Card] -> Maybe [Energy] ``` --- # โ๏ธ Attacks, redux .code70[ ```haskell data Attack = Attack { ..., damage :: Natural } ``` ] ## .grey[This is a ~~lie~~ simplification] --- # โ๏ธ Attacks, redux
--- # โ๏ธ Attacks, redux .code70[ ```haskell data Attack = Attack { ..., damage :: Natural } ``` ] ## .grey[This is a ~~lie~~ simplification] .top-margin[ - More actions than mere damage - Draw and discard cards - Actions may depend on the state - Attached cards - Coin flips - Actions may involve conditionals and loops ] --- # โ๏ธ Attacks, redux .code70[ ```haskell data Attack = Attack { ..., action :: ??? } ``` ] ## How do we model .grey[actions]? .top-margin[ - More actions than mere damage - Draw and discard cards - Actions may depend on the state - Attached cards - Coin flips - Actions may involve conditionals and loops ] --- # ๐ช Coin flips
.code70[ ```haskell data FlipOutcome = Heads | Tails data Action = FlipCoin (FlipOutcome -> Action) | Damage Natural surpriseAttackAction = FlipCoin $ \case Heads -> Damage 30 Tails -> Damage 0 ``` ] --- # ๐ช Coin flips
## .grey[๐งโ๐ป Time for practice!] .font50[`serras.github.io/zurihac-workshop`] --- # ๐ช Coin flips
```haskell ironTailAction = go 0 where go acc = FlipCoin $ \case Tails -> Damage acc Heads -> go (acc + 30) ``` --- # ๐ง Syntax/algebra and interpretation `Action` defines the **syntax** of our DSL
(also known as **algebra** in some circles) > "The language itself", "what we can say" --- # ๐ง Syntax/algebra and interpretation `Action` defines the **syntax** of our DSL
(also known as **algebra** in some circles) An **interpretation** defines how each value behaves in a certain context > "What a sentence means" 1 syntax / algebra โท โ interpretations --- # ๐ฐ Randomness interpretation During the actual game, we expect to generate random coin flips to obtain the actual damage ```haskell interpretRandom :: Action -> IO Natural ``` ## .grey[๐งโ๐ป Time for practice!] .font50[`serras.github.io/zurihac-workshop`] --- # ๐ฐ Randomness interpretation During the actual game, we expect to generate random coin flips to obtain the actual damage ```haskell interpretRandom :: Action -> IO Natural interpretRandom (Damage d) = pure d interpretRandom (FlipCoin f) = do outcome <- flipCoin interpretRandom (f outcome) -- one-liner -- flipCoin >>= interpretRandom . f ``` --- # ๐ด Actions about cards
--- # ๐ด Actions about cards .code70[ ```haskell data Action = FlipCoin (FlipOutcome -> Action) | DrawCard (Maybe Card -> Action) -- ^ there may not be more cards | QueryAttached ([Card] -> Action) -- ^ get info. about the current Pokรฉmon | Damage Natural ``` ] ### .grey[Can you spot the pattern? ๐] --- # ๐ด Actions about cards .code70[ ```haskell data Action = FlipCoin (FlipOutcome -> Action) | DrawCard (Maybe Card -> Action) -- ^ there may not be more cards | QueryAttached ([Card] -> Action) -- ^ get info. about the current Pokรฉmon | Damage Natural ``` ] - `Damage` is a **final** action - The rest "generate" a value,
which is consumed to keep going --- # ๐ฉ The `Operational` monad .code70[ ```haskell data Program instr a where Done :: a -> Program instr a (:>>=) :: instr a -> (a -> Program instr b) -> Program instr b ``` ] Split the pattern from the concrete instructions .code70[ ```haskell data Action a where FlipCoin :: Action FlipOutcome DrawCard :: Action (Maybe Card) QueryAttached :: Action [Card] ``` ] --- # โ๏ธ Generalized ADTs (GADTs) Refine the resulting type from constructors .code70[ ```haskell {-# language GADTs #-} data Action a where FlipCoin :: Action FlipOutcome DrawCard :: Action (Maybe Card) QueryAttached :: Action [Card] ``` ] Here it represents the "generated" value --- # ๐ช Operational coin flips
```haskell perform :: instr a -> Program instr a perform action = action :>>= Done instance Functor (Program instr) instance Applicative (Program instr) instance Monad (Program instr) ``` --- # ๐ช Operational coin flips
```haskell ironTailAction :: Program Action Natural ironTailAction = do outcome <- perform FlipCoin case outcome of Tails -> pure 0 Heads -> (30 +) <$> ironTailAction ``` --- # ๐ช Operational coin flips
Being a `Monad` gives access to many functions .code70[ ```haskell ironTailAction :: Program Action Natural ironTailAction = do hs <- unfoldWhileM (== Heads) (perform FlipCoin) pure $ 30 * genericLength hs ``` ] `unfoldWhileM` comes from `monad-loops` --- # ๐ด Actions about cards
## .grey[๐งโ๐ป Time for practice!] .font50[`serras.github.io/zurihac-workshop`] .margin-top[ 1. Write a function to **draw *n* ** cards 2. Add an additional operation to **discard** cards - Must include a predicate to select cards - Outcome: whether a card was discarded ] --- # ๐ฉ Interpreting `Operational` One generic function for every `Program` .code70[ ```haskell interpret :: Monad m => (forall x. instr x -> m x) -- ^ instruction interpreter -> Program instr a -> m a interpret f = go where go (Done x) = return x go (action :>>= k) = do x <- f action go (k x) -- f action >>= go . k ``` ] --- # ๐ฉ Interpreting `Operational` We can focus on each instruction,
instead of dealing with passing information .code70[ ```haskell interpretRandom :: Program Action a -> IO a interpretRandom = interpret $ \case FlipCoin -> flipCoin -- rest of cases ``` ] --- # ๐ฉป Property-based testing Generate many _random_ tests for the same function (or set of them) Focus on **properties** rather than examples - PBT frameworks are good at generating corner cases (extreme values, empty lists, ...) --- # โ Testing actions ### .grey[How can we test `ironTailAction`?]
--- # โ Testing actions ### .grey[How can we test `ironTailAction`?]
- If we get a tail as first result, we get 0 - If our outcomes start with `n` heads,
then the result is `30 * n` - Both implementations should coincide --- # โ Testing actions ### .grey[How can we test `ironTailAction`?]
โ Using `interpretRandom` would not work - The outcome is random - Testing `IO` is cumbersome --- # ๐งฎ Pure interpretation of flipping We pass the future outcomes as a parameter ```haskell interpretPure :: [FlipOutcome] -> Program Action a -> a ``` --- # ๐งฎ Pure interpretation of flipping We pass the future outcomes as a parameter ```haskell interpretPure :: [FlipOutcome] -> Program Action a -> a ``` Now we control the future ๐ฎ ```haskell > interpretPure [Heads, Heads, Tails] ironTailAction 60 ``` --- # ๐งฎ Pure interpretation of flipping The `State` monad threads the current value .code70[ ```haskell interpretPure :: [FlipOutcome] -> Program Action a -> a interpretPure outcomes = flip evalState (cycle outcomes) . interpret f where f :: Action x -> State [FlipOutcome] x f FlipCoin = do ~(result : nexts) <- get put nexts return result ``` ] --- # ๐งฎ Pure interpretation of flipping Writing it by hand might be easier... ๐ค .code70[ ```haskell interpretPure :: [FlipOutcome] -> Program Action a -> a interpretPure outcomes = go (cycle outcomes) where go :: [FlipOutcome] -> Program Action x -> x go _ (Done x) = x go ~(result : nexts) (FlipCoin :>>= k) = go nexts (k result) ``` ] --- # ๐ QuickCheck + Tasty ๐ฅง **QuickCheck** is a well-known library for property-based testing - Define properties of functions - Support for custom generators **Tasty** is a test runner - Runs and reports over a set of tests --- # ๐ QuickCheck + Tasty ๐ฅง ๐ฅง `testGroup` + ๐ `testProperty` - `outcomes` is randomly selected .code70[ ```haskell tests :: TestTree tests = testGroup "Iron Tail" [ testProperty "non-negative" $ \outcomes -> interpretPure (outcomes ++ [Tails]) ironTailAction >= 0 , ... ] ``` ] --- # โ A wrong property ```haskell interpretPure ... ironTailAction > 0 ``` A counter-example is found by QuickCheck .code70[ ``` Iron Tail non-negative: FAIL *** Failed! Falsified (after 1 test): [] Use --quickcheck-replay=139730 to reproduce. Use -p '/non-negative/' to rerun this test only. ``` ] --- #
The "times 30" property To create good properties you must... - Be creative with the inputs - Ensure that inputs are correct .code70[ ```haskell testProperty "30 * # heads" $ \(hs :: Int) -> hs > 0 ==> let outcomes = replicate hs Heads ++ [Tails] in interpretPure outcomes ironTailAction == fromIntegral (hs * 30) ```] --- # โ Testing actions ### .grey[๐งโ๐ป Time for practice!] .font50[`serras.github.io/zurihac-workshop`]
- If we get a tail as first result, we get 0 - Both implementations should coincide - ... --- # ๐ Summary ### .grey[Haskell is a great language for DSLs] .margin-top[ - ADTs model the domain sharply - We can model both data and processes - `Operational` save lots of boilerplate - One model, many interpretations - Useful for (property-based) testing ] --- # โ๏ธ Initial and final style **Initial** style: modeling actions using `data` - Variations: direct, free, operational **Final** style: modeling actions using `class` - Interpretations represented by `instance`s - Closer to `interface`s in other languages --- # ๐ธ A word from our sponsor ## `leanpub.com/book-of-monads`
Building your own monad with
Operational
and
Free
Initial and final style
Applicative
and friends
Monad transformers (and effects)
--- class: center, middle, title-slide # ๐คฉ It's been a pleasure ## Enjoy the rest of ZuriHac!
.grey[See you again on Monday? ๐ค]