From 5a6c49d4ff6dfe52e865767bac36091c1c191e26 Mon Sep 17 00:00:00 2001 From: David Feuer <David.Feuer@gmail.com> Date: Thu, 28 Oct 2021 01:28:09 -0400 Subject: [PATCH] Speed up stimes in instance Semigroup Endo As discussed at https://github.com/haskell/core-libraries-committee/issues/4 --- libraries/base/src/Data/Semigroup/Internal.hs | 43 ++++++++++++++++++- libraries/base/tests/all.T | 1 + libraries/base/tests/stimesEndo.hs | 9 ++++ libraries/base/tests/stimesEndo.stdout | 1 + 4 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 libraries/base/tests/stimesEndo.hs create mode 100644 libraries/base/tests/stimesEndo.stdout diff --git a/libraries/base/src/Data/Semigroup/Internal.hs b/libraries/base/src/Data/Semigroup/Internal.hs index 7da13f0443e9..d5443a01565f 100644 --- a/libraries/base/src/Data/Semigroup/Internal.hs +++ b/libraries/base/src/Data/Semigroup/Internal.hs @@ -1,4 +1,5 @@ {-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE PolyKinds #-} @@ -134,8 +135,46 @@ newtype Endo a = Endo { appEndo :: a -> a } -- | @since 4.9.0.0 instance Semigroup (Endo a) where - (<>) = coerce ((.) :: (a -> a) -> (a -> a) -> (a -> a)) - stimes = stimesMonoid + (<>) = coerce ((.) :: (a -> a) -> (a -> a) -> (a -> a)) + + -- See Note [stimes Endo] + stimes !n0 (Endo e) = Endo (\a0 -> + -- We check separately for 0 and 1 per + -- https://github.com/haskell/core-libraries-committee/issues/4#issuecomment-955605592 + -- We are explicitly strict in the number so strictness is calculated + -- correctly even without specialization. + case n0 of + _ | n0 < 0 -> stimesEndoError + 0 -> a0 + 1 -> e a0 + _ -> go n0 a0) + where + go !0 a = a + go n a = e (go (n - 1) a) + +{-# NOINLINE stimesEndoError #-} +-- There's no reason to put this gunk in the unfolding. +stimesEndoError :: a +stimesEndoError = errorWithoutStackTrace "stimes (for Endo): negative multiplier" + +-- Note [stimes Endo] +-- ~~~~~~~~~~~~~~~~~~ +-- +-- We used to use +-- +-- stimes = stimesMonoid +-- +-- But this is pretty bad! The function it produces is represented in memory as +-- a balanced tree of compositions. To actually *apply* that function, it's +-- necessary to walk the tree. It's much better to just construct a function +-- that counts out applications. +-- +-- Why do we break open the `Endo` construction rather than just using `mempty` +-- and `<>`? We want GHC to infer that `stimes` has an arity of 3. Currently, +-- it does so by default, but there has been some talk in the past of turning +-- on -fpedantic-bottoms, which would drop the arity to 2. Indeed, if we were +-- really careless, we could theoretically get GHC to build a *list* of +-- compositions, which would be awful. -- | @since 2.01 instance Monoid (Endo a) where diff --git a/libraries/base/tests/all.T b/libraries/base/tests/all.T index 3b92c9eb4815..d10182cb484e 100644 --- a/libraries/base/tests/all.T +++ b/libraries/base/tests/all.T @@ -317,3 +317,4 @@ test('T23697', [ when(opsys('mingw32'), skip) # header not found , when(opsys('darwin'), skip) # permission denied ], makefile_test, ['T23697']) +test('stimesEndo', normal, compile_and_run, ['']) diff --git a/libraries/base/tests/stimesEndo.hs b/libraries/base/tests/stimesEndo.hs new file mode 100644 index 000000000000..3f94e2f0a19e --- /dev/null +++ b/libraries/base/tests/stimesEndo.hs @@ -0,0 +1,9 @@ +module Main where + +import Data.Semigroup + +adder :: Int -> Endo Int +adder n = stimes n (Endo (+ 1)) + +main :: IO () +main = print $ map (\n -> appEndo (adder n) 0) [0 .. 5] diff --git a/libraries/base/tests/stimesEndo.stdout b/libraries/base/tests/stimesEndo.stdout new file mode 100644 index 000000000000..1b69d7981935 --- /dev/null +++ b/libraries/base/tests/stimesEndo.stdout @@ -0,0 +1 @@ +[0,1,2,3,4,5] -- GitLab