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

Church-encoded Empty & Error carriers #203

Merged
merged 198 commits into from Mar 15, 2020
Merged

Church-encoded Empty & Error carriers #203

merged 198 commits into from Mar 15, 2020

Conversation

robrix
Copy link
Collaborator

@robrix robrix commented Aug 29, 2019

This PR defines carriers for Empty & Error implemented as Church-encoded monad transformers à la Codensity.

  • Benchmark against the regular Error carrier.
  • Tests.
  • Changelog.
  • Link from the Error effect.

@robrix robrix changed the title Church-encoded Error carrier Church-encoded Empty & Error carriers Mar 15, 2020
Comment on lines +14 to +25
[ bench "Either" $ whnf (errorLoop :: Int -> Either Int ()) n
, bgroup "Identity"
[ bench "Church.ErrorC" $ whnf (run . Church.runError @Int (pure . Left) (pure . Right) . errorLoop) n
, bench "Either.ErrorC" $ whnf (run . Either.runError @Int . errorLoop) n
, bench "ExceptT" $ whnf (run . Except.runExceptT @Int . errorLoop) n
]
, bgroup "IO"
[ bench "Church.ErrorC IO" $ whnfAppIO (Church.runError @Int (pure . Left) (pure . Right) . errorLoop) n
, bench "Either.ErrorC IO" $ whnfAppIO (Either.runError @Int . errorLoop) n
, bench "ExceptT IO" $ whnfAppIO (Except.runExceptT @Int . errorLoop) n
]
]
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Benchmark results (-O2)
benchmarked Error/Either
time                 21.15 ms   (20.36 ms .. 21.63 ms)
                     0.996 R²   (0.992 R² .. 1.000 R²)
mean                 22.92 ms   (22.33 ms .. 24.41 ms)
std dev              1.845 ms   (637.7 μs .. 3.254 ms)
variance introduced by outliers: 34% (moderately inflated)

benchmarked Error/Identity/Church.ErrorC
time 47.47 ms (46.94 ms .. 47.98 ms)
1.000 R² (0.999 R² .. 1.000 R²)
mean 48.72 ms (48.17 ms .. 49.94 ms)
std dev 1.672 ms (858.0 μs .. 2.869 ms)

benchmarked Error/Identity/Either.ErrorC
time 33.05 ms (32.78 ms .. 33.24 ms)
1.000 R² (0.999 R² .. 1.000 R²)
mean 34.26 ms (33.83 ms .. 35.11 ms)
std dev 1.253 ms (722.1 μs .. 1.877 ms)
variance introduced by outliers: 12% (moderately inflated)

benchmarked Error/Identity/ExceptT
time 33.26 ms (32.81 ms .. 33.78 ms)
0.999 R² (0.999 R² .. 1.000 R²)
mean 34.74 ms (34.17 ms .. 36.11 ms)
std dev 1.778 ms (668.9 μs .. 3.034 ms)
variance introduced by outliers: 13% (moderately inflated)

benchmarked Error/IO/Church.ErrorC IO
time 45.44 ms (44.99 ms .. 46.05 ms)
1.000 R² (0.999 R² .. 1.000 R²)
mean 46.82 ms (46.32 ms .. 47.98 ms)
std dev 1.433 ms (704.4 μs .. 2.344 ms)

benchmarked Error/IO/Either.ErrorC IO
time 47.49 ms (46.70 ms .. 48.86 ms)
0.998 R² (0.994 R² .. 1.000 R²)
mean 49.15 ms (48.31 ms .. 51.28 ms)
std dev 2.210 ms (1.225 ms .. 3.408 ms)
variance introduced by outliers: 14% (moderately inflated)

benchmarked Error/IO/ExceptT IO
time 45.32 ms (44.81 ms .. 45.74 ms)
1.000 R² (0.999 R² .. 1.000 R²)
mean 47.76 ms (46.79 ms .. 49.27 ms)
std dev 2.341 ms (1.314 ms .. 3.400 ms)
variance introduced by outliers: 14% (moderately inflated)

As with all benchmarks, these can be assumed to be lies, but there are a couple of interesting takeaways nonetheless:

  1. You can’t really beat Either because there’s just less for the simplifier to work with. So it’s not necessarily that Either is faster; it’s probably more so that there’s just less code because it’s not a monad transformer. (I’d like to confirm this with a non-transformer Church-encoded Either, but that’s out of scope for this PR.)

  2. ghc can “see through” Identity more easily with Either.ErrorC/ExceptT than with Church.ErrorC.

  3. Church.ErrorC does a little bit better composed onto IO than does Either.ErrorC/ExceptT; this says to me that modulo cases where the simplifier can “see through” some of the types (as with Identity), Church.ErrorC is slightly faster on average.

  4. -O vs. -O2 made a small but noticeable difference.

-- 'runEmpty' ('pure' a) = 'Just' a
-- 'runEmpty' ('pure' a) = 'pure' ('Just' a)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Whoops!

Comment on lines +34 to +42
-- @
-- 'runError' j k ('pure' a) = k a
-- @
-- @
-- 'runError' j k ('throwError' e) = j e
-- @
-- @
-- 'runError' j k ('throwError' e \`'catchError'\` 'pure') = k e
-- @
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I’m really happy with how these properties capture the type’s behaviour.

-- 'runError' ('throwError' e `catchError` 'pure') = 'pure' ('Right' e)
-- 'runError' ('throwError' e \`'catchError'\` 'pure') = 'pure' ('Right' e)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I’ve been meaning to fix this for a while.

Comment on lines +38 to +40
-- @
-- runNonDet fork leaf nil ('pure' a '<|>' 'empty') = leaf a \`fork\` nil
-- @
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I love how this single property completely encapsulates NonDetC’s behaviour w.r.t. its three continuations and the operators they interpret..

Comment on lines +56 to +58
liftA2 f (ReaderC a) (ReaderC b) = ReaderC $ \ r ->
liftA2 f (a r) (b r)
{-# INLINE liftA2 #-}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I’ve implemented liftA2 for a few carriers.

Comment on lines -47 to +48
runState :: forall s m a . Applicative m => s -> StateC s m a -> m (s, a)
runState s (StateC m) = m (\ a s -> pure (s, a)) s
runState :: forall s m a b . (s -> a -> m b) -> s -> StateC s m a -> m b
runState f s (StateC m) = m f s
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It’s not super important to me that different carriers for the same effect provide exactly the same interface w.r.t. their handlers, and this definition of runState for the Church-encoded StateC is much more flexible.

@robrix robrix marked this pull request as ready for review March 15, 2020 16:27
@robrix robrix added this to the 1.1.0.0 milestone Mar 15, 2020
@robrix robrix merged commit db99b2a into master Mar 15, 2020
@robrix robrix deleted the cps-error-carrier branch March 15, 2020 17:13
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.

None yet

1 participant