altschuler
altschuler

Reputation: 3922

Use a parameter as pattern in Haskell

Is it possible to make a generic function that would take either Foo or Bar as an argument and would return a function that uses that argument in its pattern matching?

For instance, if I have

isFoo :: SomeData -> Bool
isFoo (Foo _) = True
isFoo _       = False

isBar :: SomeData -> Bool
isBar (Bar _) = True
isBar _       = False

Is there a way to create a generic function, something like

checkType :: SomeClass -> SomeData -> Bool
checkType (SomeClass _) = True
checkType _ = False

I realize the situation looks a little odd, and the actual use case is a little more complex, but the problem is identical.


The actual code I'm trying to refactor is the following

isString :: [LispVal] -> ThrowsError LispVal
isString [(String _)] = return $ Bool True
isString ((String _):xs) = isString xs >>= unpackBool >>= return . Bool
isString _ = return $ Bool False

isSymbol :: [LispVal] -> ThrowsError LispVal
isSymbol [(Atom _)] = return $ Bool True
isSymbol ((Atom _):xs) = isSymbol xs >>= unpackBool >>= return . Bool
isSymbol _ = return $ Bool False

isNumber :: [LispVal] -> ThrowsError LispVal
isNumber [(Number _)] = return $ Bool True
isNumber ((Number _):xs) = isNumber xs >>= unpackBool >>= return . Bool
isNumber _ = return $ Bool False

So I'd like some way to make this more dry

Upvotes: 12

Views: 2140

Answers (4)

josejuan
josejuan

Reputation: 9566

{-# LANGUAGE DeriveDataTypeable #-}
import Data.Data
import Data.Typeable

data Foo a = Foo1 a | Foo2 a deriving (Data, Typeable)

data Bar a = Bar1 a | Bar2 a deriving (Data, Typeable)

checkType :: Data a => Constr -> a -> Bool
checkType c v = c == toConstr v

example

zeroFoo1 = Foo1 (0 :: Int)

isFoo1 = checkType (toConstr zeroFoo1)

to generalize over a your checkType, you need a constant value (e.g. mempty) for each constructor.

(really, the only trick is toConstr a == toConstr b)

Upvotes: 2

danidiaz
danidiaz

Reputation: 27756

Prisms from the lens library can act as "first-class patterns". To define prisms for your datatype:

{-# LANGUAGE TemplateHaskell #-}

import Control.Lens

data SomeData = Foo Int 
              | Bar Char

-- Will create prisms named _Foo and _Bar
$(makePrisms ''SomeData)

Since Prisms are valid Folds, we can pass them to the has function from Control.Lens.Fold:

*Main> has _Foo (Foo 5)
True
*Main> has _Bar (Foo 5)
False

Another interesting application of prisms as first-class patterns is "overriding" the behaviour of a function for cases in which the argument matches the prism. You can use outside from Control.Lens.Prism to do that. outside is a function that takes a Prism and returns a Lens for functions, that allows you to "set" the special case. For example:

functionToOverride :: SomeData -> Int
functionToOverride = const 5

-- If the arg is a Foo, return the contained int + 1 
newFunction :: SomeData -> Int
newFunction = functionToOverride & outside _Foo .~ succ

Testing both functions:

*Main> functionToOverride (Foo 77)
5
*Main> newFunction (Bar 'a')
5
*Main> newFunction (Foo 77)
78

Upvotes: 27

ErikR
ErikR

Reputation: 52039

It seems that your isString function is just a lifting of the all function.

Consider this:

data LispVal = Str String | B Bool | Sym String | Num Integer

isLispStr (Str _) = True
isLispStr _ = False

isLispNum (Num _) = True
isLispNum _ = False

isLispSym (Sym _) = True
isLispSym _ = False

-- etc. for the other LispVal constructors.

Now consider these functions:

isString' :: [LispVal] -> LispVal
isString' = B . all isLispStr

isSymbol' :: [LispVal] -> LispVal
isSymbol' = B . all isLispSym

-- ...

These are the "pure" (i.e. non-monadic) versions of your isString and isSymbol functions. The monadic versions are just:

isString = return . isString'
isSymbol = return . isSymbol'

etc.

Upvotes: 1

John L
John L

Reputation: 28097

Currently this isn't possible, although some extensions that would allow it are in the works.

The closest workaround currently is probably to provide a function that matches on the appropriate pattern:

isString :: [LispVal] -> ThrowsError LispVal
isString [(String _)] = return $ Bool True
isString ((String _):xs) = isString xs >>= unpackBool >>= return . Bool
isString _ = return $ Bool False

You can replace the top pattern match with a function:

isLispVal :: (LispVal -> Bool) -> [LispVal] -> ThrowsError LispVal
isLispVal p [x] | p x = return $ Bool True
isLispVal p (x:xs) | p x = isLispVal p xs >>= unpackBool >>= return . Bool
isLispVal p _ = return $ Bool False

When I've done this, I've often ended up needed proper lenses instead of just predicate functions, but it depends on the use case.

Upvotes: 1

Related Questions