Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add lazily evaluated versions of the conditional functions for applicatives and monads #313

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

JoelLefkowitz
Copy link

@JoelLefkowitz JoelLefkowitz commented Jan 31, 2025

ifM, whenM and unlessM are really helpful for handling monads expressively. However, they require both of the possible outputs to be constructed. This can be problematic when the Monad is Effect, as PureScript is strictly evaluated, since both the side effects will still need to be constructed, even if they are not launched. This is because they may be expensive to compute especially if they use the imported JS functions.

Here is an example where a lazily evaluated version ifM' would be helpful:

create :: User -> Effect String
update :: User -> Effect String
exists :: User -> Effect Boolean

main :: Effect Unit
main = do
  response <- ifM' exists update create user
  log response

This PR creates lazy versions of each of the conditional functions for Applicatives and Monads:

  • ifM'
  • unless'
  • unlessM'
  • when'
  • whenM'

This is following the same approach as maybe':

Similar to maybe but for use in cases where the default value may be
expensive to compute. As PureScript is not lazy, the standard maybe has
to evaluate the default value before returning the result, whereas here
the value is only computed when the Maybe is known to be Nothing.

maybe' :: forall a b. (Unit -> b) -> (a -> b) -> Maybe a -> b
maybe' g _ Nothing = g unit
maybe' _ f (Just a) = f a

Checklist:

  • Added the change to the changelog's "Unreleased" section with a reference to this PR (e.g. "- Made a change (#0000)")
  • Linked any existing issues or proposals that this pull request should close
  • Updated or added relevant documentation
  • Added a test for the contribution (if applicable)

@garyb
Copy link
Member

garyb commented Jan 31, 2025

This is problematic when the Monad is Effect, as PureScript is strictly evaluated, since both the side effects will still happen.

Unless the Effect has been implemented improperly via the FFI it won't be evaluated unconditionally by ifM, etc - Effects are essentially thunks, so side effects only occur on bind.

One potential argument for adding these would be if it's expensive to construct the Effect even without evaluating it, I'm not sure I've ran into that scenario exactly, but another possible solution there without adding new functions would be to do something like ?cond >>= if _ then ?eff1 else ?eff2.

@JoelLefkowitz
Copy link
Author

@garyb you're right the side effects are not evaluated and I had a bad foreign import. I've changed the PR content to focus on the issue of effects that are expensive to compute.

An example I've run into where the effects are expensive to construct is when using a JS logging framework:

foreign import log :: String -> Effect Unit

main :: Effect Unit
main = do
    ifM (...) (...) (log "...")

Another small advantage of the function is that it can be more ergonomic to write ifM' exists update create user
rather than ifM (exists user) (update user) (create user)

@JoelLefkowitz
Copy link
Author

@garyb could you please take another look at this?

Comment on lines 153 to 154
-- | Similar to `ifM` but for use in cases where one of the monadic actions may
-- | be expensive to compute. As PureScript is not lazy, the standard `ifM` has
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps "construct" rather than "compute" in this first sentence? And really I think that would suffice for the explanation, elaborating further on strict eval semantics only really applies to people who are newly coming to PS from Haskell. 😉

Having the code example is still good though 👍

@JoelLefkowitz
Copy link
Author

Thanks @garyb I've cut down the ifM' docstring and removed the superfluous $ uses

@garyb
Copy link
Member

garyb commented Feb 6, 2025

I just realised all the other comments refer to performing an action lazily, could you update those too? (Sorry)

Also there's an unused dependency that is causing the build failure just now. 😉

@JoelLefkowitz
Copy link
Author

Thanks @garyb no problem :)

I've reworded them from Perform a monadic action lazily unless a condition to Construct a monadic action unless etc. Is that what you had in mind?

@JoelLefkowitz
Copy link
Author

Hey @garyb could this please be merged?

@garyb
Copy link
Member

garyb commented Feb 22, 2025

Sorry sorry, I've been crazy busy lately. Perhaps "Construct and evaluate" since it sounds like now it's just constructing them but not doing anything with them?

@JoelLefkowitz
Copy link
Author

Thanks @garyb no worries :)

I've reworded it like this:

-- | Perform a monadic action when a condition is true, where the conditional
-- | value is also in a monadic context.
whenM :: forall m. Monad m => m Boolean -> m Unit -> m Unit
whenM mb m = ifM mb m $ pure unit

-- | Perform a monadic action when a condition is true, without constructing it
-- | otherwise, where the conditional value is also in a monadic context.
whenM' :: forall m a. Monad m => (a -> m Boolean) -> (a -> m Unit) -> a -> m Unit
whenM' mb m = ifM' mb m \_ -> pure unit

I think it makes it a lot clearer that they both will only evaluate the monadic action when the condition is true, but only whenM will always construct it.

@JoelLefkowitz
Copy link
Author

Hey @garyb so sorry to tag you again in this since you are busy with other things but I'd really like to complete this PR, do you have a moment to approve it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants