Interactive Tests
& Documentation

via QuickCheck-style Declarations


LambdaConf 2016David Peter


shark.fish/talk
shark.fish/try

Outline

From Signals to Flares

Defining user interfaces with Applicatives

FlareCheck

Interactive tests & documentation

Part 1

From Signals to Flares

Flare: Preview


pow :: Int -> Int -> Int
  

Define user interface and program logic:


pow <$> int "Base"     2
    <*> int "Exponent" 8
  

Idea by Gabriel Gonzalez: typed-spreadsheet

Starting point: Signals


type Signal a = Time -> a    -- Conceptually

mousePos   :: Signal (Int, Int)
seconds    :: Signal Int
keyPressed :: Key -> Signal Bool
  

Elm-style signals by Bodil Stokke: purescript-signal

Signal as a functor


drawClock :: Int -> Drawing
drawClock secs = ..

animatedClock :: Signal Drawing
animatedClock = drawClock <$> seconds  -- <$> is infix for `map`
  

Signal as an Applicative


progressBar :: Number -> Drawing
progressBar percentage = ..

ratio :: Signal Number
ratio = (/) <$> mousePosX <*> windowWidth

progress = progressBar <$> ratio
  

Adding input elements

How can we build up the full user interface from an expression like:


pow <$> int "Base"     2
    <*> int "Exponent" 8
  
→ Go from Signals to Flares

‘Annotated’ functors
(abstract interlude)

Annotate a base functor 'f' with values of type 'a':


data Ann a f b = Ann a (f b)

instance Functor f => Functor (Ann a f) where
  map g (Ann a fb) = Ann a (map g fb)
  

Applicative

Is Ann also an applicative functor?


data Ann a f b = Ann a (f b)
  


instance Applicative f => Applicative (Ann a f) where
  pure x = Ann ??? (pure x)
  (Ann a1 f1) <*> (Ann a2 f2) = Ann ??? (f1 <*> f2)
  

Yes,

… if we require 'a' to be a Monoid:


data Ann a f b = Ann a (f b)
  


instance (Monoid a, Applicative f) => Applicative (Ann a f) where
  pure x = Ann mempty (pure x)
  (Ann a1 f1) <*> (Ann a2 f2) = Ann (a1 <> a2) (f1 <*> f2)
  

Applicative laws

Example: composition law


pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
  

Proof:


pure (.) <*> u <*> v <*> w
  = Ann mempty (pure (.)) <*> Ann mu fu <*> Ann mv fv <*> Ann mw fw
  = Ann mu (pure (.) <*> fu) <*> Ann mv fv <*> Ann mw fw
  = Ann (mu <> mv) (pure (.) <*> fu <*> fv) <*> Ann mw fw
  = Ann ((mu <> mv) <> mw) (pure (.) <*> fu <*> fv <*> fw)
  = Ann (mu <> (mv <> mw)) (fu <*> (fv <*> fw))
  = Ann mu fu <*> Ann (mv <> mw) (fv <*> fw)
  = Ann mu fu <*> (Ann mv fv <*> Ann mw fw)
  = u <*> (v <*> w)
  
Note: this needs the identity and the associativity of the Monoid.

Flare as annotated Signal


type Flare a = Ann [HTMLElement] Signal a
--                      ^               ^
--                      |               |
--              Monoid to collect     Output
--                 UI elements         type
  

Flare components


type Label = String

string  :: Label -> String  -> Flare String
int     :: Label -> Int     -> Flare Int
boolean :: Label -> Boolean -> Flare Boolean


sayHello <$> string   "Name"    "LambdaConf"
         <*> int      "Year"    2016
         <*> boolean  "Shout"   false
  

Quick & dirty user interfaces


plot <$> (numberSlider "m" 2.0 10.0 1.0  5.0)
     <*> (numberSlider "n" 3.0 10.0 0.1  4.0)
     <*> (numberSlider "s" 4.0 16.0 0.1 14.0)
     <*> (color "Fill color" indigo)
     <*> (boolean "Animated" false)
     <*> lift animationFrame
  

Part 2

FlareCheck

Idea


pow :: Int -> Int -> Int
  

Go from this:


flare =
  pow <$> int "Base"     2
      <*> int "Exponent" 8

runFlare flare
  

to this:


flareCheck pow
  

… and let the type checker do all the work.

Reminder: QuickCheck


class Arbitrary a where
  arbitrary :: Gen a

class Testable t where
  test :: t -> Gen Result

instance (Arbitrary a, Testable t) => Testable (a -> t)
  

FlareCheck


class Flammable a where
  spark :: Flare a

class Interactive t where
  interactive :: Flare t -> Flare Renderable

instance (Flammable a, Interactive t) => Interactive (a -> t)
  

Flammable Instances


Flammable Boolean
Flammable Char
Flammable String
Flammable Int
Flammable Number
(Read a) => Flammable (List a)
(Read a) => Flammable (Array a)
(Flammable a) => Flammable (Maybe a)
(Flammable a, Flammable b) => Flammable (Tuple a b)
(Flammable a, Flammable b) => Flammable (Either a b)
  

Interactive Instances


Interactive Boolean
Interactive Char
Interactive String
Interactive Int
Interactive Number
(Flammable a, Interactive t) => Interactive (a -> t)
(Generic a) => Interactive (List a)
(Generic a) => Interactive (Array a)
(Generic a) => Interactive (Maybe a)
(Generic a, Generic b) => Interactive (Tuple a b)
(Generic a, Generic b) => Interactive (Either a b)
  

Example


length :: String -> Int

flareCheck length
  

Example


charCodeAt :: Int -> String -> Maybe Int

flareCheck charCodeAt
  

Example


filter      :: forall a. (a -> Boolean) -> Array a -> Array a
even        :: Int -> Boolean

filter even :: Array Int -> Array Int

flareCheck (filter even)
  

Example


xor :: Boolean -> Boolean -> Boolean

flareCheck xor
  

Interactive documentation

Example: Data.Array module documentation

Thanks to …

  • The LambdaConf team!
  • All PureScript developers
  • Gabriel Gonzalez (idea for Flare)
  • Bodil Stokke (purescript-signal)
  • Phil Freeman (improvements in FlareCheck)

Thanks!


  https://github.com/sharkdp/

Comments about Ann

- Ann cannot be made a (law-abiding) Monad

- Ann as product of functors:


type Ann a f b = Product (Const a) f b

instance Monoid a => Applicative (Const a)
instance (Applicative f, Applicative g) => Applicative (Product f g)
  
(thanks to /r/purescript!)