packages feed

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 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