Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Functional Mocking (OsloSFP edition)

Functional Mocking (OsloSFP edition)

Mocking is an infamous technique from object-oriented programming. The goal is to be able to test stateful systems in small pieces by simulating the behaviour of certain objects. The problem with mocking is that it usually requires heavyweight frameworks and clutters test code. There are countless rants on that topic, but this talk isn't one. Instead, we'll explore the functional approach in Haskell: Designing a small language supporting the desired behaviour, and then writing interpreters which can execute its semantics in various ways. Testing I/O code was never easier.

Demo code: https://gist.github.com/larsrh/5cd5652c25ec84b8852c

Lars Hupel

June 23, 2015
Tweet

More Decks by Lars Hupel

Other Decks in Programming

Transcript

  1. What even is “Mocking”? “ In object-oriented programming, mock objects

    are simulated objects that mimic the behavior of real objects in controlled ways. Wikipedia: Mock object ” 2
  2. What even is “Mocking”? “ In object-oriented programming, mock objects

    are simulated objects that mimic the behavior of real objects in controlled ways. Wikipedia: Mock object ” 2
  3. Mocking in Scala Example: ScalaMock def testTurtle { val m

    = mock[Turtle] (m.setPosition _).expects(10.0, 10.0) (m.forward _).expects(5.0) (m.getPosition _).expects().returning(15.0, 10.0) drawLine(m, (10.0, 10.0), (15.0, 10.0)) } 3
  4. Mocking: Why Not? “ When you write a mockist test,

    you are testing the outbound calls of the SUT to ensure it talks properly to its suppliers ... Mockist tests are thus more coupled to the implementation of a method. Changing the nature of calls to collaborators usually cause a mockist test to break. Martin Fowler ” 4
  5. Mocking: Why Not? “ When you write a mockist test,

    you are testing the outbound calls of the SUT to ensure it talks properly to its suppliers ... Mockist tests are thus more coupled to the implementation of a method. Changing the nature of calls to collaborators usually cause a mockist test to break. Martin Fowler ” 4
  6. Functional Programming ▶ separation of data and operations ▶ parametric

    polymorphism ▶ higher-order functions ▶ lightweight interpreters 7
  7. A Simple Calculator data Expr = Literal Int | Var

    String | Sum Expr Expr evaluate :: Map String Int -> Expr -> Maybe Int 8
  8. A Simple Calculator data Expr = Literal Int | Var

    String | Sum Expr Expr evaluate :: (String -> Maybe Int) -> Expr -> Maybe Int 10
  9. A Simple Calculator data Expr a = Literal a |

    Var String | Sum (Expr a) (Expr a) evaluate :: Num a => (String -> Maybe a) -> Expr a -> Maybe a 10
  10. A (Not So) Simple Calculator data Expr a t =

    Literal a | Var t | Sum (Expr a t) (Expr a t) evaluate :: Num a => (t -> Maybe a) -> Expr a t -> Maybe a 10
  11. A (Not So) Simple Calculator data Expr a t =

    Literal a | Var t | Sum (Expr a t) (Expr a t) evaluateM :: (Num a, Monad m) => (t -> m a) -> Expr a t -> m a 10
  12. Why This Complexity? “ Always implement things when you actually

    need them, never when you just foresee that you need them. Ron Jeffries about YAGNI ” 12
  13. What Have We Gained? Abstraction over Num ▶ no messing

    around with the values ▶ caller knows that only the Num influences the behaviour Abstraction over Monad ▶ uniform data access ▶ Map ▶ database lookup ▶ reading from standard input 13
  14. Even More Abstraction -- get all variables vars :: Expr

    a t -> [t] vars = error ”some traversal” -- check definedness of all variables check :: Expr a (Maybe t) -> Maybe (Expr a t) check = error ”traversing again?!” -- substitute variables subst :: (t -> Expr a t) -> Expr a t -> Expr a t subst = error ”seriously?” 14
  15. Even More Abstraction -- get all variables vars :: Expr

    a t -> [t] vars = Data.Foldable.toList -- check definedness of all variables check :: Expr a (Maybe t) -> Maybe (Expr a t) check = Data.Traversable.sequenceA -- substitute variables subst :: (t -> Expr a t) -> Expr a t -> Expr a t subst = (=<<) 14
  16. YAGNI Revisited ▶ without early abstraction, many concepts stay hidden

    ▶ YAGNI limits thinking ▶ especially important when building libraries 18
  17. Interlude: Immutable Data Structures class Person { private String name;

    public String getName() { return name; } public void setName(String name) { this.name = name; } } 19
  18. Immutable Data in Haskell data Company = data Department =

    Company { Department { it :: Department boss :: Person , hr :: Department , budget :: Currency } } data Person = Person { name :: String } 21
  19. Updating Immutable Data company { it = (it company) {

    boss = (boss (it company)) { name = ”Grace Hopper” } } } 22
  20. Updating Immutable Data company { it = (it company) {

    boss = (boss (it company)) { name = ”Grace Hopper” } } } 22
  21. Lenses To The Rescue! The Naive Formulation data Lens a

    b = Lens { get :: a -> b set :: a -> b -> a } 24
  22. Lenses To The Rescue! The Naive Formulation data Lens a

    b = Lens { get :: a -> b set :: a -> b -> a } The Advanced Formulation type Lens a b = forall f. Functor f => (a -> f a) -> (b -> f b) 24
  23. What Have We Gained? ▶ Composition for free! set (it

    . boss . name) ”Grace Hopper” company ▶ Mocking for free! lenses are ordinary functions, so can be swapped out 25
  24. Calculator Revisited data Expr a t = Literal a |

    Var t | Sum (Expr a t) (Expr a t) 27
  25. Calculator Revisited data Expr a t = Literal a |

    Var t | Sum (Expr a t) (Expr a t) > :t Sum Sum :: Expr a t -> Expr a t -> Expr a t 27
  26. Calculator Revisited data Expr a t where Literal :: a

    -> Expr a t Var :: t -> Expr a t Sum :: Expr a t -> Expr a t -> Expr a t 27
  27. Calculator Revisited data Expr a t where Literal :: a

    -> Expr a t Var :: t -> Expr a t Sum :: Expr a t -> Expr a t -> Expr a t ▶ So far: Expr a t contains only a literals ▶ type a is constant in the whole expression ▶ What if we want heterogeneous operations? > :t even even :: Integral a => a -> Bool 27
  28. Calculator Revisited data Expr a where Literal :: a ->

    Expr a Sum :: Expr a -> Expr a -> Expr a 27
  29. Calculator Revisited data Expr a where Literal :: a ->

    Expr a Sum :: Num a => Expr a -> Expr a -> Expr a Even :: Integral a => Expr a -> Expr Bool 27
  30. Calculator Revisited data Expr a where Literal :: a ->

    Expr a Sum :: Num a => Expr a -> Expr a -> Expr a Even :: Integral a => Expr a -> Expr Bool Cast :: (a -> b) -> Expr a -> Expr b 27
  31. A Fancy Calculator ▶ we now have a datatype which

    represents (some) arithmetic operations ▶ Apart from evaluating, what can we do with it? 28
  32. A Fancy Calculator ▶ we now have a datatype which

    represents (some) arithmetic operations ▶ Apart from evaluating, what can we do with it? ▶ print ▶ count operations ▶ optimize 28
  33. A Fancy Calculator ▶ we now have a datatype which

    represents (some) arithmetic operations ▶ Apart from evaluating, what can we do with it? ▶ print ▶ count operations ▶ optimize 28
  34. 30

  35. monad (n.) (in the pantheistic philosophy of Giordano Bruno) a

    fundamental metaphysical unit that is spatially extended and psychically aware – Collins English Dictionary
  36. The Essence of IO ▶ a representation of a computation

    ▶ ... which interacts with the world 32
  37. The Essence of IO ▶ a representation of a computation

    ▶ ... which interacts with the world 32
  38. The Essence of IO ▶ a representation of a computation

    ▶ ... which interacts with the world ▶ in Haskell: may contain all sorts of effects ▶ in GHC: opaque, non-inspectable 32
  39. The Essence of IO ▶ a representation of a computation

    ▶ ... which interacts with the world ▶ in Haskell: may contain all sorts of effects ▶ in GHC: opaque, non-inspectable ▶ but: a better world is possible 32
  40. IO as a DSL ▶ calculator: datatype with one constructor

    per operation ▶ terminal application: datatype with one constructor per operation? ▶ read from standard input ▶ write to standard output 33
  41. IO as a DSL ▶ calculator: datatype with one constructor

    per operation ▶ terminal application: datatype with one constructor per operation? ▶ read from standard input ▶ write to standard output ▶ open file ▶ read from file ▶ ... 33
  42. IO as a DSL ▶ calculator: datatype with one constructor

    per operation ▶ terminal application: datatype with one constructor per operation? ▶ read from standard input ▶ write to standard output ▶ open file ▶ read from file ▶ ... 33
  43. A Datatype for Terminal IO data Terminal a where ReadLine

    :: Terminal String WriteLine :: String -> Terminal () 34
  44. A Datatype for Terminal IO data Terminal a where ReadLine

    :: Terminal String WriteLine :: String -> Terminal () ▶ Terminal is an ordinary GADT ▶ nicely represents what we want ▶ But what to do with it? 34
  45. A Datatype for Terminal IO data Terminal a where ReadLine

    :: Terminal String WriteLine :: String -> Terminal () ▶ Terminal is an ordinary GADT ▶ nicely represents what we want ▶ But what to do with it? 34 It looks like you need a monad. Want help with that?
  46. Free Monads Informally: We need a construction which, given any

    type constructor, produces an instance of class Monad m where (>>=) :: m a -> (a -> m b) -> m b return :: a -> m a 35
  47. Putting It All Together ▶ Recall our Terminal data type

    ▶ Stick it into FreeM and call it a day! data Terminal a where ReadLine :: Terminal String WriteLine :: String -> Terminal () 37
  48. Putting It All Together ▶ Recall our Terminal data type

    ▶ Stick it into FreeM and call it a day! ▶ ... except, no. data Terminal a where ReadLine :: Terminal String WriteLine :: String -> Terminal () 37
  49. Putting It All Together ▶ Recall our Terminal data type

    ▶ Stick it into FreeM and call it a day! ▶ ... except, no. ▶ It’s not even a Functor data Terminal a where ReadLine :: Terminal String WriteLine :: String -> Terminal () 37
  50. Putting It All Together ▶ Recall our Terminal data type

    ▶ Stick it into FreeM and call it a day! ▶ ... except, no. ▶ It’s not even a Functor data Terminal a where ReadLine :: Terminal String WriteLine :: String -> Terminal () 37 It looks like you need a functor. Want help with that?
  51. Free Functors Informally: We need a construction which, given any

    type constructor, produces an instance of class Functor f where fmap :: (a -> b) -> f a -> f b 38
  52. Coyoneda? “ What is sometimes called the co-Yoneda lemma is

    a basic fact about presheaves (a basic fact of topos theory): it says that every presheaf is a colimit of representables and more precisely that it is the “colimit over itself of all the representables contained in it”. nLab ” 39
  53. Coyoneda? “ What is sometimes called the co-Yoneda lemma is

    a basic fact about presheaves (a basic fact of topos theory): it says that every presheaf is a colimit of representables and more precisely that it is the “colimit over itself of all the representables contained in it”. nLab ” 39
  54. Simulating IO type IO a = PauseT (State RealWorld) a

    data RealWorld = RealWorld { workDir :: FilePath , files :: Map File Text , isPermitted :: FilePath -> IOMode -> Bool , handles :: Map Handle HandleData , nextHandle :: Integer , user :: User , mvars :: Map Integer MValue , nextMVar :: Integer , writeHooks :: [Handle -> Text -> IO ()] } 42
  55. Conclusion ▶ FP provides a set of techniques for abstraction

    over evaluation ▶ Use them! “ Premature evaluation is the root of all evil. ” 43
  56. Image Credits ▶ Manu Cornet, http://www.bonkersworld.net/building-software/ ▶ Randall Munroe, https://xkcd.com/1312/

    ▶ Thomas Kluyver, Kyle Kelley, Brian E. Granger, https://github.com/ipython/xkcd-font 45