问题
I have defined an F-Algebra, as per Bartosz Milewski's articles (one, two):
(This is not to say my code is an exact embodiment of Bartosz's ideas, it's merely my limited understanding of them, and any faults are mine alone.)
module Algebra where
data Expr a = Branch [a] | Leaf Int
instance Functor Expr where
fmap f (Branch xs) = Branch (fmap f xs)
fmap _ (Leaf i ) = Leaf i
newtype Fix a = Fix { unFix :: a (Fix a) }
branch = Fix . Branch
leaf = Fix . Leaf
-- | This is an example algebra.
evalSum (Branch xs) = sum xs
evalSum (Leaf i ) = i
cata f = f . fmap (cata f) . unFix
I can now do pretty much anything I want about it, for example, sum the leaves:
λ cata evalSum $ branch [branch [leaf 1, leaf 2], leaf 3]
6
This is a contrived example that I made up specifically for this question, but I actually tried some less trivial things (such as evaluating and simplifying polynomials with any number of variables) and it works like a charm. One may indeed fold and replace any parts of a structure as one runs a catamorphism through, with a suitably chosen algebra. So, I am pretty sure an F-Algebra subsumes a Foldable, and it even appears to subsume Traversable as well.
Now, can I define Foldable / Traversable instances in terms of an F-Algebra?
It seems to me that I cannot.
- I can only run a catamorphism on an initial algebra, which is a nullary type constructor. And the algebra I give it has a type
a b -> brather thana -> b, that is to say, there is a functional dependency between the "in" and "out" type. - I don't see an
Algebra a => Foldable aanywhere in type signatures. If this is not done, it must be impossible.
It seems to me that I cannot define Foldable in terms of an F-Algebra for the reason that an Expr must for that be a Functor in two variables: one for carrier, another for values, and then a Foldable in the second. So, it may be that a bifunctor is more suitable. And we can construct an F-Algebra with a bifunctor as well:
module Algebra2 where
import Data.Bifunctor
data Expr a i = Branch [a] | Leaf i
instance Bifunctor Expr where
bimap f _ (Branch xs) = Branch (fmap f xs)
bimap _ g (Leaf i ) = Leaf (g i)
newtype Fix2 a i = Fix2 { unFix2 :: a (Fix2 a i) i }
branch = Fix2 . Branch
leaf = Fix2 . Leaf
evalSum (Branch xs) = sum xs
evalSum (Leaf i ) = i
cata2 f g = f . bimap (cata2 f g) g . unFix2
It runs like this:
λ cata2 evalSum (+1) $ branch [branch [leaf 1, leaf 2], leaf 3]
9
But I still can't define a Foldable. It would have type like this:
instance Foldable \i -> Expr (Fix2 Expr i) i where ...
Unfortunately, one doesn't get lambda abstractions on types, and there's no way to put an implied type variable in two places at once.
I don't know what to do.
回答1:
An F-algebra defines a recipe for evaluating a single level of a recursive data structure, after you have evaluated all the children. Foldable defines a way of evaluating a (not necessarily recursive) data structure, provided you know how to convert values stored in it to elements of a monoid.
To implement foldMap for a recursive data structure, you may start by defining an algebra, whose carrier is a monoid. You would define how to convert a leaf to a monoidal value. Then, assuming that all children of a node were evaluated to monoidal values, you'd define a way to combine them within a node. Once you've defined such an algebra, you can run a catamorphism to evaluate foldMap for the whole tree.
So the answer to your question is that to make a Foldable instance for a fixed-point data structure, you have to define an appropriate algebra whose carrier is a monoid.
Edit: Here's an implementation of Foldable:
data Expr e a = Branch [a] | Leaf e
newtype Ex e = Ex { unEx :: Fix (Expr e) }
evalM :: Monoid m => (e -> m) -> Algebra (Expr e) m
evalM _ (Branch xs) = mconcat xs
evalM f (Leaf i ) = f i
instance Foldable (Ex) where
foldMap f = cata (evalM f) . unEx
tree :: Ex Int
tree = Ex $ branch [branch [leaf 1, leaf 2], leaf 3]
x = foldMap Sum tree
Implementing Traversable as a catamorphism is a little more involved because you want the result to be not just a summary--it must contain the complete reconstructed data structure. The carrier of the algebra must therefore be the type of the final result of traverse, which is (f (Fix (Expr b))), where f is Applicative.
tAlg :: Applicative f => (e -> f b) -> Algebra (Expr e) (f (Fix (Expr b)))
Here's this algebra:
tAlg g (Leaf e) = leaf <$> g e
tAlg _ (Branch xs) = branch <$> sequenceA xs
And this is how you implement traverse:
instance Traversable Ex where
traverse g = fmap Ex . cata (tAlg g) . unEx
The superclass of Traversable is a Functor, so you need to show that the fixed-point data structure is a functor. You can do it by implementing a simple algebra and running a catamorphism over it:
fAlg :: (a -> b) -> Algebra (Expr a) (Fix (Expr b))
fAlg g (Leaf e) = leaf (g e)
fAlg _ (Branch es) = branch es
instance Functor Ex where
fmap g = Ex . cata (fAlg g) . unEx
(Michael Sloan helped me write this code.)
回答2:
It's very nice, that you used Bifunctor. Using Bifunctor of a base functor (Expr) to define Functor on a fixpoint (Fix Expr).
That approach generalises to Bifoldable and Bitraversable (they are in base now) too.
Let's see how this would like using recursion-schemes.
It looks a bit different, as there we define normal recursive type,
say Tree e, and also its base functor: Base (Tree e) = TreeF e a with two functions:
project :: Tree e -> TreeF e (Tree e) and embed :: TreeF e (Tree e) -> Tree e.
The recursion machinery is derivable using TemplateHaskell:
Note that we have Base (Fix f) = f (project = unFix, embed = Fix),
therefore we can use refix convert Tree e to Fix (TreeF e) and back. But
we don't need to use Fix, as we able to cata Tree directly!
First includes:
{-# LANGUAGE TemplateHaskell, KindSignatures, TypeFamilies, DeriveFunctor, DeriveFoldable, DeriveTraversable #-}
import Data.Functor.Foldable
import Data.Functor.Foldable.TH
import Data.Bifunctor
import Data.Bifoldable
import Data.Bitraversable
Then the data:
data Tree e = Branch [Tree e] | Leaf e deriving Show
-- data TreeF e r = BranchF [r] | LeafF e
-- instance Traversable (TreeF e)
-- instance Foldable (TreeF e)
-- instance Functor (TreeF e)
makeBaseFunctor ''Tree
Now as we have machinery in place, we can have catamorphisms
cata :: Recursive t => (Base t a -> a) -> t -> a
cata f = c where c = f . fmap c . project
or (which we will need later)
cataBi :: (Recursive t, Bifunctor p, Base t ~ p x) => (p x a -> a) -> t -> a
cataBi f = c where c = f . second c . project
First a Functor instance. The Bifunctor instance for TreeF is as OP has written,
note how Functor falls out by itself.
instance Bifunctor TreeF where
bimap f _ (LeafF e) = LeafF (f e)
bimap _ g (BranchF xs) = BranchF (fmap g xs)
instance Functor Tree where
fmap f = cata (embed . bimap f id)
Not surprisingly, Foldable for fixpoint can be defined in terms of Bifoldable of base
functor:
instance Bifoldable TreeF where
bifoldMap f _ (LeafF e) = f e
bifoldMap _ g (BranchF xs) = foldMap g xs
instance Foldable Tree where
foldMap f = cata (bifoldMap f id)
And finally Traversable:
instance Bitraversable TreeF where
bitraverse f _ (LeafF e) = LeafF <$> f e
bitraverse _ g (BranchF xs) = BranchF <$> traverse g xs
instance Traversable Tree where
traverse f = cata (fmap embed . bitraverse f id)
As you can see the definitions are very straight forward and follow similarish pattern.
Indeed we can define traverse-like function for every fix-point which base
functor is Bitraversable.
traverseRec
:: ( Recursive t, Corecursive s, Applicative f
, Base t ~ base a, Base s ~ base b, Bitraversable base)
=> (a -> f b) -> t -> f s
traverseRec f = cataBi (fmap embed . bitraverse f id)
Here we use cataBi to make type-signature prettier: no Functor (base b) as
it's "implied" by Bitraversable base. Btw, that's a one nice function as its
type signature is three times longer than the implementation).
To conclude, I must mention that Fix in Haskell is not perfect:
We use the last argument to fix base-functor:
Fix :: (* -> *) -> * -- example: Tree e ~ Fix (TreeF e)
Thus Bartosz needs to define Ex in his answer to make kinds align,
however it would be nicer to fix on the first argument:
Fix :: (* -> k) -> k -- example: Tree e = Fix TreeF' e
where data TreeF' a e = LeafF' e | BranchF' [a], i.e. TreeF with indexes
flipped. That way we could have Functor (Fix b) in terms of Bifunctor f,
Bifunctor (Fix b) in terms of (non-existing in common libraries) Trifunctor etc.
You can read about my failed attempts about that and Edward Kmett's comments on the issue in https://github.com/ekmett/recursion-schemes/pull/23
来源:https://stackoverflow.com/questions/48488021/once-i-have-an-f-algebra-can-i-define-foldable-and-traversable-in-terms-of-it