Rob
Rob

Reputation: 555

Excluding Types in Haskell

How would I use pattern matching in order to exclude certain types of inputs? For instance given the following:

f list k =
    if null list
    then []
    else head list + k : f (tail list) k

How can I use pattern matching to make f s/t it only allows Int and Integer but not Double or Float?

Upvotes: 2

Views: 784

Answers (4)

user6428287
user6428287

Reputation:

f/mapAdd implements a map. map is

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

and can be used to formulate mapAdd:

Prelude> mapAdd n  =  map (+ n)
Prelude> mapAdd 3 [1,2,3,4]
[4,5,6,7]

GHCi's :type command tells you the automatically inferred type signature of mapAdd:

Prelude> :type mapAdd
mapAdd :: Num b => b -> [b] -> [b]

Here, mapAdd's bs are constrained to the general numerical type class Num. You can explicitly constrain to the type class Integral, which (basically) exclusively contains the data types Int, and Integer. Integral is a subclass of Num.

Prelude> :{
Prelude| mapAdd :: Integral a => a -> [a] -> [a]
Prelude| mapAdd n  =  map (+ n)
Prelude| :}
Prelude> :t mapAdd
mapAdd :: Integral a => a -> [a] -> [a]

mapAdd n = map (+ n) is pointfree for mapAdd n lst = map (+ n) lst.

Upvotes: 3

Steven Armstrong
Steven Armstrong

Reputation: 475

There are a lot of ways to constrain types, the most direct is just with type families.

type family Elem x xs :: Constraint where
    Elem x (x ': xs) = ()
    Elem x (y ': xs) = Elem x xs
    Elem x '[] = TypeError ('ShowType x :<>: 'Text " is not a permitted type.")

type a ~~ b = Elem a b

function :: (a ~~ [Int, Integer], Num a) => [a] -> [a]

This allows you to white list a set of types. You won't know which one in particular you have though, that's more complicated.

However, you said you wanted to exclude certain types, so perhaps you want a black list. We simply flip the first and last case:

type family NotElem x xs :: Constraint where
    NotElem x (x ': xs) = TypeError ('ShowType x :<>: 'Text " is a forbidden type.")
    NotElem x (y ': xs) = NotElem x xs
    NotElem x '[] = ()

type a !~~ b = NotElem a b

function :: (a !~~ [Double, Float], Num a) => [a] -> [a]

function will now accept any Num type that is not a Double or Float.

However, there's no reason to actually do any of this for your example function. You lose nothing at all by allowing it to work on the full range of Num types. In fact, these hijinks will at the least cost you a tiny smidgen more time on type checking, or if you go further and do reflection based things, possibly run time overhead as well.

Upvotes: 0

dfeuer
dfeuer

Reputation: 48611

As suchtgott explained, you should not actually try to limit your function to exactly Int and Integer; you probably want to use an Integral constraint.

Suppose, for fun, that you really did want to limit it just like that. The way to do this in Haskell 98 is a bit weird (jump down to the break to see how this is done in GHC Haskell:

class Integral a => IntOrInteger a where
  intOrInteger :: Either (f a -> f Int) (f a -> f Integer)
instance IntOrInteger Int where
  intOrInteger = Left id
instance IntOrInteger Integer where
  intOrInteger = Right id

How do you use such a thing? Well, to start off with

intOrIntegerSimple :: IntOrInteger a => Either (a -> Int) (a -> Integer)
intOrIntegerSimple = case intOrInteger of
  Left f -> Left (runIdentity . f . Identity)
  Right f -> Right (runIdentity . f . Identity)

But how do you flip it around, and turn an Int or Integer back to an instance of IntOrInteger?

newtype Switch f a i = Switch {runSwitch :: f i -> f a}

intOrInteger' :: IntOrInteger a => Either (f Int -> f a) (f Integer -> f a)
intOrInteger' = case intOrInteger of
                  Left f -> Left (runSwitch (f (Switch id)))
                  Right f -> Right (runSwitch (f (Switch id)))

intOrIntegerSimple' :: IntOrInteger a => Either (Int -> a) (Integer -> a)
intOrIntegerSimple' = case intOrInteger' of
  Left f -> Left (runIdentity . f . Identity)
  Right f -> Right (runIdentity . f . Identity)

When you don't really care whether you have an Int or an Integer, but want to be sure you really have one of them, you have to watch out for invalid instances. What might an invalid instance look like? This is one option:

instance IntOrInteger Word where
  intOrInteger = Left (const undefined)

To just test that the instance is valid, you can use this (importing Data.Proxy):

ensureIntOrInteger :: IntOrInteger a => Proxy a -> ()
ensureIntOrInteger p = case intOrInteger of
  Left f -> f p `seq` ()
  Right f -> f p `seq` ()

The result of ensureIntOrInteger will be defined if and only if the type a is actually an Int or an Integer.


Nice; it works. But in practice it's pretty nasty to use. You can do much better with a few GHC extensions:

{-# LANGUAGE GADTs, TypeFamilies, TypeOperators, ConstraintKinds,
      UndecidableInstances, UndecidableSuperClasses, DataKinds #-}

-- In 8.0 and later, Constraint is also available from Data.Kind
import GHC.Exts (Constraint)
import Data.Type.Equality ((:~:)(..))
import GHC.TypeLits (TypeError, ErrorMessage (..))

type family IntOrIntegerC a :: Constraint where
  IntOrIntegerC Int = ()
  IntOrIntegerC Integer = ()
  IntOrIntegerC t = TypeError ('ShowType t :<>:
                               'Text " is not an Int or an Integer.")

class (Integral a, IntOrIntegerC a) => IntOrInteger a where
  intOrInteger :: Either (a :~: Int) (a :~: Integer)
instance IntOrInteger Int where
  intOrInteger = Left Refl
instance IntOrInteger Integer where
  intOrInteger = Right Refl

With this formulation, the IntOrIntegerC constraint family blocks out any invalid types without your needing to do anything, giving a useful error message if someone tries to write a bogus instance. And if you actually do need to use the equality evidence, it's simply a matter of pattern matching on intOrInteger or using the various handy functions in Data.Type.Equality.


A point of style: using head and tail is generally discouraged in Haskell. We prefer to pattern match instead. Your function could be written

f [] _ = []
f (x : xs) k = x + k : f xs k

Upvotes: 3

ZhekaKozlov
ZhekaKozlov

Reputation: 39556

You can create a dumb type class with instances only for Int and Integer and then use a type constraint:

class (Num a) => IntOrInteger a
instance IntOrInteger Int
instance IntOrInteger Integer

f :: (IntOrInteger a) => [a] -> a -> [a]
f list k =
    if null list
    then []
    else head list + k : f (tail list) k

Then if you use f with Int, it will compile:

> f [1,2,3] 1
[2,3,4]

But it will not compile with Double:

> f [1,2,3] (1::Double)

<interactive>:19:1: error:
    * No instance for (IntOrInteger Double) arising from a use of `f'
    * In the expression: f [1, 2, 3] (1 :: Double)
      In an equation for `it': it = f [1, 2, 3] (1 :: Double)

Upvotes: 1

Related Questions