Skip to content

Commit

Permalink
Refactor to more consistently use "capability style" in loading + ini…
Browse files Browse the repository at this point in the history
…tializing code (#1392)

Closes #1122 .  General principles:

- Use `SystemFailure` as error rather than `Text` as much as possible, and use `prettyFailure` only at the very top level.
- Replace `ExceptT` with `Has (Throw SystemFailure)` constraint.
- Use `Accum (Seq SystemFailure)` constraints to accumulate warnings that should not abort computation, rather than returning a pair of a list of warnings + result.
- Use `Has (Lift IO)` constraint instead of `MonadIO`, which means using `sendIO` instead of `liftIO`.
- In general, use `runThrow` to dispatch a `Throw` constraint (results in returning an `Either`, just like `runExceptT`), and `runM` to dispatch a final `Lift IO` constraint to result in an `IO` computation.
- Use `withThrow` to adapt from one type of error to another.
  • Loading branch information
byorgey authored Aug 6, 2023
1 parent c69d76e commit 8aea6a2
Show file tree
Hide file tree
Showing 28 changed files with 696 additions and 388 deletions.
139 changes: 139 additions & 0 deletions src/Control/Carrier/Accum/FixedStrict.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
-- This file is a temporary copy of the code from fused-effects, with
-- https://github.com/fused-effects/fused-effects/issues/449 fixed
-- (the fixed line of code is marked with a comment below). We should
-- keep this only until the above issue is fixed upstream.
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}

-- | A carrier for 'Accum' effects.
-- This carrier performs its append operations strictly and thus avoids the space leaks inherent in lazy writer monads.
-- These appends are left-associative; as such, @[]@ is a poor choice of monoid for computations that entail many calls to 'tell'.
-- The [Seq](http://hackage.haskell.org/package/containersdocs/Data-Sequence.html) or [DList](http://hackage.haskell.org/package/dlist) monoids may be a superior choice.
--
-- @since 1.1.2.0
module Control.Carrier.Accum.FixedStrict (
-- * Accum carrier
runAccum,
execAccum,
evalAccum,
AccumC (AccumC),

-- * Accum effect
module Control.Effect.Accum,
) where

import Control.Algebra
import Control.Applicative (Alternative (..))
import Control.Effect.Accum
import Control.Monad (MonadPlus (..))
import Control.Monad.Fail as Fail
import Control.Monad.Fix
import Control.Monad.IO.Class
import Control.Monad.Trans.Class

-- | Run an 'Accum' effect with a 'Monoid'al log, applying a continuation to the final log and result.
--
-- @
-- 'runAccum' w0 ('pure' a) = 'pure' (w0, a)
-- @
-- @
-- 'runAccum' w0 ('add' w) = 'pure' (w0 <> w, ())
-- @
-- @
-- 'runAccum' w0 ('add' w >> 'look') = 'pure' (w0 <> w, w0 <> w)
-- @
--
-- @since 1.1.2.0
runAccum :: w -> AccumC w m a -> m (w, a)
runAccum = flip runAccumC
{-# INLINE runAccum #-}

-- | Run a 'Accum' effect (typically with a 'Monoid'al log),
-- producing the final log and discarding the result value.
--
-- @
-- 'execAccum' w = 'fmap' 'fst' . 'runAccum' w
-- @
--
-- @since 1.1.2.0
execAccum :: (Functor m) => w -> AccumC w m a -> m w
execAccum w = fmap fst . runAccum w
{-# INLINE execAccum #-}

-- | Run a 'Accum' effect (typically with a 'Monoid'al log),
-- producing the result value and discarding the final log.
--
-- @
-- 'evalAccum' w = 'fmap' 'snd' . 'runAccum' w
-- @
--
-- @since 1.1.2.0
evalAccum :: (Functor m) => w -> AccumC w m a -> m a
evalAccum w = fmap snd . runAccum w
{-# INLINE evalAccum #-}

-- | @since 1.1.2.0
newtype AccumC w m a = AccumC {runAccumC :: w -> m (w, a)}

instance Monoid w => MonadTrans (AccumC w) where
lift ma = AccumC $ \_ -> (mempty,) <$> ma
{-# INLINE lift #-}

instance Functor m => Functor (AccumC w m) where
fmap f ma = AccumC $ fmap (fmap f) . runAccumC ma
{-# INLINE fmap #-}

instance (Monad m, Monoid w) => Applicative (AccumC w m) where
pure a = AccumC $ const $ pure (mempty, a)
{-# INLINE pure #-}

mf <*> ma = AccumC $ \w -> do
(w', f) <- runAccumC mf w
(w'', a) <- runAccumC ma $ mappend w w'
return (mappend w' w'', f a)
{-# INLINE (<*>) #-}

instance (Alternative m, Monad m, Monoid w) => Alternative (AccumC w m) where
empty = lift empty
{-# INLINE empty #-}

ma1 <|> ma2 = AccumC $ \w -> runAccumC ma1 w <|> runAccumC ma2 w
{-# INLINE (<|>) #-}

instance (Monad m, Monoid w) => Monad (AccumC w m) where
ma >>= f = AccumC $ \w -> do
(w', a) <- runAccumC ma w
(w'', b) <- runAccumC (f a) $ mappend w w'
return (mappend w' w'', b)
{-# INLINE (>>=) #-}

instance (MonadPlus m, Monoid w) => MonadPlus (AccumC w m) where
mzero = lift mzero
{-# INLINE mzero #-}

ma1 `mplus` ma2 = AccumC $ \w -> runAccumC ma1 w `mplus` runAccumC ma2 w
{-# INLINE mplus #-}

instance (MonadFail m, Monoid w) => MonadFail (AccumC w m) where
fail = AccumC . const . Fail.fail
{-# INLINE fail #-}

instance (MonadFix m, Monoid w) => MonadFix (AccumC w m) where
mfix ma = AccumC $ \w -> mfix $ flip runAccumC w . ma . snd
{-# INLINE mfix #-}

instance (MonadIO m, Monoid w) => MonadIO (AccumC w m) where
liftIO = lift . liftIO
{-# INLINE liftIO #-}

instance (Algebra sig m, Monoid w) => Algebra (Accum w :+: sig) (AccumC w m) where
alg hdl sig ctx = AccumC $ \w -> case sig of
L accum -> case accum of
Add w' -> pure (w', ctx)
Look -> pure (mempty, w <$ ctx)
R other -> thread (uncurry runAccum ~<~ hdl) other (mempty, ctx) -- THIS IS THE FIXED LINE
{-# INLINE alg #-}
13 changes: 8 additions & 5 deletions src/Swarm/App.hs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ module Swarm.App where

import Brick
import Brick.BChan
import Control.Carrier.Lift (runM)
import Control.Carrier.Throw.Either (runThrow)
import Control.Concurrent (forkIO, threadDelay)
import Control.Lens (view, (%~), (&), (?~))
import Control.Monad (forever, void, when)
import Control.Monad.Except (runExceptT)
import Control.Monad.IO.Class (liftIO)
import Data.IORef (newIORef, writeIORef)
import Data.Text qualified as T
import Data.Text.IO qualified as T
import Graphics.Vty qualified as V
import Swarm.Game.Failure (SystemFailure)
import Swarm.Game.Robot (ErrorLevel (..), LogSource (ErrorTrace, Said))
import Swarm.Language.Pretty (prettyText)
import Swarm.ReadableIORef (mkReadonly)
import Swarm.TUI.Controller
import Swarm.TUI.Model
Expand Down Expand Up @@ -45,9 +48,9 @@ app eventHandler =
-- some communication channels, and runs the UI.
appMain :: AppOpts -> IO ()
appMain opts = do
res <- runExceptT $ initAppState opts
res <- runM . runThrow $ initAppState opts
case res of
Left errMsg -> T.hPutStrLn stderr errMsg
Left err -> T.hPutStrLn stderr (prettyText @SystemFailure err)
Right s -> do
-- Send Frame events as at a reasonable rate for 30 fps. The
-- game is responsible for figuring out how many steps to take
Expand Down Expand Up @@ -112,9 +115,9 @@ demoWeb :: IO ()
demoWeb = do
let demoPort = 8080
res <-
runExceptT $ initAppState (defaultAppOpts {userScenario = demoScenario})
runM . runThrow $ initAppState (defaultAppOpts {userScenario = demoScenario})
case res of
Left errMsg -> T.putStrLn errMsg
Left err -> T.putStrLn (prettyText @SystemFailure err)
Right s -> do
appStateRef <- newIORef s
chan <- newBChan 5
Expand Down
41 changes: 16 additions & 25 deletions src/Swarm/Doc/Gen.hs
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,12 @@ module Swarm.Doc.Gen (
noPageAddresses,
) where

import Control.Arrow (left)
import Control.Effect.Lift
import Control.Effect.Throw
import Control.Lens (view, (^.))
import Control.Lens.Combinators (to)
import Control.Monad (zipWithM, zipWithM_)
import Control.Monad.Except (ExceptT (..), runExceptT, withExceptT)
import Control.Monad.IO.Class (liftIO)
import Data.Containers.ListUtils (nubOrd)
import Data.Either.Extra (eitherToMaybe)
import Data.Foldable (find, toList)
import Data.List (transpose)
import Data.Map.Lazy (Map)
Expand All @@ -42,15 +40,12 @@ import Data.Text (Text, unpack)
import Data.Text qualified as T
import Data.Text.IO qualified as T
import Data.Tuple (swap)
import Data.Yaml (decodeFileEither)
import Data.Yaml.Aeson (prettyPrintParseException)
import Swarm.Doc.Pedagogy
import Swarm.Game.Display (displayChar)
import Swarm.Game.Entity (Entity, EntityMap (entitiesByName), entityDisplay, entityName, loadEntities)
import Swarm.Game.Entity qualified as E
import Swarm.Game.Failure qualified as F
import Swarm.Game.Failure (SystemFailure)
import Swarm.Game.Recipe (Recipe, loadRecipes, recipeInputs, recipeOutputs, recipeRequirements, recipeTime, recipeWeight)
import Swarm.Game.ResourceLoading (getDataFileNameSafe)
import Swarm.Game.Robot (Robot, equippedDevices, instantiateRobot, robotInventory)
import Swarm.Game.Scenario (Scenario, loadScenario, scenarioRobots)
import Swarm.Game.WorldGen (testWorld2Entites)
Expand All @@ -61,10 +56,10 @@ import Swarm.Language.Pretty (prettyText)
import Swarm.Language.Syntax (Const (..))
import Swarm.Language.Syntax qualified as Syntax
import Swarm.Language.Typecheck (inferConst)
import Swarm.Util (both, guardRight, listEnums, quote, simpleErrorHandle)
import Swarm.Util (both, listEnums, quote)
import Swarm.Util.Effect (simpleErrorHandle)
import Text.Dot (Dot, NodeId, (.->.))
import Text.Dot qualified as Dot
import Witch (from)

-- ============================================================================
-- MAIN ENTRYPOINT TO CLI DOCUMENTATION GENERATOR
Expand Down Expand Up @@ -123,19 +118,15 @@ generateDocs = \case
Just st -> case st of
Commands -> T.putStrLn commandsPage
Capabilities -> simpleErrorHandle $ do
entities <- ExceptT loadEntities
liftIO $ T.putStrLn $ capabilityPage address entities
entities <- loadEntities
sendIO $ T.putStrLn $ capabilityPage address entities
Entities -> simpleErrorHandle $ do
let loadEntityList fp = left (from . prettyPrintParseException) <$> decodeFileEither fp
let f = "entities.yaml"
let e2m = fmap eitherToMaybe . runExceptT
Just fileName <- liftIO $ e2m $ getDataFileNameSafe F.Entities f
entities <- liftIO (loadEntityList fileName) >>= guardRight "load entities"
liftIO $ T.putStrLn $ entitiesPage address entities
entities <- loadEntities
sendIO $ T.putStrLn $ entitiesPage address (Map.elems $ entitiesByName entities)
Recipes -> simpleErrorHandle $ do
entities <- ExceptT loadEntities
recipes <- withExceptT F.prettyFailure $ loadRecipes entities
liftIO $ T.putStrLn $ recipePage address recipes
entities <- loadEntities
recipes <- loadRecipes entities
sendIO $ T.putStrLn $ recipePage address recipes
TutorialCoverage -> renderTutorialProgression >>= putStrLn . T.unpack

-- ----------------------------------------------------------------------------
Expand Down Expand Up @@ -423,8 +414,8 @@ recipePage = recipeTable

generateRecipe :: IO String
generateRecipe = simpleErrorHandle $ do
entities <- ExceptT loadEntities
recipes <- withExceptT F.prettyFailure $ loadRecipes entities
entities <- loadEntities
recipes <- loadRecipes entities
classic <- classicScenario
return . Dot.showDot $ recipesToDot classic entities recipes

Expand Down Expand Up @@ -545,9 +536,9 @@ recipeLevels recipes start = levels
else go (n : ls) (Set.union n known)

-- | Get classic scenario to figure out starting entities.
classicScenario :: ExceptT Text IO Scenario
classicScenario :: (Has (Throw SystemFailure) sig m, Has (Lift IO) sig m) => m Scenario
classicScenario = do
entities <- loadEntities >>= guardRight "load entities"
entities <- loadEntities
fst <$> loadScenario "data/scenarios/classic.yaml" entities

startingHelper :: Scenario -> Robot
Expand Down
28 changes: 14 additions & 14 deletions src/Swarm/Doc/Pedagogy.hs
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,32 @@ module Swarm.Doc.Pedagogy (
TutorialInfo (..),
) where

import Control.Carrier.Accum.FixedStrict (evalAccum)
import Control.Lens (universe, view, (^.))
import Control.Monad (guard, when)
import Control.Monad.Except (ExceptT (..))
import Control.Monad.IO.Class (liftIO)
import Control.Monad (guard)
import Data.List (foldl', intercalate, sort, sortOn)
import Data.List.Extra (notNull, zipFrom)
import Data.List.Extra (zipFrom)
import Data.Map (Map)
import Data.Map qualified as M
import Data.Maybe (mapMaybe)
import Data.Sequence (Seq)
import Data.Set (Set)
import Data.Set qualified as S
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.IO qualified as T
import Swarm.Constant
import Swarm.Game.Entity (loadEntities)
import Swarm.Game.Failure (prettyFailure)
import Swarm.Game.Failure (SystemFailure)
import Swarm.Game.Scenario (Scenario, scenarioDescription, scenarioName, scenarioObjectives, scenarioSolution)
import Swarm.Game.Scenario.Objective (objectiveGoal)
import Swarm.Game.ScenarioInfo (ScenarioCollection, ScenarioInfoPair, flatten, loadScenariosWithWarnings, scenarioCollectionToList, scenarioPath)
import Swarm.Game.ScenarioInfo (ScenarioCollection, ScenarioInfoPair, flatten, loadScenarios, scenarioCollectionToList, scenarioPath)
import Swarm.Language.Module (Module (..))
import Swarm.Language.Pipeline (ProcessedTerm (..))
import Swarm.Language.Syntax
import Swarm.Language.Text.Markdown (findCode)
import Swarm.Language.Types (Polytype)
import Swarm.TUI.Controller (getTutorials)
import Swarm.Util (simpleErrorHandle)
import System.IO (hPutStrLn, stderr)
import Swarm.Util.Effect (simpleErrorHandle)

-- * Constants

Expand Down Expand Up @@ -159,11 +157,13 @@ generateIntroductionsSequence =
-- For unit tests, can instead access the scenarios via the GameState.
loadScenarioCollection :: IO ScenarioCollection
loadScenarioCollection = simpleErrorHandle $ do
entities <- ExceptT loadEntities
(failures, loadedScenarios) <- liftIO $ loadScenariosWithWarnings entities
when (notNull failures) . liftIO $
hPutStrLn stderr "Loading failures: " >> mapM_ (T.hPutStrLn stderr . prettyFailure) failures
return loadedScenarios
entities <- loadEntities

-- Note we ignore any warnings generated by 'loadScenarios' below,
-- using 'evalAccum'. Any warnings will be caught when loading all
-- the scenarios via the usual code path; we do not need to do
-- anything with them here while simply rendering pedagogy info.
evalAccum (mempty :: Seq SystemFailure) $ loadScenarios entities

renderUsagesMarkdown :: CoverageInfo -> Text
renderUsagesMarkdown (CoverageInfo (TutorialInfo (s, si) idx _sCmds dCmds) novelCmds) =
Expand Down
Loading

0 comments on commit 8aea6a2

Please sign in to comment.