PureScript by Example - Exercise Clarification (Monad Transformer Stacks)

At the end of Monad Transformer Stacks section of the Monadic Adventures chapter in PureScript by Example there’s an exercise that builds on a previous one, however, I don’t get how a possible solution should work. Here’s the exercise’s text.

  1. (Difficult) Use the ReaderT and WriterT monad transformers to reimplement the document printing library which we wrote earlier using the Reader monad. Instead of using line to emit strings and cat to concatenate strings, use the Array String monoid with the WriterT monad transformer, and tell to append a line to the result.

For me this tells that I should rework the previous document printing solution (I have one similar to this) in a way that I don’t use the line and cat functions and somehow I should use the tell function instead of these, but I’m not sure how is this possible.

An example of how using this new solution might go would be useful, along the lines of this example from a previous section.

 render
    $ cat
        [ line "Here is some indented text:"
        , indent
            $ cat
                [ line "I am indented"
                , line "So am I"
                , indent $ line "I am even more indented"
                ]
        ]

Any insight is gladly appreciated.

Mentioning @paf31 in hope of first-hand insight.

I think the goal would be to write something like:

render do
  line "Here is some indented text:"
  indent do
    line "I am indented"
    line "So am I"
    indent do
      line "I am even more intented"

Basically you don’t see cat anymore explicitly. I kept line, but it’s tell <<< Array.singleton. Otherwise you have to write

render do
  tell [ "Here is some indented text:" ]
  indent do
    tell [ "I am indented" ]
    tell [ "So am I" ]
    indent do
      tell [ "I am even more intented" ]

Hmm, I came to the same conclusion, but what bugs me is that if we omit the line function or if it’s simply tell <<< Array.singleton then how each line will get indented? I couldn’t figure this part out … yet.

My guess is that you did (did you?) and if that’s the case it helps me to see that an example like yours above could work.

I would look at the MonadWriter interface. tell is part of MonadTell, which isn’t enough to complete this task. However MonadWriter has operations for observing and manipulating the accumulator.

https://pursuit.purescript.org/packages/purescript-transformers/4.2.0/docs/Control.Monad.Writer.Class#t:MonadWriter

Ah, thanks for the insight. The book doesn’t mention this, I assumed what was covered by the section at hand would be enough.

Edit:

I was able to use censor to achieve what was needed, although my interpretation of Level was rather something like a tab-shift width. I didn’t need to use local this way tho’.

import Prelude
import Control.Monad.Reader (ReaderT, ask, runReaderT)
import Control.Monad.Writer (WriterT, censor, execWriterT, tell)
import Data.Array (replicate)
import Data.Identity (Identity)
import Data.Newtype (unwrap)
import Data.String (joinWith)
import Data.String.CodeUnits (fromCharArray)

type Level
  = Int

type Doc
  = WriterT (Array String) (ReaderT Level Identity) Unit

indent :: Doc -> Doc
indent d = do
  level <- ask
  censor (map (append (fromCharArray (replicate level ' ')))) d

render :: Doc -> String
render d = joinWith "\n" (unwrap (runReaderT (execWriterT d) 2))

I think that in the original exercise Level in the Reader Monad was used to indicate how deep current line is, not how much each line should be indented.
I’m sorry for reviving an old thread, but I was stuck with this task and your advise helped me a lot, so maybe someone will find this useful too :slight_smile:

Here’s my approach:

import Prelude
import Control.Monad.Reader (ReaderT, ask, local, runReaderT)
import Control.Monad.Writer (WriterT, execWriterT, tell)
import Control.Monad.Trans.Class (lift)
import Data.String (joinWith)
import Data.Identity (Identity)
import Data.Newtype (unwrap)
import Data.Monoid (power)

type Level = Int
type Doc = (WriterT (Array String) (ReaderT Level Identity)) Unit
  
line :: String -> Doc
line s = do
  level <- lift $ ask
  tell [ (power "  " level) <> s ]
  pure unit

indent :: Doc -> Doc
indent = local ((+) 1)

render :: Doc -> String
render doc = joinWith "\n" $ unwrap $ runReaderT (execWriterT doc) 0

and the usage:

suite "indents" do
  let 
    expectedText = 
      "Here is some indented text:\n\
      \  I am indented\n\
      \  So am I\n\
      \    I am even more indented"
  test "should render with indentations" do
      Assert.equal expectedText
      $ render $ do
          line "Here is some indented text:"
          indent $ do
            line "I am indented"
            line "So am I"
            indent $ do
              line "I am even more indented"

I’d be happy to incorporate this test and solution into the book if you make a PR.
This is one of the four remaining chapters without tests.

1 Like

PR is here:

2 Likes