I recently had an email thread with Greg Weber and Max Cantor about
classy-prelude. We ended up focusing on classy-prelude's implementation of
map. Currently, classy-prelude defines a CanMap typeclass as follows:
class CanMap ci co i o | ci -> i, co -> o, ci o -> co, co i -> ci where
map :: (i -> o) -> ci -> coOur conversation revolved mostly around how this leads to difficult type signatures and error messages, which is certainly a known problem with the class-based approach. However, that's not what I want to focus on now. (If anyone's interested in that conversation, let me know, I think it's an interesting one to discuss more broadly as well.)
The issue we're trying to solve with this typeclass is dealing with many
different shapes of containers, from polymorphic without constraints (e.g.,
list, vector), to constrained polymorphic (e.g., Set) to completely monomorphic
(e.g., ByteString, Text). I wanted to investigate prior art in this area, so I
decided to look at Edward's ever-popular lens package, something I'm always
itching to learn more about.
I'm by no means an expert on this package, but it seems like the relevant type
class for the map function would be Each, defined as:
class (Functor f, Index s ~ Index t) => Each f s t a b | s -> a, t -> b, s b -> t, t a -> s where
each :: IndexedLensLike (Index s) f s t a bI was shocked at just how similar these two typeclasses were to each other.
Using each, it's possible to define a map function:
map :: Each.Each Lens.Mutator s t a b => (a -> b) -> s -> t
map = Lens.over Each.eachI tried replacing this in classy-prelude (using FP Haskell
Center to do my coding, of course), and the
result
was nearly perfect. As expected, I had to modify some type signatures in my
test suite to use Each instead of CanMap. Also, there appear are no
instances of Each for Set and HashSet; please see my explanation
below based on a very good explanation I got from Edward.
To me, this is a very interesting direction to consider heading in.
classy-prelude has always focused on pragmatism, and retaining the
programming style most users are accustomed to. lens, on the other hand,
takes a much more principled approach to its type classes, but has a steeper
learning curve. Perhaps by building classy-prelude on top of lens in this
manner, we can get an easy-to-learn library which gradually exposes users to
powerful and well-designed abstractions. This would also allow the community to
focus on making instances for one set of typeclasses, and then users could
essentially layer whatever high-level interface on top of it that they want.
I haven't looked into the other typeclasses in classy-prelude yet, but I have
a strong feeling that many of them can be implemented on top of lens classes.
Is this something others find interesting and worth pursuing?
Coming back to that original thread from Greg and Max, I am a bit concerned
that such a change would only make the error message and type signature issues
even harder to deal with. But classy-prelude has never been a
beginner-friendly project, and at least if both classy-prelude and lens
users got the same terrifying error messages, it might be easier to build up
some common documentation on how to address them.
No Set/HashSet instances
I'm sure most of us are familiar with the common identity:
map f . map g = map (f . g)This is also the second functor law:
fmap f . fmap g = fmap (f . g)However, this identity does not apply with Set, as you can see with a simple
example:
import qualified Data.Set as Set
newtype AlwaysEq a = AlwaysEq { unAlwaysEq :: a }
instance Eq (AlwaysEq a) where
_ == _ = True
instance Ord (AlwaysEq a) where
_ `compare` _ = EQ
main :: IO ()
main = do
let s = Set.fromList [1..3]
print $ (Set.map unAlwaysEq . Set.map AlwaysEq) s
print $ Set.map (unAlwaysEq . AlwaysEq) sSaid another way, maping on a Set can change its shape, by causing some
elements to be removed. This is in constrast to other examples of map, which
ensure the shape is maintained.
As far as classy-prelude is concerned, I'd be content to reexport Set.map
under a name like setMap, but not have it represented by the map function.