bower install purescript-run-console-experiment
Module documentation is published on Pursuit.
This is only an experiment because I don't know if I can maintain it well enough and I don't feel like squatting on the name purescript-run-console.
It has feature parity with purescript-console.
If that's your concern, be at ease.
If you want to maintain this, please do, but maintain it under purescript-run-console!
Maybe this is could go in purescript-contrib or directly in purescript?
Much like purescript-console.
You write your code using functions, and at the end you have to "run" it.
If you had something like this:
module Main where
import Prelude
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE, log)
main :: forall e. Eff (console :: CONSOLE | e) Unit
main = do
log "Hello sailor!"You would change it to this:
module Main where
import Prelude
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE)
import Run (runBaseEff)
import Run.Console (log, runConsole)
main :: forall e. Eff (console :: CONSOLE | e) Unit
main = runBaseEff $ runConsole do
log "Hello sailor!"You may be asking yourself, "Self, why would I use this?" There's probably a ton of reasons someone could give you for why you would. Here are a couple of reasons.
Sometimes you write logging code for development, that you don't want to show up in production. If you use this library, the change is minimal.
Say you have some code like this:
module Main where
import Prelude
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE)
import Run (runBaseEff)
import Run.Console (log, runConsole)
main :: forall e. Eff (console :: CONSOLE | e) Unit
main = runBaseEff $ runConsole do
log "Hello sailor!"
-- do a bunch of stuff
log "Goodbye sailor!"To turn off logging, you change the function runConsole to runNoConsole.
That's it!
module Main where
import Prelude
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE)
import Run (runBaseEff)
import Run.Console (log, runNoConsole)
main :: forall e. Eff (console :: CONSOLE | e) Unit
main = runBaseEff $ runNoConsole do
log "Hello sailor!"
-- do a bunch of stuff
log "Goodbye sailor!"If you wanted to be more specific,
you could change the line to pure $ extract $ runNoConsole and remove the Control.Monad.Eff.Console.CONSOLE effect:
module Main where
import Prelude
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE)
import Run (extract)
import Run.Console (log, runNoConsole)
main :: forall e. Eff e Unit
main = pure $ extract $ runNoConsole do
log "Hello sailor!"
-- do a bunch of stuff
log "Goodbye sailor!"Sometimes you want to ensure that what is being logged is correct. You can take the console messages you write, and run them with a different interpreter that accumulates all logs.
Say you had some code like this:
module Main where
import Prelude
import Run.Console (CONSOLE, log)
sailorTime :: forall r. Run (console :: CONSOLE | r) Unit
sailorTime = do
log "Hello sailor!"
log "Goodbye sailor!"In order to test that the two messages are sent properly, we can run an accumulating interpreter:
module Main where
import Prelude
import Data.List.Types (List)
import Run (extract)
import Run.Console (CONSOLE, log, runAccumulate)
sailorTime :: forall r. Run (console :: CONSOLE | r) Unit
sailorTime = do
log "Hello sailor!"
log "Goodbye sailor!"
logs :: List String
logs = extract $ runAccumulate sailorTimeThis code does not reach out to the actual console and write anything; it's completely pure!
All it does is evaluate sailorTime as though it were any other value in PureScript.
logs contains both of those messages and you could write a test that verifies
module Main where
import Prelude
import Data.List.Types (List(..), (:))
import Run (extract)
import Run.Console (CONSOLE, log, runAccumulate)
import Test.Spec (it)
import Test.Spec.Assertions (shouldEqual)
import Test.Spec.Reporter.Spec (specReporter)
import Test.Spec.Runner as Test.Spec.Runner
sailorTime :: forall r. Run (console :: CONSOLE | r) Unit
sailorTime = do
log "Hello sailor!"
log "Goodbye sailor!"
logs :: List String
logs = extract $ runAccumulate sailorTime
main = do
Test.Spec.Runner.run [specReporter] do
it "logs are correct" do
logs `shouldEqual` ("Hello sailor!" : "Goodbye sailor!" : Nil)Let's say that you don't like the idea of turning off all messages in production.
Instead, you'd like to still show the error messages, but ignore info, log, and warning.
Additionally, you want to prefix the error messages with a bright red label.
You can do that by supplying a different interpreter to run.
Let's write that interpreter!
The first thing to know is that Console a is just a Plain Old PureScript Type.
It has a case for each level of message it can handle: Error, Info, Log, and Warn.
So we can do things like case on it and decide what to do in each case.
In every case except the Error case, we want to ignore the supplied message.
The general idea of what we want is a function like:
module Main where
import Prelude
import Ansi.Codes (Color(..))
import Ansi.Output (foreground, withGraphics)
import Control.Monad.Eff.Console as Eff
import Run.Console (Console(..))
go :: forall a e. Console a -> Eff (console :: Eff.CONSOLE | e) a
go = case _ of
Error s x -> Eff.log (withGraphics (foreground BrightRed) "[ERROR] " <> s) $> x
Info _ x -> pure x
Log _ x -> pure x
Warn _ x -> pure xWe pull in purescript-ansi for the coloring and do the following for the Error case:
- construct the
BrightRedlabel - append the string we get to this label
- log the string to the actual console
- replace the
UnitfromEff (console :: Eff.CONSOLE | e) Unitwitha
As it turns out, this function is a NaturalTransformation from Console to Eff (console :: Eff.CONSOLE | e).
You might see NaturalTransformation as an alias (~>) often.
Notice that we don't touch whatever the a is in Console;
we just pass it right along to Eff (console :: Eff.CONSOLE | e).
So we have something like Canvas ~> Eff (console :: Eff.CONSOLE | e).
We can rewrite the signature to reflect that fact.
module Main where
import Prelude
import Ansi.Codes (Color(..))
import Ansi.Output (foreground, withGraphics)
import Control.Monad.Eff.Console as Eff
import Run.Console (Console(..))
go :: forall e. Console ~> Eff (console :: Eff.CONSOLE | e)
go = case _ of
Error s x -> Eff.log (withGraphics (foreground BrightRed) "[ERROR] " <> s) $> x
Info _ x -> pure x
Log _ x -> pure x
Warn _ x -> pure xNow that we have our function that does what we want,
we can use it with runEff to build an interpreter!
module Main where
import Prelude
import Ansi.Codes (Color(..))
import Ansi.Output (foreground, withGraphics)
import Control.Monad.Eff.Console as Eff
import Run (EFF, Run)
import Run.Console (CONSOLE, Console(..), runEff)
go :: forall e. Console ~> Eff (console :: Eff.CONSOLE | e)
go = case _ of
Error s x -> Eff.log (withGraphics (foreground BrightRed) "[ERROR] " <> s) $> x
Info _ x -> pure x
Log _ x -> pure x
Warn _ x -> pure x
runProduction
:: forall a e r
. Run (console :: CONSOLE, eff :: EFF (console :: Eff.CONSOLE | e) | r) a
-> Run (eff :: EFF (console :: Eff.CONSOLE | e) | r) a
runProduction = runEff goAside: Notice that to write an interpreter that interprets into EFF e,
we have to assume the given Run r a already has EFF e as part of its row r.
This idea may seem confusing.
But, consider what would happen if we did not specify the given Run r a
had EFF e as part of its row r.
We would be saying that by interpreting with runProduction,
we would be "introducing" the EFF e into the row r.
If we had a similar interpreter, it too would "introduce" EFF e into the row r.
We would end up with duplicate labels!
Rather than opening that can of worms,
purescript-run forces us to say that
the given Run r a already has EFF e as part of r.
In fact, we can even inline the function if we'd like:
module Main where
import Prelude
import Ansi.Codes (Color(..))
import Ansi.Output (foreground, withGraphics)
import Control.Monad.Eff.Console as Eff
import Run (EFF, Run)
import Run.Console (CONSOLE, Console(..), runEff)
runProduction
:: forall a e r
. Run (console :: CONSOLE, eff :: EFF (console :: Eff.CONSOLE | e) | r) a
-> Run (eff :: EFF (console :: Eff.CONSOLE | e) | r) a
runProduction = runEff case _ of
Error s x -> Eff.log (withGraphics (foreground BrightRed) "[ERROR] " <> s) $> x
Info _ x -> pure x
Log _ x -> pure x
Warn _ x -> pure xThat's it! Now, we can run this interpreter just like any other. But it will only print the error messages.
module Main where
import Prelude
import Ansi.Codes (Color(..))
import Ansi.Output (foreground, withGraphics)
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console as Eff
import Run (EFF, Run, runBaseEff)
import Run.Console (CONSOLE, Console(..), error, log, runEff)
runProduction
:: forall a e r
. Run (console :: CONSOLE, eff :: EFF (console :: Eff.CONSOLE | e) | r) a
-> Run (eff :: EFF (console :: Eff.CONSOLE | e) | r) a
runProduction = runEff case _ of
Error s x -> x <$ Eff.log (withGraphics (foreground BrightRed) "[ERROR] " <> s)
Info _ x -> pure x
Log _ x -> pure x
Warn _ x -> pure x
main :: forall e. Eff (console :: Eff.CONSOLE | e) Unit
main = runBaseEff $ runProduction do
log "Hello sailor!"
-- do a bunch of stuff
error "Oh no sailor!"
log "Goodbye sailor!"The only message we'll see is "[ERROR] Oh no sailor!" where the [ERROR] is bright red.
All of the rest, we ignore.
This idea can be extended to most anything you need. You could format log messages in a different format, apply filtering on certain messages, add timestamps.