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 sailorTime
This 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 x
We pull in purescript-ansi
for the coloring and do the following for the Error
case:
- construct the
BrightRed
label - append the string we get to this label
- log the string to the actual console
- replace the
Unit
fromEff (console :: Eff.CONSOLE | e) Unit
witha
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 x
Now 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 go
Aside: 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 x
That'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.