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