repos / handle-examples.hs.git


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
M .gitignore
+1, -0
1@@ -1,3 +1,4 @@
2+.DS_Store
3 dist
4 dist-*
5 cabal-dev
M README.md
+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.
D backpack-handle/CHANGELOG.md
+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.
M backpack-handle/Main.hs
+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
M backpack-handle/backpack-handle.cabal
+1, -1
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
M backpack-handle/domain/WeatherReporter.hs
+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
M backpack-handle/test/Test.hs
+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"
A backpack-handles/LICENSE
+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.
A backpack-handles/Main.hs
+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
A backpack-handles/Setup.hs
+2, -0
1@@ -0,0 +1,2 @@
2+import Distribution.Simple
3+main = defaultMain
A backpack-handles/backpack-handles.cabal
+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)
A backpack-handles/domain/WeatherProvider.hsig
+14, -0
 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
A backpack-handles/domain/WeatherReporter.hsig
+10, -0
 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
A backpack-handles/impl/SuperWeatherProvider.hs
+16, -0
 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
A backpack-handles/impl/SuperWeatherReporter.hs
+25, -0
 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
A backpack-handles/test-impl/TestWeatherProvider.hs
+23, -0
 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
A backpack-handles/test/Test.hs
+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"
D records-handle/CHANGELOG.md
+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.
M records-handle/Main.hs
+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
M records-handle/domain/WeatherReporter.hs
+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
M records-handle/records-handle.cabal
+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
M records-handle/test/Test.hs
+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"
D simple-handle/CHANGELOG.md
+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.
M simple-handle/Main.hs
+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
M simple-handle/domain/WeatherReporter.hs
+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
M simple-handle/simple-handle.cabal
+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
A simple/LICENSE
+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.
A simple/Main.hs
+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
A simple/Setup.hs
+2, -0
1@@ -0,0 +1,2 @@
2+import Distribution.Simple
3+main = defaultMain
A simple/domain/WeatherProvider.hs
+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
A simple/domain/WeatherReporter.hs
+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
A simple/simple.cabal
+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