From 7022ae9e748604303d2f0f24110d9ffc553e04b0 Mon Sep 17 00:00:00 2001 From: Evgenii Akentev Date: Sun, 31 Jan 2021 19:28:44 +0300 Subject: [PATCH 1/1] Here we go again --- .gitignore | 2 + archetypes/default.md | 2 + config.toml | 21 + .../implementations-of-the-handle-pattern.md | 658 ++++++++++++++++++ ...houghts-on-backpack-modules-and-records.md | 285 ++++++++ static/CNAME | 1 + themes/theme/layouts/404.html | 0 themes/theme/layouts/_default/list.html | 18 + themes/theme/layouts/_default/single.html | 13 + themes/theme/layouts/index.html | 18 + themes/theme/layouts/partials/footer.html | 6 + themes/theme/layouts/partials/header.html | 14 + themes/theme/layouts/partials/headline.html | 1 + themes/theme/layouts/partials/math.html | 3 + themes/theme/static/css/stylesheet.css | 115 +++ themes/theme/theme.toml | 1 + 16 files changed, 1158 insertions(+) create mode 100644 .gitignore create mode 100644 archetypes/default.md create mode 100644 config.toml create mode 100644 content/implementations-of-the-handle-pattern.md create mode 100644 content/thoughts-on-backpack-modules-and-records.md create mode 100644 static/CNAME create mode 100644 themes/theme/layouts/404.html create mode 100644 themes/theme/layouts/_default/list.html create mode 100644 themes/theme/layouts/_default/single.html create mode 100644 themes/theme/layouts/index.html create mode 100644 themes/theme/layouts/partials/footer.html create mode 100644 themes/theme/layouts/partials/header.html create mode 100644 themes/theme/layouts/partials/headline.html create mode 100644 themes/theme/layouts/partials/math.html create mode 100644 themes/theme/static/css/stylesheet.css create mode 100644 themes/theme/theme.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..592a6ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +public diff --git a/archetypes/default.md b/archetypes/default.md new file mode 100644 index 0000000..ac36e06 --- /dev/null +++ b/archetypes/default.md @@ -0,0 +1,2 @@ ++++ ++++ diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..19da133 --- /dev/null +++ b/config.toml @@ -0,0 +1,21 @@ +baseURL = "http://ak3n.com/" +languageCode = "en-us" +title = "ak3n.com" +theme = "theme" + +[params] + sitename = "ak3n.com" + +[markup] + [markup.highlight] + anchorLineNos = true + codeFences = true + guessSyntax = false + hl_Lines = "" + lineAnchors = "" + lineNoStart = 1 + lineNos = true + lineNumbersInTable = true + noClasses = true + style = "vs" + tabWidth = 4 \ No newline at end of file diff --git a/content/implementations-of-the-handle-pattern.md b/content/implementations-of-the-handle-pattern.md new file mode 100644 index 0000000..88657ab --- /dev/null +++ b/content/implementations-of-the-handle-pattern.md @@ -0,0 +1,658 @@ +--- +title: Implementations of the Handle pattern +date: 2021-01-31 +draft: false +--- + +In ["Monad Transformers and Effects with Backpack"](https://blog.ocharles.org.uk/posts/2020-12-23-monad-transformers-and-effects-with-backpack.html), [@acid2](https://twitter.com/acid2) presented how to apply Backpack to monad transformers. There is a less-popular approach to deal with effects — [Handle](https://jaspervdj.be/posts/2018-03-08-handle-pattern.html) ([Service](https://www.schoolofhaskell.com/user/meiersi/the-service-pattern)) pattern. I recommend reading both posts at first since they answer many questions regarding the design decisions behind the Handle pattern (why `IO`, why not type classes, etc). In this post, I want to show different implementations of the Handle pattern and compare them. All examples described below are available [in this repository](https://github.com/ak3n/handle-examples). + +### When you might need the Handle pattern [[simple](https://github.com/ak3n/handle-examples/tree/main/simple)] + +Suppose we have a domain logic with a side effect: + +```haskell +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 +``` + +```haskell +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 +``` + +At some point in time, there appeared a need for tests to ensure that the domain logic is correct. There are different ways to do that: + +- integration tests +- stub implementation of the service +- minimize the logic with side effects moving as much as possible to pure functions for proper unit testing +- maybe something else + +All solutions have their pros and cons and the final choice depends on many factors — especially on the number of side effects and how they interact with each other. We are interested in how to achieve the second one with the Handle pattern. + +### Simple Handle [[simple-handle](https://github.com/ak3n/handle-examples/tree/main/simple-handle)] + +Let's start with a simple Handle that doesn't support multiple implementations. +Here is the updated domain logic: + +```haskell +module WeatherReporter where + +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 :: 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 $ createWeatherReport weatherData + +``` + +And the implementation of the `WeatherProvider`: + +```haskell +module WeatherProvider where + +type Temperature = Int +data WeatherData = WeatherData { temperature :: Temperature } + +type Location = String +type Day = String + +-- | Our Handle is empty, but usually other dependencies are stored here +data Handle = Handle + +-- | Constructor for Handle +new :: Handle +new = Handle + +-- | This is some concrete implementation. +-- In this example we return a constant value. +getWeatherData :: Handle -> Location -> Day -> IO WeatherData +getWeatherData _ _ _ = return $ WeatherData 30 +``` + +We have wrapped our service with the Handle interface. It's not possible to have multiple implementations yet, but we got an interface of the service and can hide all the dependencies of the service into Handle. + +### Handle with records [[records-handle](https://github.com/ak3n/handle-examples/tree/main/records-handle)] + +The approach with records is described in the aforementioned posts. Records are used as a dictionary with functions just like dictionary passing with type classes but explicitly. `WeatherReporter` module stays the same — it continues to use `WeatherProvider.Handle` while the `WeatherProvider` becomes an interface: + +```haskell +module WeatherProvider where + +type Temperature = Int +data WeatherData = WeatherData { temperature :: Temperature } + +type Location = String +type Day = String + +-- | The interface of `WeatherProvider` with available methods. +data Handle = Handle { getWeatherData :: Location -> Day -> IO WeatherData } +``` + +The good thing is that we do not need to change our domain logic at all since `getWeatherData :: Handle -> Location -> Day -> IO WeatherData` has the same type. The interface allows us to create a concrete implementation for the application: + +```haskell +module SuperWeatherProvider where + +import WeatherProvider + +new :: Handle +new = Handle { getWeatherData = getSuperWeatherData } + +-- | This is some concrete implementation `WeatherProvider` interface +getSuperWeatherData :: Location -> Day -> IO WeatherData +getSuperWeatherData _ _ = return $ WeatherData 30 +``` + +And the stub for testing that we can control: + +```haskell +module TestWeatherProvider where + +import WeatherProvider + +-- | This is a configuration that allows to setup the provider for tests. +data Config = Config { initTemperature :: Temperature } + +new :: Config -> Handle +new config = Handle { getWeatherData = getTestWeatherData $ initTemperature config } + +-- | This is an implementation `WeatherProvider` interface for tests +getTestWeatherData :: Int -> Location -> Day -> IO WeatherData +getTestWeatherData temp _ _ = return $ WeatherData temp +``` + +The downside of this approach is the cost of an unknown function call mentioned in ["The Service Pattern"](https://www.schoolofhaskell.com/user/meiersi/the-service-pattern) post: + +> In terms of call overhead, we pay the cost of an unknown function call, which is probably a bit slower than a virtual method invocation in an OOP langauge like Java. If this becomes a performance bottleneck, we will have to avoid the abstraction and specialize at compile time. Backpack will allow us to do this in a principled fashion without losing modularity. + +Here is the STG of `WeatherReporter`: + +``` +weatherProvider = + \r [ds_s1C4] case ds_s1C4 of { Handle ds1_s1C6 -> ds1_s1C6; }; + +new = \r [eta_B1] Handle [eta_B1]; + +createWeatherReport1 = "The current temperature in London is "#; + +$wcreateWeatherReport = + \r [ww_s1C7] + let { + sat_s1Cd = + \u [] + case ww_s1C7 of { + I# ww3_s1C9 -> + case $wshowSignedInt 0# ww3_s1C9 [] of { + (#,#) ww5_s1Cb ww6_s1Cc -> : [ww5_s1Cb ww6_s1Cc]; + }; + }; + } in unpackAppendCString# createWeatherReport1 sat_s1Cd; + +createWeatherReport = + \r [w_s1Ce] + case w_s1Ce of { + WeatherData ww1_s1Cg -> $wcreateWeatherReport ww1_s1Cg; + }; + +getCurrentWeatherReportInLondon5 = "London"#; + +getCurrentWeatherReportInLondon4 = + \u [] unpackCString# getCurrentWeatherReportInLondon5; + +getCurrentWeatherReportInLondon3 = "now"#; + +getCurrentWeatherReportInLondon2 = + \u [] unpackCString# getCurrentWeatherReportInLondon3; + +getCurrentWeatherReportInLondon1 = + \r [ds_s1Ch void_0E] + case ds_s1Ch of { + Handle wph_s1Ck -> + case wph_s1Ck of { + Handle ds1_s1Cm -> + case + ds1_s1Cm + getCurrentWeatherReportInLondon4 + getCurrentWeatherReportInLondon2 + void# + of + { Unit# ipv1_s1Cp -> + let { sat_s1Cq = \u [] createWeatherReport ipv1_s1Cp; + } in Unit# [sat_s1Cq]; + }; + }; + }; + +getCurrentWeatherReportInLondon = + \r [eta_B2 void_0E] getCurrentWeatherReportInLondon1 eta_B2 void#; + +Handle = \r [eta_B1] Handle [eta_B1]; +``` + +`getCurrentWeatherReportInLondon` takes two arguments — the first one is `WeatherReporter`'s Handle dictionary which we pass to `getCurrentWeatherReportInLondon1`. We match on this dictionary to get `wph_s1Ck` — this is our `WeatherProvider`'s Handle. Matching on it we get `ds1_s1Cm` — `getWeatherData` function which is called with arguments: `getCurrentWeatherReportInLondon4 = "London"` and `getCurrentWeatherReportInLondon2 = "now"`. + +The result of `getWeatherData "London" "now" = ipv1_s1Cp` is then passed to `createWeatherReport` where we show the result in `$wcreateWeatherReport` and append it to `createWeatherReport1 = "The current temperature in London is "`. + +The STG looks as expected. There are two allocations: one in `getCurrentWeatherReportInLondon1` and the other one in `$wcreateWeatherReport`. + +### Handle with Backpack [[backpack-handle](https://github.com/ak3n/handle-examples/tree/main/backpack-handle)] + +`WeatherProvider` becomes a signature in the cabal file: + +``` +library domain + hs-source-dirs: domain + signatures: WeatherProvider + exposed-modules: WeatherReporter + default-language: Haskell2010 + build-depends: base +``` + +and we rename `WeatherProvider.hs` to `WeatherProvider.hsig` with a little change — instead of using a concrete type for `Temperature` we make it abstract and will instantiate in implementations. + +```haskell +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 +``` + +Our implementation module is almost the same as in the simple Handle case, but we have to follow the signature and export the same types. It's possible to move all common types to a different cabal library and import in the signature and the implementations. + +```haskell +module SuperWeatherProvider where + +type Temperature = Int +data WeatherData = WeatherData { temperature :: Temperature } + +type Location = String +type Day = String + +-- | Our Handle is empty, but usually other dependencies are stored here +data Handle = Handle + +-- | Constructor for Handle +new :: Handle +new = Handle + +-- | This is some concrete implementation. +-- In this example we return a constant value. +getWeatherData :: Handle -> Location -> Day -> IO WeatherData +getWeatherData _ _ _ = return $ WeatherData 30 +``` + +For tests we setup a configuration type to control the behavior: + +```haskell +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 +``` + +Now we need to tell which module to use instead of `WeatherProvider` hole. There are two ways: by using mixins or by reexporting modules as `WeatherProvider` in the definition libraries: + +``` +library impl + hs-source-dirs: impl + exposed-modules: SuperWeatherProvider + reexported-modules: SuperWeatherProvider as WeatherProvider + 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 backpack-handle-exe + main-is: Main.hs + build-depends: base, 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, QuickCheck, hspec, domain, test-impl +``` + +That's all. We do not need to change our domain logic or tests. Here is the STG of `WeatherReporter`: + +``` +weatherProvider = + \r [ds_s1Bq] case ds_s1Bq of { Handle ds1_s1Bs -> ds1_s1Bs; }; + +new = \r [eta_B1] Handle [eta_B1]; + +createWeatherReport1 = "The current temperature in London is "#; + +$wcreateWeatherReport = + \r [ww_s1Bt] + let { + sat_s1Bz = + \u [] + case ww_s1Bt of { + I# ww3_s1Bv -> + case $wshowSignedInt 0# ww3_s1Bv [] of { + (#,#) ww5_s1Bx ww6_s1By -> : [ww5_s1Bx ww6_s1By]; + }; + }; + } in unpackAppendCString# createWeatherReport1 sat_s1Bz; + +createWeatherReport = + \r [w_s1BA] + case w_s1BA of { + WeatherData ww1_s1BC -> $wcreateWeatherReport ww1_s1BC; + }; + +getCurrentWeatherReportInLondon3 = + \u [] + case $wshowSignedInt 0# 30# [] of { + (#,#) ww5_s1BE ww6_s1BF -> : [ww5_s1BE ww6_s1BF]; + }; + +getCurrentWeatherReportInLondon2 = + \u [] + unpackAppendCString# + createWeatherReport1 getCurrentWeatherReportInLondon3; + +getCurrentWeatherReportInLondon1 = + \r [ds_s1BG void_0E] + case ds_s1BG of { + Handle _ -> Unit# [getCurrentWeatherReportInLondon2]; + }; + +getCurrentWeatherReportInLondon = + \r [eta_B2 void_0E] getCurrentWeatherReportInLondon1 eta_B2 void#; + +Handle = \r [eta_B1] Handle [eta_B1]; +``` + +We can see that GHC inlined our constant implementation in the `getCurrentWeatherReportInLondon3` — we show `30` immediately. + +### Handles with Backpack [[backpack-handles](https://github.com/ak3n/handle-examples/tree/main/backpack-handles)] + +I decided to go further and make `WeatherReporter` a signature as well. Turned out this step required more actions with libraries. Here is `WeatherReporter.hsig`: + +```haskell +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 +``` + +Then we need to split the `domain` library into two because `WeatherReport` depends on `WeatherProvider`. I tried to implement them both with one implementation library but seems it's impossible. The structure of libraries becomes the following: + +``` +library domain-provider + 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-provider + +library impl-provider + hs-source-dirs: impl + exposed-modules: SuperWeatherProvider + reexported-modules: SuperWeatherProvider as WeatherProvider + default-language: Haskell2010 + build-depends: base + +library impl-reporter + hs-source-dirs: impl + exposed-modules: SuperWeatherReporter + reexported-modules: SuperWeatherReporter as WeatherReporter + default-language: Haskell2010 + build-depends: base, domain-provider +``` + +Instead of `domain` we have `domain-provider` and `domain-reporter`. It allows to depend on them individually and instantiate with different implementations. In [the example](https://github.com/ak3n/handle-examples/blob/main/backpack-handles/backpack-handles.cabal#L52), I have instantiated the provider with implementation from `test-impl` using the reporter from `impl-reporter`. This is useful if you want to gradually write tests for different parts of the logic. + +### Handle with Vinyl [[vinyl-handle](https://github.com/ak3n/handle-examples/tree/main/vinyl-handle)] + +Suppose that we want to extend our `WeatherData` and return not only temperature but wind's speed too. We need to add a field to `WeatherData`: + +``` +data WeatherData = WeatherData { temperature :: T.Temperature, wind :: W.WindSpeed } +``` + +But also we want to provide separate Handles for these values: `TemperatureProvider` and `WindProvider`. We can create these two providers and then duplicate their methods in `WeatherProvider`. That might work if there are no so many methods, but what if their number will grow? + +We know that records are nominally typed and can't be composed. There is a library called [vinyl](https://hackage.haskell.org/package/vinyl) that provides structural records supporting merge operation. I recommend Jon Sterling's [talk on Vinyl](https://vimeo.com/102785458) where you can learn why records are sheaves and other details on Vinyl. + +Let's explore what the Handle pattern will look like if we replace records with Vinyl records. We create our data providers `TemperatureProvider` and `WindProvider`: + +```haskell +module TemperatureProvider where + +import HandleRec +import QueryTypes + +type Temperature = Int + +type Methods = '[ '("getTemperatureData", (Location -> Day -> IO Temperature)) ] + +type Handle = HandleRec Methods + +getTemperatureData :: Handle -> Location -> Day -> IO Temperature +getTemperatureData = getMethod @"getTemperatureData" + +``` + +```haskell +module WindProvider where + +import HandleRec +import QueryTypes + +type WindSpeed = Int + +type Methods = '[ '("getWindData", (Location -> Day -> IO WindSpeed)) ] + +type Handle = HandleRec Methods + +getWindData :: Handle -> Location -> Day -> IO WindSpeed +getWindData = getMethod @"getWindData" +``` + +Note that we provide `getTemperatureData` and `getWindData` functions which satisfy our Handle interface. + +We can compose them together and extend them to create `WeatherProvider`: + +```haskell +module WeatherProvider where + +import Data.Vinyl.TypeLevel +import HandleRec +import qualified WindProvider as W +import qualified TemperatureProvider as T +import QueryTypes + +data WeatherData = WeatherData { temperature :: T.Temperature, wind :: W.WindSpeed } + +-- We union the methods of providers and extend it with a common method. +type Methods = '[ '("getWeatherData", (Location -> Day -> IO WeatherData)) + ] ++ W.Methods ++ T.Methods + +type Handle = HandleRec Methods + +getWeatherData :: Handle -> Location -> Day -> IO WeatherData +getWeatherData = getMethod @"getWeatherData" +``` + +Here is how our providers are implemented: + +```haskell +module SuperTemperatureProvider where + +import Data.Vinyl +import TemperatureProvider +import QueryTypes + +new :: Handle +new = Field getSuperTemperatureData :& RNil + +getSuperTemperatureData :: Location -> Day -> IO Temperature +getSuperTemperatureData _ _ = return 30 +``` + +```haskell +module SuperWindProvider where + +import Data.Vinyl +import WindProvider +import QueryTypes + +new :: Handle +new = Field getSuperWindData :& RNil + +getSuperWindData :: Location -> Day -> IO WindSpeed +getSuperWindData _ _ = return 5 +``` + +```haskell +module SuperWeatherProvider where + +import Data.Vinyl +import WeatherProvider +import qualified TemperatureProvider +import qualified WindProvider +import QueryTypes + +new :: WindProvider.Handle -> TemperatureProvider.Handle -> Handle +new wp tp = Field getSuperWeatherData :& RNil <+> wp <+> tp + +-- | This is some concrete implementation `WeatherProvider` interface +getSuperWeatherData :: Location -> Day -> IO WeatherData +getSuperWeatherData _ _ = return $ WeatherData 30 10 +``` + +The domain logic and tests stay the same thanks to the Handle interface. The STG of `WeatherReporter`: + +``` +createWeatherReport2 = "The current temperature in London is "#; + +createWeatherReport1 = " and wind speed is "#; + +$wcreateWeatherReport = + \r [ww_s2fz ww1_s2fA] + let { + sat_s2fN = + \u [] + case ww_s2fz of { + I# ww3_s2fC -> + case $wshowSignedInt 0# ww3_s2fC [] of { + (#,#) ww5_s2fE ww6_s2fF -> + let { + sat_s2fM = + \s [] + let { + sat_s2fL = + \u [] + case ww1_s2fA of { + I# ww9_s2fH -> + case $wshowSignedInt 0# ww9_s2fH [] of { + (#,#) ww11_s2fJ ww12_s2fK -> + : [ww11_s2fJ ww12_s2fK]; + }; + }; + } in unpackAppendCString# createWeatherReport1 sat_s2fL; + } in ++_$s++ sat_s2fM ww5_s2fE ww6_s2fF; + }; + }; + } in unpackAppendCString# createWeatherReport2 sat_s2fN; + +createWeatherReport = + \r [w_s2fO] + case w_s2fO of { + WeatherData ww1_s2fQ ww2_s2fR -> + $wcreateWeatherReport ww1_s2fQ ww2_s2fR; + }; + +getCurrentWeatherReportInLondon5 = "London"#; + +getCurrentWeatherReportInLondon4 = + \u [] unpackCString# getCurrentWeatherReportInLondon5; + +getCurrentWeatherReportInLondon3 = "now"#; + +getCurrentWeatherReportInLondon2 = + \u [] unpackCString# getCurrentWeatherReportInLondon3; + +getCurrentWeatherReportInLondon1 = + \r [ds_s2fS void_0E] + case ds_s2fS of { + Handle wph_s2fV -> + case wph_s2fV of { + :& x1_s2fX _ -> + case x1_s2fX of { + Field _ x2_s2g1 -> + case + x2_s2g1 + getCurrentWeatherReportInLondon4 + getCurrentWeatherReportInLondon2 + void# + of + { Unit# ipv1_s2g4 -> + let { sat_s2g5 = \u [] createWeatherReport ipv1_s2g4; + } in Unit# [sat_s2g5]; + }; + }; + }; + }; + +getCurrentWeatherReportInLondon = + \r [eta_B2 void_0E] getCurrentWeatherReportInLondon1 eta_B2 void#; + +Handle = \r [eta_B1] Handle [eta_B1]; +``` + +As we can see there is not much vinyl-specific runtime overhead in this case — we pattern match on `wph_s2fV` and `x1_s2fX` to get the function `x2_s2g1`. But keep in mind that accessing an element is linear since Vinyl is based on HList and compilation time will grow because of type-level machinery. Vinyl can be replaced for an alternative with logarithmic complexity. + +### Conclusions + +No surprises here. Backpack works as expected specifying things at compile time. Vinyl allows to compose records and can be replaced with any [alternative](https://github.com/danidiaz/red-black-record#alternatives). The Handle pattern works since it's just a type signature `... :: Handle -> ...`. + +The Handle allows us to hide dependencies and to create interfaces, allowing us to easily replace the implementation without changes on the client-side — statically using Backpack for better performance or dynamically using records or alternatives in runtime (in first-class modules manner). Backpack might be too tedious for Handles that depend on each other but in simple cases, it introduces not much additional cost compared to records. And it's possible to mix them. + +This post inspired me to write a follow-up post on [Backpack, modules, and records](/thoughts-on-backpack-modules-and-records). diff --git a/content/thoughts-on-backpack-modules-and-records.md b/content/thoughts-on-backpack-modules-and-records.md new file mode 100644 index 0000000..ebfe1df --- /dev/null +++ b/content/thoughts-on-backpack-modules-and-records.md @@ -0,0 +1,285 @@ +--- +title: Thoughts on Backpack, modules, and records +date: 2021-01-31 +draft: false +--- + +In ["Implementations of the Handle pattern"](/implementations-of-the-handle-pattern), I have explored how Backpack might be used for the Handle pattern. It helped me to take a better look at Backpack and reflect a bit on modules and records in Haskell. + +### What's wrong with Backpack? + +Backpack works as expected but there are downsides of the current implementation: + 1. It's `cabal`-only. It means it's hard to integrate it in any big project because developers tend to choose different build tools. + 2. It's not user-friendly — it may throw [some mysterious errors](https://twitter.com/ak3n/status/1345358277803180034) without explaining what's going on. Or [parse errors in mixins block](https://github.com/haskell/cabal/issues/5150). + 3. It's not maintained. `git blame` tells that there was not much activity in Backpack modules in `cabal` repository for three years. + 4. It does not support mutual recursive modules, sealing, higher-order units, hierarchical modules, and other things. The functionality can be improved. + 5. It lacks documentation. + 6. It's [not supported by Nix](https://github.com/NixOS/nixpkgs/issues/40128). I don't think it's Backpack's problem but since Nix has become a very popular choice for Haskell's build infrastructure it's a strong blocker for Backpack's integration. + 7. It's not used. Even if we close the eyes on problems with cabal or nix, the developers don't use Backpack because it's too [heavy-weight to use](https://www.reddit.com/r/haskell/comments/7ea0qg/backpack_why_does_this_cabal_file_fail_to_build/dq46myo/) and it's unidiomatic. + +These issues seem to be related: no users => bad support and bad support => no users. Backpack was an attempt to bring a subset of ML modules system to Haskell, the language with anti-modular features and a big amount of legacy code written in that style. In my opinion, that attempt failed. The ML modules system is about explicit manipulation of modules in the program by design — the style that most Haskell programmers seem to dislike. + +What's the point of having an unused feature which is unmaintained and unfinished then? I write this not to criticize Backpack, but to understand its future. It's a great project that explored the important design space. The aforementioned downsides can be fixed and improved. The real question is "Should the community invest resources in that?" + +### What's in the proposals + +I looked at [the GHC proposals](https://github.com/ghc-proposals/ghc-proposals/) to see if there are any improvements of modularity in the future and, in my opinion, most proposals are focused on concrete problems instead of reflecting on the whole language. It may work well but also it may create inconsistency in the language. + +#### [QualifiedDo](https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0216-qualified-do.rst) + +`QualifiedDo` brings syntax sugar for overloading `do` notation: + +```haskell +{-# LANGUAGE LinearTypes #-} +{-# LANGUAGE NoImplicitPrelude #-} +module Control.Monad.Linear (Monad(..)) where + +class Monad m where + return :: a #-> m a + (>>=) :: m a #-> (a #-> m b) #-> mb + +----------------- + +module M where + +import qualified Control.Monad.Linear as Linear + +f :: Linear.Monad m => a #-> m b +f a = Linear.do + b <- someLinearFunction a Linear.>>= someOtherLinearFunction + c <- anotherLinearFunction b + Linear.return c + +g :: Monad m => a -> m b +g a = do + b <- someNonLinearFunction a >>= someOtherNonLinearFunction + c <- anotherNonLinearFunction b + return c +``` + +`Linear.do` overloads `return` and `(>>=)` with functions from `Control.Monad.Linear`. Hey, isn't this is why Backpack was created? To replace the implementation? Also, it can be achieved with records. And the authors used that solution in [linear-base](https://github.com/tweag/linear-base/blob/0d6165fbd8ad84dd1574a36071f00a6137351637/src/System/IO/Resource.hs#L119-L120): + +```haskell +(<*>) :: forall a b. RIO (a ->. b) ->. RIO a ->. RIO b +f <*> x = do + f' <- f + x' <- x + Linear.pure $ f' x' + where + Linear.Builder { .. } = Linear.monadBuilder +``` + +The quote from the proposal: + +>There is a way to emulate ``-XQualifiedDo`` in current GHC using ``-XRecordWildcards``: have no ``(>>=)`` and such in scope, and import a builder with ``Builder {..} = builder``. It is used in `linear-base`. This is not a very good solution: it is rather a impenetrable idiom, and, if a single function uses several builders, it yields syntactic contortion (which is why shadowing warnings are deactivated [here](https://github.com/tweag/linear-base/blob/0d6165fbd8ad84dd1574a36071f00a6137351637/src/System/IO/Resource.hs#L1)) + +I don't mind about using records. It might be uncomfortable, but not worthing to patch the language. Maybe I'm missing something important. + +At first glance, this syntactic extension may even look okay, but a more fundamental solution would be local modules support where you can open a module locally and bring its content into the scope: + +```haskell +(<*>) :: forall a b. RIO (a ->. b) ->. RIO a ->. RIO b +f <*> x = let open Linear in do + f' <- f + x' <- x + Linear.pure $ f' x' +``` + +```haskell +(<*>) :: forall a b. RIO (a ->. b) ->. RIO a ->. RIO b +f <*> x = do + f' <- f + x' <- x + Linear.pure $ f' x' + where + { .. } = open Linear +``` + +It's just like opening the records! It might be tedious to write these imports instead of `Linear.do`, but we wouldn't need to bring new functionality to the language if we had local imports. Maybe it means that "do notation" is really important to the Haskell community but to me, it feels like a temporary hack at the moment, not a fundamental part of the language. + +#### [Local modules](https://github.com/goldfirere/ghc-proposals/blob/local-modules/proposals/0000-local-modules.rst) + +`LocalModules` sounds like a good step in the right direction. It will be possible to create modules in modules and to open them locally! Just like in the example above with `Linear` module. Another good thing is that each `data` declaration implicitly creates a new local module, [like in Agda](https://agda.readthedocs.io/en/v2.6.1/language/record-types.html#record-modules). I'm not sure if it's possible to open them, I haven't found it in the proposal, but that would unify the opening of records and modules. Unfortunately, the proposal doesn't explore the interaction with Backpack: + +>This proposal does not appear to interact with Backpack. It does not address signatures, the key feature in Backpack. Perhaps the ideas here could be extended to work with signatures. + +Does it mean that there will be `LocalSignatures` in the future? Why not design the whole mechanism at once? Is there a risk of missing something important that would be hard to fix later? + +#### [First class modules](https://github.com/michaelpj/ghc-proposals/blob/imp/first-class-modules/proposals/0000-first-class-modules.rst) + +This proposal is about making a module a first-class entity in the language. Its status is *dormant* because it's too much to change while the benefits seem to be the same as in `LocalModules`. While `LocalModules` is a technical proposal that goes into details, `First class modules` is more about language design proposal. `LocalModules` does not replace `First class modules`, but *a part of it*. This is exactly what I was looking for, the proposal that tries to build a vision for the language, what it might look in the future. The proposal mentions Backpack only once: + +> Interface files must be able to handle the possibility that an exported name refers to a module. This may have some interaction with Backpack. + +Unfortunately, the work on this proposal was stopped because most interesting things were described in `LocalModules` — a proposal that is about *local namespaces*, not *modules*. There is nothing about abstraction there. + +#### Summing up + +It's important to remember that Haskell modules are just namespaces without any additional features. It's possible to import the contents of a module, everything or only specific things, and control what to export (except instances). While `LocalModules` will significantly improve developers' lives providing a flexible way to deal with scopes, it's unclear what the whole picture with modules will look like. And why Backpack is ignored by the proposals? What's wrong with it? + +The Backpack is ignored because it's not a proper part of the Haskell language. It's a mechanism that consists of *mixin linking* which is indifferent to Haskell source code, and *typechecking against interfaces*, which is purely the concern of the compiler. That's why it depends so much on Cabal — a frontend client for mixins description which can be replaced the compiler supports typechecking of interfaces. More details are available in Edward Z. Yang's [thesis](https://github.com/ezyang/thesis/releases/tag/rev20170925). + +### Modules and records, and type classes + +We understood that Backpack is a tool that uses the compiler and the package manager to express the features that are not supported in the language internally. Is it possible for Haskell to go further and improve the modularity in the language internally? To fit together anti-modular type classes and some subset of modules with abstract types? I want to explore what's in the ML languages at first. + +#### What's in the ML land + +Let's take a look at what happens in languages with the ML modules system. The users of these languages accept the cost of the explicitness. Although, they would be glad to reduce the boilerplate when it's necessary. + +The theoretical works demonstrate that type classes and modules are not that different. ["ML Modules and Haskell Type Classes: A Constructive Comparison"](https://github.com/ak3n/modules-papers/blob/master/pdfs/Wehr_ML_modules_and_Haskell_type_classes.pdf) showed that a subset of Haskell with type classes can be expressed with ML modules and vice versa with some limitations. ["Modular Type Classes"](https://github.com/ak3n/modules-papers/blob/master/pdfs/main-long.pdf) went further in expressing type classes with modules. It starts with modules as the fundamental concept and then recovers type classes as a particular mode of use of modularity. + +The example of bringing type classes to the language with modules can be found in ["Modular implicits"](https://arxiv.org/abs/1512.01895) — an extension to the OCaml language for ad-hoc polymorphism inspired by Scala implicits and modular type classes. It's not supported by mainstream OCaml yet because the proper implementation requires a big change in the language. But here is a small example of how it might look for the `Show` type class: + +```ocaml +(* + We express `Show` as a module type. It's a module signature that + can be instantiated with different implementations. It has an + abstract type `t` and a function `show` that takes `t` and returns `string`. + + class Show t where + show :: t -> String +*) +module type Show = sig + type t + val show : t -> string +end + +(* + This is a global function `show` with an implicit argument `S : Show`. + It means that it takes a module that implements the module type `Show`, + an argument `x`, and calls `S.show x` using the `show` from `S`. + + show' :: Show a => a -> String + show' = show +*) +let show {S : Show} x = S.show x + +(* + An implementation of `Show` for `int`. + + instance Show Int where + show = string_of_int +*) +implicit module Show_int = struct + type t = int + let show x = string_of_int x +end + +(* + An implementation of `Show` for `float`. + + instance Show Float where + show = string_of_float +*) +implicit module Show_float = struct + type t = float + let show x = string_of_float x +end + +(* + An implementation of `Show` for `list`. + Since `list` contains elements of some type `t` we require + a module `S` to show them. + + instance Show a => Show [a] where + show = string_of_list show +*) +implicit module Show_list {S : Show} = struct + type t = S.t list + let show x = string_of_list S.show x +end + +let () = + print_endline ("Show an int: " ^ show 5); + print_endline ("Show a float: " ^ show 1.5); + print_endline ("Show a list of ints: " ^ show [1; 2; 3]); +``` + +As we can see the languages with ML modules system can get ad-hoc programming support to some degree. + +#### Why modularity? + +Haskell's culture relies heavily on type classes. It's a foundation for monads, do-notation, and libraries. All instances of type classes should be unique and it's a cultural belief because the *global uniqueness of instances* is [just an expectation that isn't forced by GHC](http://blog.ezyang.com/2014/07/type-classes-confluence-coherence-global-uniqueness/). What's the point of living in an anti-modular myth that forbids the integration of possible modularity features? + +>It is easy to dismiss this example as an implementation wart in GHC, and continue pretending that global uniqueness of instances holds. However, the problem with global uniqueness of instances is that they are inherently nonmodular: you might find yourself unable to compose two components because they accidentally defined the same type class instance, even though these instances are plumbed deep in the implementation details of the components. This is a big problem for Backpack, or really any module system, whose mantra of separate modular development seeks to guarantee that linking will succeed if the library writer and the application writer develop to a common signature. + +The example with `QualifiedDo` shows that even one of the key features of Haskell needs modularity — monads and do-notation. That required additional patching of the language because do-notation is based on type classes instead of modules. + +`LocalModules` may help to deal with scopes (it's really annoying sometimes), but they won't provide the abstraction mechanism — the key feature of a module system. Modules with abstract types allow replacing the implementations because of Reynolds’s abstraction theorem. + +Type classes allow to create interfaces and implement them for different types, but they are usually global and ad-hoc. They make the project, the libraries, and the whole Hackage into one global world. Not always in practice for now, but it can be very annoying to track the import statement that brings an instance into the scope especially in a big codebase. Type classes become too expensive at some point as an abstraction tool. + +#### Local signatures + +I wrote `LocalSignatures` as a joke when was writing about `LocalModules`, but then later I understood that it might work since GHC already supports type checking against interfaces it can be used to implement signatures on the language level. It might require lots of syntax changes though. Something like that: + +```haskell +module type EQ where + data T + eq :: T -> T -> Bool + +module type MAP where + data Key + data Map a + empty :: Map a + lookup :: Key -> Map a -> Maybe a + add :: Key -> a -> Map a -> Map a + +module Map (Key :: EQ) as MAP with (type Key as Key.T) where + type Key = Key.T + type Map a = Key -> Maybe a + empty x = Nothing + lookup x m = m x + add x y m = \z -> if Key.eq z x then Just y else m z +``` + +We have created two signatures (module types) `EQ` and `MAP`. And a module `Map` that implements `MAP`. Writing `as MAP`, we seal the module `Map` and hide the implementation details behind the signature `MAP`, specifying that `Key.T` is the same as `Key`. + +This is just an example to demonstrate how it might look like. It introduces a module system to the language but on a different level. We can't pass a module to a function — modules and core terms are separated. + +#### Modules and records + +The Handle pattern demonstrates that Haskell's modules and records are alike in some way — they implement the Handle interface provided to the user, both containing functions. Records are dynamic and can be replaced in the runtime while signatures are static and can be specialized during the compilation. What if we could merge them into one entity? + +[1ML](https://github.com/ak3n/modules-papers/blob/master/pdfs/1ml-jfp-draft.pdf) does that — it merges two languages in one: *core* with types and expressions, and *modules*, with signatures, structures and functors. And requires only System Fω. The example for local signatures mentioned above is the code I adapted from 1ML. Here is the original version: + +```ocaml +type EQ = +{ + type t; + eq : t -> t -> bool; +}; + +type MAP = +{ + type key; + type map a; + empty 'a : map a; + lookup 'a : key -> map a -> opt a; + add 'a : key -> a -> map a -> map a; +}; + +Map (Key : EQ) :> MAP with (type key = Key.t) = +{ + type key = Key.t; + type map a = key -> opt a; + empty = fun x => none; + lookup x m = m x; + add x y m = fun z => if Key.eq z x then some y else m z; +}; + +datasize = 117; +OrderedMap = Map {include Int; eq = curry (==)} :> MAP; +HashMap = Map {include Int; eq = curry (==)} :> MAP; + +Map = if datasize <= 100 then OrderedMap else HashMap : MAP; +``` + +Looks similar, but now we can choose what module to use by analyzing the runtime value similar to what can be done in Haskell with records, vinyl, or something else. But they lack the abstract types support. + +### Conclusions + +I haven't thought about how local signatures or first-class modules may interact with type classes to achieve the same experience as with implicits in Agda, Scala, or OCaml. According to [the recent paper on new implicits calculus](https://lirias.kuleuven.be/2343745), implicits don't have global uniqueness of instances, just GHC's type classes in practice, but have coherence and stability of type substitutions. It makes me wonder why not try to drop the global uniqueness property and improve the module system instead. + +The recently created [Haskell Foundation](https://haskell.foundation/en/) states that it's going to address the need for *driving adoption*. It means more developers will write Haskell, more libraries, more projects, more type classes, and more instances that are *global* for the entire Hackage. I think it's important to decide what to do with Backpack, consider the improvement of the module system, and design the language more carefully taking in mind the whole picture. diff --git a/static/CNAME b/static/CNAME new file mode 100644 index 0000000..e45682d --- /dev/null +++ b/static/CNAME @@ -0,0 +1 @@ +ak3n.com \ No newline at end of file diff --git a/themes/theme/layouts/404.html b/themes/theme/layouts/404.html new file mode 100644 index 0000000..e69de29 diff --git a/themes/theme/layouts/_default/list.html b/themes/theme/layouts/_default/list.html new file mode 100644 index 0000000..8b3cafb --- /dev/null +++ b/themes/theme/layouts/_default/list.html @@ -0,0 +1,18 @@ +{{ partial "header.html" . }} + +
+ +
+ +{{ partial "footer.html" . }} diff --git a/themes/theme/layouts/_default/single.html b/themes/theme/layouts/_default/single.html new file mode 100644 index 0000000..6544323 --- /dev/null +++ b/themes/theme/layouts/_default/single.html @@ -0,0 +1,13 @@ +{{ partial "header.html" . }} + +
+

{{ .Title }}

+
+ {{ .Date.Format (.Site.Params.dateform | default "January 02, 2006") }} +
+
+ {{ partial "headline.html" .Content }} +
+
+ +{{ partial "footer.html" . }} diff --git a/themes/theme/layouts/index.html b/themes/theme/layouts/index.html new file mode 100644 index 0000000..fbc0a19 --- /dev/null +++ b/themes/theme/layouts/index.html @@ -0,0 +1,18 @@ +{{ partial "header.html" . }} + +
+ +
+ +{{ partial "footer.html" . }} diff --git a/themes/theme/layouts/partials/footer.html b/themes/theme/layouts/partials/footer.html new file mode 100644 index 0000000..e6e24df --- /dev/null +++ b/themes/theme/layouts/partials/footer.html @@ -0,0 +1,6 @@ + + + {{ if or .Params.math .Site.Params.math }} + {{ partial "math.html" . }} + {{ end }} + \ No newline at end of file diff --git a/themes/theme/layouts/partials/header.html b/themes/theme/layouts/partials/header.html new file mode 100644 index 0000000..0e1c21c --- /dev/null +++ b/themes/theme/layouts/partials/header.html @@ -0,0 +1,14 @@ + + + + + + + {{.Title}} + + + +
+

¯\_(ツ)_/¯

+
+ diff --git a/themes/theme/layouts/partials/headline.html b/themes/theme/layouts/partials/headline.html new file mode 100644 index 0000000..cd86bf7 --- /dev/null +++ b/themes/theme/layouts/partials/headline.html @@ -0,0 +1 @@ +{{ . | replaceRE "()" "${1} # ${3}" | safeHTML }} \ No newline at end of file diff --git a/themes/theme/layouts/partials/math.html b/themes/theme/layouts/partials/math.html new file mode 100644 index 0000000..e078d13 --- /dev/null +++ b/themes/theme/layouts/partials/math.html @@ -0,0 +1,3 @@ + + + diff --git a/themes/theme/static/css/stylesheet.css b/themes/theme/static/css/stylesheet.css new file mode 100644 index 0000000..912d84e --- /dev/null +++ b/themes/theme/static/css/stylesheet.css @@ -0,0 +1,115 @@ +body { + font-family: 'Ubuntu', sans-serif; + color: black; + margin: auto; + max-width: 50em; +} + +#page-title { + text-align: center; +} + +#page-title h1 a, .post a { + color: black; +} + +a { + text-decoration: none; +} + +#dashboard ul { + list-style-type: none; +} + +ul li h4 { + margin-bottom: 0.5em; +} + +.post a:hover { + text-decoration: underline; +} + +.date-time-title { + display: inline-block; +} + +.posts { + padding-left: 2em; +} + +.post, .subcategory { + padding-left: 1em; +} + +.blog-post { + padding-left: 1em; +} + +.blog-post img { + width: 65%; +} + +.blog-post img.full { + width: 100%; +} + +.blog-post img, iframe { + display: block; + margin-left: auto; + margin-right: auto; +} + +.blog-post-content { + line-height: 1.5; +} + +.blog-post-content code { + background-color: rgba(27,31,35,.05); + padding: .2em .4em; + margin: 0; + line-height: 1; +} + +.blog-post-content pre { + background-color: rgba(27,31,35,.05); +} + +.blog-post-content pre code { + background-color: transparent; + padding: 0; +} + +.headline-hash { + font-size: 70%; + color: #000; + visibility: hidden; +} + +h1 { + font-style: normal; + font-size: 2rem; +} + +h2 { + font-style: normal; + font-size: 1.5rem; + margin: 0 0 .8rem; +} + +h3 { + font-style: normal; + font-size: 1.3rem; + margin: 0 0 .8rem; +} + +h4 { + font-style: normal; + font-size: 1.1rem; + margin: 0 0 .8rem; +} + +h1:hover a, +h2:hover a, +h3:hover a, +h4:hover a, +h5:hover a { visibility: visible } \ No newline at end of file diff --git a/themes/theme/theme.toml b/themes/theme/theme.toml new file mode 100644 index 0000000..f99396e --- /dev/null +++ b/themes/theme/theme.toml @@ -0,0 +1 @@ +name = "theme" -- 2.50.1