+.DS_Store
dist
dist-*
cabal-dev
-# backpack-handle-example
+This repository contains examples of the Handle pattern. We start from `simple` that contains domain logic and iterate trying to use records and backpack as a way to test the domain logic.
-The examples of the Handle pattern in simple case, with records, and backpack.
+- `simple` is a library with two modules: `WeatherProvider` (provides data) and `WeatherReporter` (uses the data to create a report).
+
+- `simple-handle` introduces a single-implementation Handle to both modules.
+
+- `records-handle` implements a polymorphic Handle for `WeatherProvider` allowing us to replace the implementation and write tests for domain logic.
+
+- `backpack-handle` does the same thing as `records-handle` but using Backpack instead. It allows us to specialize function calls.
+
+- `backpack-handles` goes further and makes both `WeatherProvider` and `WeatherReporter` signatures. Unfortunately, it doesn't work yet.
+++ /dev/null
-# Revision history for backpack-handle-pattern
-
-## 0.1.0.0 -- YYYY-mm-dd
-
-* First version. Released on an unsuspecting world.
main :: IO ()
main = do
let wph = SuperWeatherProvider.new
- weatherReportInLondon <- WeatherReporter.getCurrentWeatherReportInLondon wph
+ let wrh = WeatherReporter.new wph
+ weatherReportInLondon <- WeatherReporter.getCurrentWeatherReportInLondon wrh
putStrLn weatherReportInLondon
default-language: Haskell2010
build-depends: base
-executable backpack-handle-exe
+executable main
main-is: Main.hs
build-depends: base >=4.13 && <4.14
, impl
module WeatherReporter where
-import WeatherProvider
+import qualified WeatherProvider
type WeatherReport = String
--- | This is domain logic. It uses `WeatherProvider` to get the actual data.
-getCurrentWeatherReportInLondon :: WeatherProvider.Handle -> IO WeatherReport
-getCurrentWeatherReportInLondon wph = do
+-- | We hide dependencies in the handle
+data Handle = Handle { weatherProvider :: WeatherProvider.Handle }
+
+-- | Constructor for Handle
+new :: WeatherProvider.Handle -> Handle
+new = Handle
+
+-- | Domain logic. Usually some pure code that might use mtl, free monads, etc.
+createWeatherReport :: WeatherProvider.WeatherData -> WeatherReport
+createWeatherReport (WeatherProvider.WeatherData temp) =
+ "The current temperature in London is " ++ (show temp)
+
+-- | Domain logic that uses external dependency to get data and process it.
+getCurrentWeatherReportInLondon :: Handle -> IO WeatherReport
+getCurrentWeatherReportInLondon (Handle wph) = do
weatherData <- WeatherProvider.getWeatherData wph "London" "now"
- return $ "Current temperature in London is " ++ (show $ temperature weatherData)
\ No newline at end of file
+ return $ createWeatherReport weatherData
import Test.Hspec
-import qualified TestWeatherProvider
import qualified WeatherProvider
import qualified WeatherReporter
main :: IO ()
main = hspec spec
-weatherWithTemp :: WeatherProvider.Temperature -> WeatherProvider.Handle
-weatherWithTemp = TestWeatherProvider.new . TestWeatherProvider.Config
+weatherWithTemp :: WeatherProvider.Temperature -> WeatherReporter.Handle
+weatherWithTemp = WeatherReporter.new
+ . WeatherProvider.new
+ . WeatherProvider.Config
spec :: Spec
spec = describe "WeatherReporter" $ do
it "weather in London is 0" $ do
weatherReportInLondon <- WeatherReporter.getCurrentWeatherReportInLondon $
weatherWithTemp 0
- weatherReportInLondon `shouldBe` "Current temperature in London is 0"
+ weatherReportInLondon `shouldBe` "The current temperature in London is 0"
it "weather in London is -5" $ do
weatherReportInLondon <- WeatherReporter.getCurrentWeatherReportInLondon $
weatherWithTemp (-5)
- weatherReportInLondon `shouldBe` "Current temperature in London is -5"
+ weatherReportInLondon `shouldBe` "The current temperature in London is -5"
--- /dev/null
+MIT License
+
+Copyright (c) 2021 Evgenii Akentev
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
--- /dev/null
+module Main where
+
+import qualified SuperWeatherProvider
+import qualified SuperWeatherReporter
+
+-- | This is an actual application where we use
+-- our concrete implementation of `WeatherProvider`.
+main :: IO ()
+main = do
+ let wph = SuperWeatherProvider.new
+ let wpr = SuperWeatherReporter.Handle wph
+ weatherReportInLondon <- SuperWeatherReporter.getCurrentWeatherReportInLondon wpr
+ putStrLn weatherReportInLondon
--- /dev/null
+import Distribution.Simple
+main = defaultMain
--- /dev/null
+cabal-version: >=2
+name: backpack-handles
+version: 0.1.0.0
+license-file: LICENSE
+author: Evgenii Akentev
+maintainer: i@ak3n.com
+build-type: Simple
+extra-source-files: CHANGELOG.md
+
+library domain
+ hs-source-dirs: domain
+ signatures: WeatherProvider
+ default-language: Haskell2010
+ build-depends: base
+
+library domain-reporter
+ hs-source-dirs: domain
+ signatures: WeatherReporter
+ default-language: Haskell2010
+ build-depends: base, domain
+
+library impl
+ hs-source-dirs: impl
+ exposed-modules: SuperWeatherProvider
+ , SuperWeatherReporter
+ reexported-modules: SuperWeatherProvider as WeatherProvider,
+ SuperWeatherReporter as WeatherReporter
+ default-language: Haskell2010
+ build-depends: base
+
+library test-impl
+ hs-source-dirs: test-impl
+ exposed-modules: TestWeatherProvider
+ reexported-modules: TestWeatherProvider as WeatherProvider
+ default-language: Haskell2010
+ build-depends: base
+
+executable main
+ main-is: Main.hs
+ build-depends: base >=4.13 && <4.14
+ , impl
+ , domain
+ default-language: Haskell2010
+
+test-suite spec
+ type: exitcode-stdio-1.0
+ hs-source-dirs: test
+ main-is: Test.hs
+ default-language: Haskell2010
+ build-depends: base >= 4.7 && < 5
+ , QuickCheck
+ , hspec
+ , domain
+ , test-impl
+ , impl
+ mixins:
+ impl (WeatherProvider as UnusedWeatherProvider, WeatherReporter)
--- /dev/null
+signature WeatherProvider where
+
+data Temperature
+instance Show Temperature
+
+data WeatherData = WeatherData { temperature :: Temperature }
+
+type Location = String
+type Day = String
+
+data Handle
+
+-- | The interface of `WeatherProvider` with available methods.
+getWeatherData :: Handle -> Location -> Day -> IO WeatherData
--- /dev/null
+signature WeatherReporter where
+
+import qualified WeatherProvider
+
+type WeatherReport = String
+
+data Handle = Handle { weatherProvider :: WeatherProvider.Handle }
+
+-- | This is domain logic. It uses `WeatherProvider` to get the actual data.
+getCurrentWeatherReportInLondon :: Handle -> IO WeatherReport
--- /dev/null
+module SuperWeatherProvider where
+
+type Temperature = Int
+data WeatherData = WeatherData { temperature :: Temperature }
+
+type Location = String
+type Day = String
+
+data Handle = Handle
+
+new :: Handle
+new = Handle
+
+-- | This is some concrete implementation `WeatherProvider` interface
+getWeatherData :: Handle -> Location -> Day -> IO WeatherData
+getWeatherData _ _ _ = return $ WeatherData 30
--- /dev/null
+module SuperWeatherReporter where
+
+import qualified SuperWeatherProvider
+
+type WeatherReport = String
+
+type WeatherProviderHandle = SuperWeatherProvider.Handle
+
+-- | We hide dependencies in the handle
+data Handle = Handle { weatherProvider :: SuperWeatherProvider.Handle }
+
+-- | Constructor for Handle
+new :: SuperWeatherProvider.Handle -> Handle
+new = Handle
+
+-- | Domain logic. Usually some pure code that might use mtl, free monads, etc.
+createWeatherReport :: SuperWeatherProvider.WeatherData -> WeatherReport
+createWeatherReport (SuperWeatherProvider.WeatherData temp) =
+ "The current temperature in London is " ++ (show temp)
+
+-- | Domain logic that uses external dependency to get data and process it.
+getCurrentWeatherReportInLondon :: Handle -> IO WeatherReport
+getCurrentWeatherReportInLondon (Handle wph) = do
+ weatherData <- SuperWeatherProvider.getWeatherData wph "London" "now"
+ return $ createWeatherReport weatherData
--- /dev/null
+module TestWeatherProvider where
+
+type Temperature = Int
+data WeatherData = WeatherData { temperature :: Temperature }
+
+type Location = String
+type Day = String
+
+-- | This is a configuration that allows to setup the provider for tests.
+data Config = Config
+ { initTemperature :: Temperature
+ }
+
+data Handle = Handle
+ { config :: Config
+ }
+
+new :: Config -> Handle
+new = Handle
+
+-- | This is an implementation `WeatherProvider` interface for tests
+getWeatherData :: Handle -> Location -> Day -> IO WeatherData
+getWeatherData (Handle conf) _ _ = return $ WeatherData $ initTemperature conf
--- /dev/null
+import Test.Hspec
+
+import qualified WeatherProvider
+import qualified WeatherReporter
+
+main :: IO ()
+main = hspec spec
+
+weatherWithTemp :: WeatherProvider.Temperature -> WeatherReporter.Handle
+weatherWithTemp t = WeatherReporter.new
+ $ WeatherProvider.new
+ $ WeatherProvider.Config t
+
+spec :: Spec
+spec = describe "WeatherReporter" $ do
+ it "weather in London is 0" $ do
+ weatherReportInLondon <- WeatherReporter.getCurrentWeatherReportInLondon $
+ weatherWithTemp 0
+ weatherReportInLondon `shouldBe` "The current temperature in London is 0"
+ it "weather in London is -5" $ do
+ weatherReportInLondon <- WeatherReporter.getCurrentWeatherReportInLondon $
+ weatherWithTemp (-5)
+ weatherReportInLondon `shouldBe` "The current temperature in London is -5"
+++ /dev/null
-# Revision history for backpack-handle-pattern
-
-## 0.1.0.0 -- YYYY-mm-dd
-
-* First version. Released on an unsuspecting world.
main :: IO ()
main = do
let wph = SuperWeatherProvider.new
- weatherReportInLondon <- WeatherReporter.getCurrentWeatherReportInLondon wph
+ let wrh = WeatherReporter.new wph
+ weatherReportInLondon <- WeatherReporter.getCurrentWeatherReportInLondon wrh
putStrLn weatherReportInLondon
module WeatherReporter where
-import WeatherProvider
+import qualified WeatherProvider
type WeatherReport = String
--- | This is domain logic. It uses `WeatherProvider` to get the actual data.
-getCurrentWeatherReportInLondon :: WeatherProvider.Handle -> IO WeatherReport
-getCurrentWeatherReportInLondon wph = do
+-- | We hide dependencies in the handle
+data Handle = Handle { weatherProvider :: WeatherProvider.Handle }
+
+-- | Constructor for Handle
+new :: WeatherProvider.Handle -> Handle
+new = Handle
+
+-- | Domain logic. Usually some pure code that might use mtl, free monads, etc.
+createWeatherReport :: WeatherProvider.WeatherData -> WeatherReport
+createWeatherReport (WeatherProvider.WeatherData temp) =
+ "The current temperature in London is " ++ (show temp)
+
+-- | Domain logic that uses external dependency to get data and process it.
+getCurrentWeatherReportInLondon :: Handle -> IO WeatherReport
+getCurrentWeatherReportInLondon (Handle wph) = do
weatherData <- WeatherProvider.getWeatherData wph "London" "now"
- return $ "Current temperature in London is " ++ (show $ temperature weatherData)
\ No newline at end of file
+ return $ createWeatherReport weatherData
build-depends: base
, domain
-executable records-handle-exe
+executable main
main-is: Main.hs
build-depends: base >=4.13 && <4.14
, domain
main :: IO ()
main = hspec spec
-weatherWithTemp :: WeatherProvider.Temperature -> WeatherProvider.Handle
-weatherWithTemp = TestWeatherProvider.new . TestWeatherProvider.Config
+weatherWithTemp :: WeatherProvider.Temperature -> WeatherReporter.Handle
+weatherWithTemp = WeatherReporter.new
+ . TestWeatherProvider.new
+ . TestWeatherProvider.Config
spec :: Spec
spec = describe "WeatherReporter" $ do
it "weather in London is 0" $ do
weatherReportInLondon <- WeatherReporter.getCurrentWeatherReportInLondon $
weatherWithTemp 0
- weatherReportInLondon `shouldBe` "Current temperature in London is 0"
+ weatherReportInLondon `shouldBe` "The current temperature in London is 0"
it "weather in London is -5" $ do
weatherReportInLondon <- WeatherReporter.getCurrentWeatherReportInLondon $
weatherWithTemp (-5)
- weatherReportInLondon `shouldBe` "Current temperature in London is -5"
+ weatherReportInLondon `shouldBe` "The current temperature in London is -5"
+++ /dev/null
-# Revision history for backpack-handle-pattern
-
-## 0.1.0.0 -- YYYY-mm-dd
-
-* First version. Released on an unsuspecting world.
main :: IO ()
main = do
let wph = WeatherProvider.new
- weatherReportInLondon <- WeatherReporter.getCurrentWeatherReportInLondon wph
+ let wrh = WeatherReporter.new wph
+ weatherReportInLondon <- WeatherReporter.getCurrentWeatherReportInLondon wrh
putStrLn weatherReportInLondon
module WeatherReporter where
-import WeatherProvider
+import qualified WeatherProvider
type WeatherReport = String
+-- | We hide dependencies in the handle
+data Handle = Handle { weatherProvider :: WeatherProvider.Handle }
+
+-- | Constructor for Handle
+new :: WeatherProvider.Handle -> Handle
+new = Handle
+
-- | Domain logic. Usually some pure code that might use mtl, free monads, etc.
-createWeatherReport :: WeatherData -> WeatherReport
-createWeatherReport (WeatherData temp) =
+createWeatherReport :: WeatherProvider.WeatherData -> WeatherReport
+createWeatherReport (WeatherProvider.WeatherData temp) =
"The current temperature in London is " ++ (show temp)
-- | Domain logic that uses external dependency to get data and process it.
-getCurrentWeatherReportInLondon :: WeatherProvider.Handle -> IO WeatherReport
-getCurrentWeatherReportInLondon wph = do
+getCurrentWeatherReportInLondon :: Handle -> IO WeatherReport
+getCurrentWeatherReportInLondon (Handle wph) = do
weatherData <- WeatherProvider.getWeatherData wph "London" "now"
return $ createWeatherReport weatherData
default-language: Haskell2010
build-depends: base
-executable simple-handle-exe
+executable main
main-is: Main.hs
build-depends: base >=4.13 && <4.14
, domain
--- /dev/null
+MIT License
+
+Copyright (c) 2021 Evgenii Akentev
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
--- /dev/null
+module Main where
+
+import qualified WeatherReporter
+
+main :: IO ()
+main = do
+ weatherReportInLondon <- WeatherReporter.getCurrentWeatherReportInLondon
+ putStrLn weatherReportInLondon
--- /dev/null
+import Distribution.Simple
+main = defaultMain
--- /dev/null
+module WeatherProvider where
+
+type Temperature = Int
+data WeatherData = WeatherData { temperature :: Temperature }
+
+type Location = String
+type Day = String
+
+-- | This is some concrete implementation.
+-- In this example we return a constant value.
+getWeatherData :: Location -> Day -> IO WeatherData
+getWeatherData _ _ = return $ WeatherData 30
--- /dev/null
+module WeatherReporter where
+
+import qualified WeatherProvider
+
+type WeatherReport = String
+
+-- | Domain logic. Usually some pure code that might use mtl, free monads, etc.
+createWeatherReport :: WeatherProvider.WeatherData -> WeatherReport
+createWeatherReport (WeatherProvider.WeatherData temp) =
+ "The current temperature in London is " ++ (show temp)
+
+-- | Domain logic that uses external dependency to get data and process it.
+getCurrentWeatherReportInLondon :: IO WeatherReport
+getCurrentWeatherReportInLondon = do
+ weatherData <- WeatherProvider.getWeatherData "London" "now"
+ return $ createWeatherReport weatherData
--- /dev/null
+cabal-version: >=2
+name: simple
+version: 0.1.0.0
+license-file: LICENSE
+author: Evgenii Akentev
+maintainer: i@ak3n.com
+build-type: Simple
+extra-source-files: CHANGELOG.md
+
+library domain
+ hs-source-dirs: domain
+ exposed-modules: WeatherProvider
+ , WeatherReporter
+ default-language: Haskell2010
+ build-depends: base
+
+executable main
+ main-is: Main.hs
+ build-depends: base >=4.13 && <4.14
+ , domain
+ default-language: Haskell2010