Since a Monoid is closed (a -> a -> a), How can we get a second type 'b' along the way ? I have the impression foldr is too permissive, in the sense that I could use a function for folding that it's not closed. You'll notice as well that fold and foldMap only have 'a'.
Below a snippet of the Foldable typeclass :
class Foldable t where
fold :: Monoid m => t m -> m
foldMap :: Monoid m => (a -> m) -> t a -> m
foldr :: (a -> b -> b) -> b -> t a -> b
e.g :
foldr (+) 0 [1..5] // ok (+) is a monoid
foldr (++) "" ["ab", " cd"] // ok (++) is a monoid for String
foldr (:) [] [1,2,3] // (:) :: a -> [a] -> [a] is not a monoid...
I was thinking a Foldable should/could only fold with a monoid, is that statement wrong ? (e.g : In my head it was like reduce is using a commutative monoid and fold a simple monoid.. (see Difference between reduce and foldLeft/fold in functional programming (particularly Scala and Scala APIs)?))
Looking at the Foldable
class definition you quoted, the type of foldr
just with its arguments re-ordered a bit,
foldr' :: Foldable t => (a -> (b -> b)) -> t a -> (b -> b) ,
actually unifies with (becomes the same as) the type of
foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m ,
provided b -> b
is a monoid, (b -> b) ~ Monoid m => m
i.e. Monoid (b -> b)
, the type of so-called endofunctions (i.e. functions from a type going to the same type), which are associatively combined with functional composition just as you expected.
Indeed they do form a monoid.
What does that mean, endofunctions forming a monoid under functional composition ((f . g) x = f (g x)
) ? Simply, that any two endofunctions can be composed, with functional composition being associative ( (f . g) . h == f . (g . h)
-- you can check this out yourself by using its definition). Also it means the existence of the special function id
such that id . f == f == f . id
; indeed id x = x
fits the bill. That's all, believe it or not.
Indeed (:) :: a -> [a] -> [a]
(read: (:)
has type a -> [a] -> [a]
), which is a kind of a -> b -> b
; so, with one :: Int ; one = 1
we have (one :) :: [Int] -> [Int]
which is a kind of b -> b
as well.
Also (one +) :: Int -> Int
, which is an even more specialized kind of b -> b
, but still.
As a technicality, a newtype
must be used to actually give this type its Monoid instance. This basically means, you can treat Endo
/ appEndo
as syntactic noise, when reading the code.
So foldr'
is indeed foldMap
, up to some newtype tagging (with Endo
) and untagging (with appEndo
) : appEndo (Endo f) == f
, that's all:
Sum 1 <> Sum 2 = Sum (1 + 2) -- Sum is a tag, saying "we want (+)"
Endo (1:) <> Endo (2:) = Endo ((1:) . (2:)) -- Endo is a tag, saying "we want (.)"
foldMap Sum [1,2,3] = Sum (1 + 2 + 3)
foldMap (Endo . (:)) [1,2,3] = Endo ((1:) . (2:) . (3:))
foldr' (:) [1,2,3] [4,5] = appEndo (foldMap (Endo . (:)) [1,2,3]) [4,5]
= appEndo (Endo ((1:) . (2:) . (3:))) [4,5]
= ((1:) . (2:) . (3:)) [4,5]
= [1,2,3,4,5] =
foldr (:) [4,5] [1,2,3]
Notice the lack of inner parentheses in (1 + 2 + 3)
as well as in ((1:) . (2:) . (3:))
. The associativity of <>
(here, +
, and .
) means the actual parenthesization doesn't matter semantically, and can only matter operationally. Which is the whole point, because grouping the calculations as a tree opens them up for the possibility of being calculated in parallel:
(1 + 2 + 3 + 4 + 5 + + ...)
=
( ( (1 + 2) + (3 + 4) ) + ( ( (5 + 6) + ... ) ... ) ... )
=
......
Of course the actual order with foldr
/ Endo
is linear:
foldr (:) [] [1,2,3,4]
=
1 : (2 : (3 : (4 : [] )))
(copying here some content from the comments, as we are supposed to do, on SO.)
Endo f <> Endo g = Endo $ x -> (f . g) x
, write it instead as an equational pseudocode, like appEndo (Endo f <> Endo g) x = (f . g) x
seen as the definition of <>
– it's not a valid Haskell, but expresses the intent more clearly, for me. I.e. I tend to see the lambdas as implementation, and equations as, well, equations (an example) ("tangled" does not apply here, of course, but e.g., like, with Continuation monad, etc.) You're asking,
"what
#.
does in(Endo #. f)
?
Endo #. (:) = coerce (:) `asTypeOf` (Endo . (:))
. this is the same as writing let { g :: a -> Endo [a] ; g = coerce (:) } in g
. Endo b
is practically the same as b -> b
, save for the tag Endo
which lets us define the Monoid instance for it. instance Monoid (b->b) where ....
isn't valid Haskell. So we can read Endo #. (:)
as just Endo . (:)
. newtype
is. it can be seen as compile-time tag. it disappears at run-time. (like, Sum 2
is actually just 2
at run time, and Product 2
is just 2
as well). But writing Sum . (1+)
hides its ephemeral nature, which apparently "can lead to very inefficient code". IOW this stuff is for compiler, it's not for the language itself. language-wise, #.
is just .
. #
is usually used in such situations.