iso1 :: Collector a b -> ([a] -> b)
iso1 (MakeCollector f x g) = g . foldl f x
iso2 :: ([a] -> b) -> Collector a b
iso2 f = MakeCollector (flip (:)) [] (f . reverse) iso1 :: Collector a b -> ([a] -> b)
iso1 (MakeCollector f x g) = g . foldl f x
iso2 :: ([a] -> b) -> Collector a b
iso2 f = MakeCollector (flip (:)) [] (f . reverse)Another difference is that you can combine a "Collector a b" and a "Collector a c" into a "Collector a (b,c)" that, like its components, only requires a single pass of the data. (This would be the Applicative instance for collectors.) Combining functions [a] -> b and [a] -> c doesn't necessarily "stream".
Also, I didn't use GADTs, only GADT "syntax" in which data constructors are provided as functions with signatures (MakeCollector :: ...)
Personally, I find this syntax much clearer for existential types. It amounts to having a type variable in a parameter which doesn't appear in the return type of the constructor.
Meanwhile, with a Collector, you can read the inputs with standard IO actions just fine, and feed them as they are produced.