mathematical.coffee
mathematical.coffee

Reputation: 56935

Reuse same function body for multiple type signatures

I have a function like so:

test :: [Block] -> [Block]
test ((Para foobar):rest) = [Para [(foobar !! 0)]] ++ rest
test ((Plain foobar):rest) = [Plain [(foobar !! 0)]] ++ rest

Block is a datatype that includes Para, Plain and others. What the function does is not particularly important, but I notice that the body of the function ([Para [(foobar !! 0)]] ++ rest) is the same for both Para and Plain, except that the constructor used is the type of foobar.

Question: is there someway to concisely write this function with the two cases combined? Something like

test :: [Block] -> [Block]
test ((ParaOrPlain foobar):rest) = [ParaOrPlain [(foobar !! 0)]] ++ rest

where the first ParaOrPlain matches either Para foobar or Plain foobar and the second ParaOrPlain is Para or Plain respectively.

Note that Block can also be (say) a BulletList or OrderedList and I don't want to operate on those. (edit: test x = x for these other types.)

The key is that I don't want to replicate the function body twice since they're identical (barring the call to Para or Plain).

I get the feeling I could use Either, or perhaps my own data type, but I'm not sure how to do it.


Edit To clarify, I know my function body is cumbersome (I'm new to Haskell) and I thank various answerers for their simplifications.

However at the heart of the problem, I wish to avoid the replication of the Para and Plain line. Something like (in my madeup language...)

# assume we have `test (x:rest)` where `x` is an arbirtrary type
if (class(x) == 'Para' or class(x) == 'Plain') {
    conFun = Plain
    if (class(x) == 'Para')
        conFun = Para
    return [conFun ...]
}
# else do nothing (ID function, test x = x)
return x:rest

i.e., I want to know if it's possible in Haskell to assign a constructor function based on the type of the input parameter in this way. Hope that clarifies the question.

Upvotes: 4

Views: 359

Answers (6)

Cubic
Cubic

Reputation: 15683

This can be done pretty nicely with ViewPatterns

Note: view patterns isn't actually required here, it just makes it looks a bit nicer imho

Note': This assumes the list inside the block is of the same type in both cases

{-# LANGUAGE ViewPatterns #-}
paraOrPlain :: Block a- > Maybe (a -> Block a, a)
paraOrPlain (Plain xs) = Just (Plain,xs)
paraOrPlain (Para xs) = Just (Para,xs)
paraOrPlain _ = Nothing

-- or even better

para (Para xs) = Just (Para,xs)
para _ = Nothing

plain (Plain xs) = Just (Plain,xs)
plain _ = Nothing

paraOrPlain' b = para b <|> plain b -- requires Control.Applicative

test ((paraOrPlain -> Just (con,xs)) : rest) = con (take 1 xs) : rest

Of course you still need to do the pattern matching somewhere - you can't do that generically without TH or Generic - but you can do it in a way that lends itself to reuse.

Upvotes: 1

leftaroundabout
leftaroundabout

Reputation: 120731

The closest you can get to your original idea is probably a "generic Para-or-Plain-modifying" function.

{-# LANGUAGE RankNTypes #-}
modifyBlock :: (forall a . [a]->[a]) -> Block -> Block
modifyBlock f (Plain foobar) = Plain $ f foobar
modifyBlock f (Para foobar)  = Plain $ f foobar

Observe that f has in each use a different type!

With that, you can then write

test (b:bs) = modifyBlock (\(h:_) -> [h]) b : bs

Upvotes: 1

Shoe
Shoe

Reputation: 76280

Assuming a data type declaration in the form:

data Block
    = Para { field :: [Something] }
    | Plain { field :: [Something] }
    ...

You can simply use a generic record syntax:

test :: [Block] -> [Block]
test (x:rest) = x { field = [(field x) !! 0] } : rest

Live demo

Upvotes: 3

yatima2975
yatima2975

Reputation: 6610

This is about as nice as I can get it without using Template Haskell or Data.Data :-)

First of all, we need to enable some extensions and fix the Block datatype for concreteness (if my simplifying assumptions are wrong, tell me and I'll see what can be salvaged!)

{-# LANGUAGE GADTs, LambdaCase #-}
data Block = Para [Int]
           | Plain [String]
           | Other Bool Block
           | YetAnother deriving (Show,Eq)

The important point here is that Para and Plain are unary constructors, but that the datatype itself can contain constructors with a different number of arguments.

As @leftaroundabout and @user5402 explained, we can separate the concerns of modifying a single Block and applying that modification to the first element of a list only, so I'll focus on rewriting

modifyBaseline :: Block -> Block
modifyBaseline (Para xs) = Para [xs !! 0]
modifyBaseline (Plain xs) = Plain [xs !! 0]
modifyBaseline rest = rest

Next, we need to be able to talk about unary constructors as values. There are 3 important things here:

  • does a value match a given constructor,
  • if so what values are 'inside',
  • and how can we (re-)construct such a value.

We package this up nicely in a custom type (t is the type the constructor belongs to, and a is what's inside):

data UnaryConstructor t a = UnaryConstructor {
    destruct:: t -> Maybe a
    construct :: a -> t
}

So now we can define

para :: UnaryConstructor Block [Int]
para = UnaryConstructor (\case { Para ns -> Just ns ; _ -> Nothing }) Para
plain :: UnaryConstructor Block [Int]
plain = UnaryConstructor (\case { Plain ss -> Just ss ; _ -> Nothing }) Plain

(You can get rid of the LambdaCase extension if you write (\xs -> case xs of { Para ns -> Just ns; _ -> Nothing}) but this way it's nice and compact!)

Next, we need to 'package up' a unary constructor and the function to apply to the value contained in it:

data UnaryConstructorModifier t where
    UnaryConstructorModifier :: UnaryConstructor t a -> (a -> a) -> UnaryConstructorModifier t

so that we can write

modifyHelper :: [UnaryConstructorModifier t] -> t -> t
modifyHelper [] t = t
modifyHelper ((UnaryConstructorModifier uc f):ucms) t 
    | Just x <- destruct uc t = construct uc (f x)
    | otherwise = modifyHelper ucms t

and finally (the lambda could have been (\xs -> [xs !! 0]) or (\xs -> [head xs]) to taste):

modify :: Block -> Block
modify = modifyHelper [UnaryConstructorModifier para (\(x:_) -> [x]), 
                       UnaryConstructorModifier plain (\(x:_) -> [x])]

If we now test it with

ghci> let blocks = [Para [1,2,3,4], Plain ["a","b"], Other True (Para [42,43]), YetAnother ] 
ghci> map modify blocks == map modifyBaseline blocks

we get True - hooray!


The repetitive bit is now in the definitions of para and plain where you have to write the name of the constructor three times, but there's no way around that without using Template Haskell or Data.Data (that I can see).

Further options for improvement would be to do something for constructors of different arity, and to put a function of type a -> Maybe a in UnaryConstructorModifier to deal with the partiality of (\(x:_) -> [x]) but I think this answers your question nicely.

I hope you can make sense of this, as I've glossed over a couple of details, including what's going on in the definition of UnaryConstructorModifier and the use of pattern guards in modifyHelper - so ask if you need clarification!

Upvotes: 1

leftaroundabout
leftaroundabout

Reputation: 120731

First note that, as user5402 already wrote, you should replace [Para [(foobar !! 0)]] ++ rest with Para [foobar !! 0] : rest, which looks already a good deal nicer. Next note that basically, all you're doing is modify the head of a list, so as a meaningful helper I'd use

modifyHead :: (a->a) -> [a] -> [a]
modifyHead f (x:xs) = f x : xs
modifyHead _ [] = []

test = modifyHead m
 where m (Para foobar) = Para [head foobar]  -- `head` is bad, but still better than `(!!0)`!
       m (Plain foobar) = Plain [head foobar]

Upvotes: 1

ErikR
ErikR

Reputation: 52049

One approach is to use a (polymorphic) helper function, e.g.:

helper ctor xs rest = ctor [ head xs ] : rest

test :: [Block] -> [Block]
test ((Para xs):rest)  = helper Para xs rest
test ((Plain xs):rest) = helper Plain xs rest
test bs                = bs

Note that Para and Plain are just functions of type [whatever] -> Block.

Upvotes: 3

Related Questions