Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stripping the newtype constructor

More often than not I am writing functions that are stripping the only constructor of a new type, such as in the following function to return the first argument that is not Nothing:

process (Pick xs) = (\(First x) -> x) . mconcat . map (First . process) $ xs

I think the lambda is unnecessarily verbose. I would like to write something like this:

process (Pick xs) = -First . mconcat . map (First . process) $ xs

Do the meta programming facilities of Haskell allow for anything similar to this? Any other solution to solve this problem in a more concise way is also welcome.

UPD. The whole code has been requested:

data Node where
  Join :: [Node] -> Node
  Pick :: [Node] -> Node
  Given :: Maybe String -> Node
  Name :: String -> Node

process :: Node -> Maybe String
process (Join xs) = liftM os_path_join (mapM process xs)
process (Pick xs) = getFirst . mconcat . map (First . process) $ xs
process (Name x) = Just x
process (Given x) = x
like image 649
NioBium Avatar asked May 05 '15 18:05

NioBium


2 Answers

In this case you can actually use the newtypes package to solve this problem more generically:

process :: Node -> Maybe String
process (Pick xs) = ala' First foldMap process xs
process (Join xs) = liftM os_path_join (mapM process xs)
process (Name x) = Just x
process (Given x) = x

You could even have a more generic version that takes a Newtype n (Maybe String) like

process'
    :: (Newtype n (Maybe String), Monoid n)
    => (Maybe String -> n) -> Node -> Maybe String
process' wrapper (Pick xs) = ala' wrapper foldMap (process' wrapper) xs
process' wrapper (Join xs) = liftM os_path_join (mapM (process' wrapper) xs)
process' wrapper (Name x) = Just x
process' wrapper (Given x) = x

Then

> let processFirst = process' First
> let processLast = process' Last
> let input = Pick [Given Nothing, Name "bar", Given (Just "foo"), Given Nothing]
> processFirst input
Just "bar"
> ProcessLast input
Just "foo"

As an explanation for how this works, the ala' function takes a newtype wrapper to determine the instance of Newtype to use, a function which in this case we want to be foldMap:

foldMap :: (Monoid m, Foldable t) => (a -> m) -> t a -> m

since foldMap f ends up being a generalized mconcat . map f over Foldable types instead of just lists, then a function to use as a "preprocessor" for hooking into the higher-order function being passed to ala' (foldMap), then in this case some Foldable t => t Node to process. If you didn't want the preprocessing step you'd just use ala, which uses id for its preprocessor. Using this function can sometimes be difficult due to its complex type, but as the examples in the documentation show foldMap is often a good choice.

The power of this is if you wanted to write your own newtype wrapper for Maybe String:

newtype FirstAsCaps = FirstAsCaps { getFirstAsCaps :: Maybe String }

firstAsCaps :: Maybe String -> FirstAsCaps
firstAsCaps = FirstAsCaps . fmap (fmap toUpper)

instance Monoid FirstAsCaps where
    mempty = firstAsCaps Nothing
    mappend (FirstAsCaps f) (FirstAsCaps g)
        = FirstAsCaps $ ala First (uncurry . on (<>)) (f, g)

instance Newtype FirstAsCaps (Maybe String) where
    pack = firstAsCaps
    unpack = getFirstAsCaps

Then

> process' firstAsCaps input
Just "BAR"
like image 191
bheklilr Avatar answered Sep 27 '22 22:09

bheklilr


If you're using Data.Monoid.First, then this is just getFirst. Many newtype wrappers use record syntax to provide an easy function to unwrap the newtype.

like image 41
Louis Wasserman Avatar answered Sep 27 '22 23:09

Louis Wasserman