Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rationale for two separate map functions in ClassyPrelude

ClassyPrelude has two map functions, namely:

  1. map
  2. omap

map works on any Functor. However, things like Text are not functors as they aren't generic containers. That is, they can't contain any type, unlike a list. As can be seen in the following code:

module Main where
  import Prelude ()
  import ClassyPrelude
  import qualified Data.Text as T
  import Data.Char as C

  main = do
    let l = [1,2,3] :: [Int]
    let t = (T.pack "Hello")
    let m = Just 5
    print $ map (*2) l
    print $ map (*2) m
    print $ omap C.toUpper t
    return ()

Notice one has to use omap to deal with the Text. As map requires the type to be a Functor, map f text fails. The thing is, I found it trivially easy to redefine map to work for both calls. Here's the code:

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverlappingInstances #-}

module Main where
  import Prelude hiding (map)
  import qualified Data.Text as T
  import Data.Char as C
  import Control.Monad (Functor)
  import qualified Data.Vector.Unboxed as U

  class CanMap a b where
    type Element a :: *
    type Container a b :: *
    map :: (Element a -> b) -> a -> Container a b

  instance (Functor f) => CanMap (f a) b where
    type Element (f a) = a
    type Container (f a) b = f b
    map = fmap    

  instance CanMap T.Text Char where
    type Element T.Text = Char
    type Container T.Text Char = T.Text
    map = T.map

  instance (U.Unbox a, U.Unbox b) => CanMap (U.Vector a) b where
    type Element (U.Vector a) = a
    type Container (U.Vector a) b = U.Vector b
    map = U.map

  main = do
    let l = [1,2,3] :: [Int]
    let m = Just 5
    let t = (T.pack "Hello")
    let u = U.generate 3 id 
    print $ map (*2) l
    print $ map (*2) m
    print $ map C.toUpper t
    print $ map (*2) u
    return ()

All that's required is to add instances to CanMap for any monomorphic containers. ClassyPrelude already does this anyway with "omap" in the Data.MonoTraversable module. I suspect however there's a good reason I'm missing about why there should be two separate map functions to deal with these alternate situations, but I'm wondering what that is.

like image 549
Clinton Avatar asked Feb 12 '15 04:02

Clinton


1 Answers

I think the problem is with types such as unboxed Vectors, where the type looks almost like it should have a Functor instance but you actually need a restriction on the element types to do the mapping:

instance U.Unbox a => MonoFunctor (U.Vector a)

When you try to include such types in your system, with the way GHC only looks at the heads and not the contexts of instances when looking them up, you will necessarily end up with overlapping instance problems.

It can also be useful, in the case of map, to know that you really can convert to any "element" type you want. With only one multiparameter typeclass you cannot express that the instance sometimes doesn't depend on the element.

like image 96
Ørjan Johansen Avatar answered Sep 20 '22 10:09

Ørjan Johansen