moo-nad (empty) → 0.1.0.1
raw patch · 9 files changed
+366/−0 lines, 9 filesdep +basedep +dep-tdep +moo-nad
Dependencies added: base, dep-t, moo-nad, mtl, tasty, tasty-hunit, transformers
Files
- CHANGELOG.md +5/−0
- LICENSE +30/−0
- lib-example-impl/Moo.hs +47/−0
- lib-example-logic-that-logs/LogicThatLogs.hs +21/−0
- lib-example-logic-that-logs/Moo.hsig +50/−0
- lib/Moo.hsig +34/−0
- lib/Moo/Prelude.hs +63/−0
- moo-nad.cabal +82/−0
- test/tests.hs +34/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for moo-nad++## 0.1.0.0 -- YYYY-mm-dd++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2021, Daniel Diaz++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Daniel Diaz nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ lib-example-impl/Moo.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE TypeSynonymInstances #-}+-- | This is the implementation module for the signature of the same name.+module Moo (module Moo, runReaderT) where++import Data.Kind+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Reader+import Control.Monad.Dep.Has++type M = ReaderT EnvIO IO+type E = EnvIO+type D = IO+liftD :: D x -> M x+liftD = lift++-- | Same definition as the one in the module signature.+class HasLogger d e | e -> d where+ logger :: e -> Int -> String -> d ()++-- | Same definition as the one in the module signature.+data Counter d = Counter { + askCounter :: d Int,+ incCounter :: Int -> d ()+ }++-- | This is the point at which we define a concrete type for the environment.+--+-- The 'HasLogger' instance is backed by a bare function defined at the top+-- level, while the 'Counter' component is a field by itself.+data EnvIO = EnvIO {+ _logger :: Int -> String -> IO (),+ _counter :: Counter IO+ }++instance HasLogger D E where+ logger (EnvIO {_logger}) = _logger++-- | 'Control.Monad.Dep.Has' establishes a relationship between the environment 'E',+-- the nominal record defining a component, and the effect monad the component+-- uses in the environment..+instance Has Counter D E where+ dep (EnvIO {_counter}) = _counter+
+ lib-example-logic-that-logs/LogicThatLogs.hs view
@@ -0,0 +1,21 @@+-- | Example of program logic in the ReaderT-record-of-functions style that+-- abstracts away the concrete reader-like monad by depending on a module+-- signature.+module LogicThatLogs where++-- The main Moo signature has been expanded through signature merging with a signature+-- local to this library.+import Moo +import Moo.Prelude++-- | 'M' is a 'Monad'. Which one? We don't know yet, because this program logic+-- lives in an indefinite package.+logic :: M ()+logic = do+ -- Use 'self' for special-purpose HasX capabilities, like HasLogger.+ self logger 7 "this is a message"+ -- Use 'call' for record components located through "Control.Monad.Dep.Has.Has". + c <- call askCounter+ if c == 0 + then call incCounter 1+ else pure ()
+ lib-example-logic-that-logs/Moo.hsig view
@@ -0,0 +1,50 @@+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FunctionalDependencies #-}+-- | The logic in this sub-library needs to know how to perform logging,+-- and use a counter+-- +-- To allow for that, we expand the signature through signature merging, +-- defining a 'HasLogger' typeclass and demanding that the environment 'E'+-- has an instance.+--+-- We also define a 'Counter' component (basically a record-of-functions polymorphic on +-- the base monad) and tie it to the environment 'E' using the generic Has class from +-- Control.Monad.Dep.Has.+signature Moo where++import Data.Kind+import Control.Monad.Dep.Has++data D :: Type -> Type +data E :: Type +instance HasLogger D E -- this wasn't in the base Moo signature+instance Has Counter D E -- this wasn't, either++-- A typeclass for extracting some capability from an environment.+--+-- Program logic invokes should invoke its method using "Moo.Prelude.self".+--+-- Typeclass definitions are allowed in signatures,+-- methods and all. The corresponding definitions in the implementation+-- modules must match exactly.+--+-- The typeclass could also have been imported from another package.+class HasLogger d e | e -> d where+ logger :: e -> Int -> String -> d ()++-- A component is a record-of-functions parameterized by the effect monad +-- used by the functions.+--+-- Components are tied to the environment using the "Control.Monad.Dep.Has.Has" typeclass,+-- and program logic should invoke their functions using "Moo.Prelude.call".+--+-- Datatype definitions are allowed in signatures. The corresponding definitions in+-- the implementation modules must match exactly.+--+-- The datatype could also have been imported from another package.+data Counter d = Counter { + askCounter :: d Int,+ incCounter :: Int -> d ()+ }+
+ lib/Moo.hsig view
@@ -0,0 +1,34 @@+{-# LANGUAGE KindSignatures, MultiParamTypeClasses #-}+-- | A module signature for reader-like monads carrying a record-of-functions as+-- environment.+--+-- To be useful for writing indefinite code, it should be expanded through "signature merging"+-- to require extra instances for the main monad 'M' (like MonadIO or MonadUnliftIO) and/or+-- the environment type 'E' (like some HasX typeclass).+--+-- https://github.com/danidiaz/really-small-backpack-example/tree/master/lesson3-signature-merging+signature Moo where++import Control.Monad+import Control.Monad.Reader+import Data.Kind++-- | A reader-like monad.+data M :: Type -> Type+instance Functor M+instance Applicative M+instance Monad M+instance MonadReader E M++-- | The monad environment.+data E :: Type++-- | The monad in which the functions in the environment have their effects.+--+-- We don't require Functor, Applicative or Monad from 'D' because the +-- plan is to always lift 'D' values to 'M' as soon as they're obtained.+data D :: Type -> Type++-- | Lifts an effects from the secondary monad 'D' into 'M'.+liftD :: D x -> M x+
+ lib/Moo/Prelude.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE StandaloneKindSignatures #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE ScopedTypeVariables #-}+-- | Invocation helpers for functions with effects in a monad 'D' and which are+-- stored in the environment 'E' of a reader-like monad 'M'.+module Moo.Prelude (+ self,+ call,+ -- * Re-exports from Moo+ M,+ E,+ D +) where++import Moo+import Data.Kind+import GHC.TypeLits+import Control.Monad.Reader+import Control.Monad.Dep.Has++type Call :: Type -> Constraint+class Call curried where+ type LiftedD curried :: Type+ -- | Given a way of extracting from the environment 'E' a @curried@+ -- function that ends in a 'D' action, lift the @curried@ 'D'-function into the main+ -- monad 'M'.+ self :: (E -> curried) -> LiftedD curried++instance Call (D r) where+ type LiftedD (D r) = M r+ self extractor = do+ e <- ask+ liftD $ extractor e++instance Call curried' => Call (a -> curried') where+ type LiftedD (a -> curried') = a -> LiftedD curried'+ self extractor a = + let extractor' = \e -> extractor e a+ in self @curried' extractor'++-- | Provided that the environment 'E' 'Control.Monad.Dep.Has' a @component@, and+-- given a way of extracting from the @component@ a @curried@ function that+-- ends in a 'D' action, lift the @curried@ 'D'-function into the main monad+-- 'M'.+--+-- The extractor must be monomorphic on the @component@, so that the intended+-- instance of 'Control.Monad.Dep.Has' is picked. +--+-- The typical case is for the @component@ to be a parameterized record and for+-- the extractor to be a field accessor.+call :: forall component curried . (Has component D E, Call curried) => (component D -> curried) -> LiftedD curried+call extractor = self (extractor . dep @component)+
+ moo-nad.cabal view
@@ -0,0 +1,82 @@+cabal-version: 3.0+name: moo-nad+version: 0.1.0.1++synopsis: Invocation helpers for the ReaderT-record-of-functions style.+description:+ Using a record-of-functions as the environment of some + reader-like monad is a common way of structuring Haskell + applications, somewhat resembling dependency injection in OOP.+ + We often want our program logic to be polymorphic over both the+ concrete monad and the environment. One common solution is to+ abstract the monad using @MonadReader@, and abstract the environment+ using @HasX@-style typeclasses.+ + One minor annoyance though is that invoking the function in the + environment is often a bit cumbersome: you have to ask+ the environment for the function, and then lift the result of+ the function back into the reader-like monad.+ + This library supports a special twist on @ReaderT@-record-of-functions + style: instead of depending only on typeclasses for abstraction, + we also use a module signature. This comes with different tradeoffs.+ + One benefit is that we support a simpler way of invoking functions from the+ environment, using a helper that takes care of both asking the environment+ and lifting function results, and which works uniformly for functions of any+ arity.++license: BSD-3-Clause+license-file: LICENSE+author: Daniel Diaz Carrete+maintainer: diaz_carrete@yahoo.com++extra-source-files: CHANGELOG.md++common common+ build-depends: base ^>= 4.15.0.0,+ mtl ^>= 2.2,+ dep-t ^>= 0.4.4+ default-language: Haskell2010++-- Indefinite library which provides an invocation helper over some abstract+-- readerlike monad which carries a record-of-functions.+library+ import: common+ signatures: Moo+ exposed-modules: Moo.Prelude+ build-depends: + hs-source-dirs: lib++-- Example indefinite program logic.+library example-logic-that-logs+ import: common+ -- The Moo signature is enriched locally through signature merging.+ signatures: Moo+ exposed-modules: LogicThatLogs+ build-depends: + moo-nad+ hs-source-dirs: lib-example-logic-that-logs++-- Example library that implements the Moo signature.+library example-impl+ import: common+ exposed-modules: Moo+ -- Notice that we don't depend explicitly on the Moo signature.+ build-depends: + transformers ^>= 0.5+ hs-source-dirs: lib-example-impl++-- The tests put together the moo-nad library, the example indefinite package+-- with the program logic, and the sublibrary carrying the implementation.+test-suite tests+ import: common+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: tests.hs+ build-depends: + tasty ^>= 1.3.1,+ tasty-hunit ^>= 0.10.0.2,+ example-logic-that-logs,+ example-impl
+ test/tests.hs view
@@ -0,0 +1,34 @@+module Main where++import Test.Tasty+import Test.Tasty.HUnit+import Data.IORef++import LogicThatLogs+import Moo++tests :: TestTree+tests =+ testGroup+ "All"+ [ + let test = do+ loggerRef <- newIORef "" + counterRef <- newIORef 0+ let env = EnvIO {+ _logger = \_ -> writeIORef loggerRef+ , _counter = Counter {+ askCounter = readIORef counterRef+ , incCounter = \v -> modifyIORef counterRef (+v)+ }+ }+ runReaderT logic env+ msg <- readIORef loggerRef+ assertEqual "log output" "this is a message" msg+ counterVal <- readIORef counterRef+ assertEqual "counter output" 1 counterVal+ in testCase "run logic" $ test+ ]++main :: IO ()+main = defaultMain tests