Skip to content

Strictness testing of transformers #123

Description

@L0neGamer

It would be good to assert the strictness properties of various transformers.

The transformers to test would be StateT.{Lazy,Strict}, WriterT.{Lazy,Strict,CPS}, and RWST.{Lazy,Strict,CPS}.

Tests would include setting values to undefined to see when an exception is thrown (if at all).

Different base monads should be used such as Identity and IO to test strictness with regards to base monad.

Tests should be of varying complexity, from the basic (enter transformer, bind value, leave transformer), to complex (enter transformer, perform various operations, possibly include other transformers or monads in the stack).

We don't currently use a testing framework like tasty (@sjshuck has found that they don't compile for some reason), so this will likely have to be a bit basic. If you do get something working with compilation/CI, all the better!

This should cover the tests I was worried about for #122 (review).

StrictCheck may be useful but we may not be able to use it.

Activity

  1. sjshuck commented on Jun 10, 2026

    @sjshuck
    Collaborator
  2. ynishiza commented on Jun 12, 2026

    @ynishiza
    Contributor

    Hello! I've been looking for an opportunity to contribute to open source and came across this in Haskell weekly.
    Is this something I can try my hand on? I can also look into setting up a testing framework (e.g. tasty + hspec).

  3. L0neGamer commented on Jun 12, 2026

    @L0neGamer
    CollaboratorAuthor

    Yeah go for it!

    We had some issues with trying to use a test framework so don't try too hard if you get esoteric errors when using them.

  4. ynishiza commented on Jun 12, 2026

    @ynishiza
    Contributor

    @L0neGamer Great, I'll get started then. Thank you. I think I understand what needs to be tested from the context you provided above, but do you have any preference about how the tests are written? e.g. similar tests in some other repo. I apologize in advance if I ask silly/obvious things, as this is my first time.

  5. sjshuck commented on Jun 12, 2026

    @sjshuck
    Collaborator

    @ynishiza If you're unsure, just look at the test that already exists and copy that pattern. We will eventually reformat all of it if and when we decide to, and it'll be easier if they're all from the same format.

  6. ynishiza commented on Jun 14, 2026

    @ynishiza
    Contributor

    Hello @L0neGamer @sjshuck
    I have a preliminary PR #125 with basic test package setup that I think allows using tasty (+ other test tools).
    Also have a few WriterT tests with: ynishiza#2
    Sample CI run https://github.com/ynishiza/transformers/actions/runs/27491953139

    Would love to know if this seems like the right direction + any other comments. Thanks!

  7. ynishiza commented on Jun 15, 2026

    @ynishiza
    Contributor

    @L0neGamer One question: is this blocking anything? I see that the constraint weakening where this came up have already been implemented.
    I should have asked this first but is there a time you would like to have this done by?
    The reason I ask is because I have work during the day so might progress will slow down...

  8. L0neGamer commented on Jun 15, 2026

    @L0neGamer
    CollaboratorAuthor

    No time limit at all! If there were I'd be frantically working at it.

  9. ynishiza commented on Jun 15, 2026

    @ynishiza
    Contributor

    Haha, got it. One pressure off my back as well. This is also serving as a good review of my understanding of strictness and WHNF.. some are a bit tricky.

  10. ynishiza commented on Jun 27, 2026

    @ynishiza
    Contributor

    Hello @L0neGamer @sjshuck , just wanted to let you know that I have a PR for the Writer tests #127. Please take a look when you have a chance!
    I wasn't able to edit the reviewers for this PR.. perhaps some security setting?

  11. ynishiza commented on Aug 9, 2026

    @ynishiza
    Contributor

    Hello @L0neGamer @sjshuck @Bodigrim

    I may have found a breaking change in the state transformer due to #122

    The weakened implementation of the new lazy evalStateT is:

    evalStateT :: Functor m => StateT s m a -> s -> m a
    evalStateT m s = (\(~(a, _)) -> a) <$> runStateT m s

    The issue is that now, if I understand it correctly, this ~ is rather pointless. This is effectively the same as fst, which is strict regardless of the ~. So evalStateT is actually now the same for both lazy and strict States.

    In the original implement with a monad bind, the lazy pattern match does make a difference:

    evalStateT :: (Monad m) => StateT s m a -> s -> m a
    evalStateT m s = do
    ~(a, _) <- runStateT m s
    return a

    This lazy evalStateT will always return a monad even if the tuple is undefined, whereas the strict version may not.

    For example:

    ghci> import qualified Control.Monad.Trans.State.Strict as S
    ghci> import qualified Control.Monad.Trans.State.Lazy as L
    ghci> (S.evalStateT (S.StateT (const $ return undefined )) () :: Maybe ()) `seq` ()       -- Fails before weakening, OK after weakening
    ghci> (L.evalStateT (L.StateT (const $ return undefined )) () :: Maybe ()) `seq` ()       -- OK in both cases
    

    I think I'll have to tweak the writer tests again in #127 as well, as it probably does not catch breakages at this level.

  12. L0neGamer commented on Aug 9, 2026

    @L0neGamer
    CollaboratorAuthor

    I think that this is an acceptable weakening but I would appreciate the commentary of others too.

  13. ynishiza commented on Aug 9, 2026

    @ynishiza
    Contributor

    I realize I'm not the one who's comment is mainly sought for here, so just my humble observation take.

    I'm guessing in the end it will come down to what are the canonical use cases of these lazy/strict transformers...
    I can't say off the top of my head, but the documentation has this example.

    https://hackage-content.haskell.org/package/transformers-0.6.3.0/docs/Control-Monad-Trans-State-Lazy.html

    Image

    This still works in the weakened version since it mainly depends on the laziness weakness in the monad bind.

    ghci> take 3 $ L.evalState (sequence $ repeat $ do { n <- L.get; L.put (n*2); return n }) 1
    [1,2,4]
    

    Questions related to what are the assumed use cases has also come up during the review of #127 so I'm thinking of mentioning them in the documentation of the tests.

  14. Bodigrim commented on Aug 13, 2026

    @Bodigrim
    Contributor

    Thanks for analysis @ynishiza, much appreciated.

    I think it's fine and probably even better. The strictness of a transformer relates to the strictness of the bind (>>=) operation, because this is what matters for pathological accumulation of thunks. The strictness of runStateT / evalStateT / execStateT is rather irrelevant in the grand scheme of things: there is no unbounded chain of them. That's why I believe that changing evalStateT / execStateT in this regard is fine.

    Why is it even better? Because now runStateT / evalStateT / execStateT are all aligned in their strictness. Note that you always had

    > import qualified Control.Monad.Trans.State.Strict as S
    > (S.runStateT (S.StateT (const $ return undefined )) () :: Maybe ((), ())) `seq` ()
    ()
    > import qualified Control.Monad.Trans.State.Lazy as L
    > (L.runStateT (L.StateT (const $ return undefined )) () :: Maybe ((), ())) `seq` ()
    ()

    That's because runStateT just unwraps a newtype and does not force any evaluation. And now, as noticed above, the same holds for evalStateT / execStateT.

  15. ynishiza commented on Aug 22, 2026

    @ynishiza
    Contributor

    Thank you @Bodigrim. That makes sense to me!

    I suppose in a sense, a potential breakage is also suggested a little in the weakening itself with the context now being Functor instead of Monad since Functors don't provide control over the outer constructor.
    I was mainly concerned that this could break, for example, any error handling with a lazy data type that depended on the outer constructor bottoming like the Maybe example above, but hopefully that kind of code is rare and it sounds like a bad idea anyway...

    If we do accept this change, perhaps it'd be better to remove the ~ here for clarity, since it has no effect on the strictness of this lambda?

    evalStateT :: Functor m => StateT s m a -> s -> m a
    evalStateT m s = (\(~(a, _)) -> a) <$> runStateT m s

  16. Bodigrim commented on Aug 23, 2026

    @Bodigrim
    Contributor

    Sorry, I don't have capacity for a deep dive at the moment. Could you compare Core (-ddump-simpl -dsuppress-all -dno-suppress-type-signatures) for evalState? Is it indeed the same with and without ~? If yes, it can be safely removed.

  17. ynishiza commented on Aug 25, 2026

    @ynishiza
    Contributor

    @Bodigrim happy to look into it this week 🙂

  18. ynishiza commented on Aug 26, 2026

    @ynishiza
    Contributor

    Before:

    Image

    After. The variable is changed to b to be 100% sure I'm looking at the Core of the modified version, since there should be no other observable difference.

    Image

    Core diff of evalStateT and execStateT. Left is before with ~, right is without.

    Image

    Looks like the same expressions.

    For reference, the monad implementation before the weakening, with ~ on the left and without on the right. Here the evaluation is lazy.

    Image
  19. Bodigrim commented on Aug 26, 2026

    @Bodigrim
    Contributor

    Yep, that's identical. We can indeed drop ~, thanks for the analysis.

  20. ynishiza commented on Aug 29, 2026

    @ynishiza
    Contributor

    Create PR #129

  21. ynishiza commented on Oct 3, 2026

    @ynishiza
    Contributor

    Sorry for the apparent inactivity for the past 2 weeks..
    The current state of the state transformer tests can be seen here: ynishiza#4
    I still need to iron out the details and add documentation but please feel free to comment if you notice anything.
    Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions