Skip to content

mikeizbicki/ifcxt

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

IfCxt

This package introduces the function:

ifCxt :: IfCxt cxt => proxy cxt -> (cxt => a) -> a -> a

This function acts like an if statement where the proxy cxt parameter is the condition. If the type checker can satisfy the cxt constraint, then the second argument cxt => a is returned; otherwise, the third argument a is returned.

Before seeing more details about how ifCxt is implemented, let's look at three examples of how to use it.

Example 1: show every type

The cxtShow function below is polymorphic over the type a. If a is an instance of Show, then cxtShow a evaluates to show a; but if a is not an instance of Show, cxtShow a evaluates to <<unshowable>>.

cxtShow :: forall a. IfCxt (Show a) => a -> String
cxtShow a = ifCxt (Proxy::Proxy (Show a))
    (show a)
    "<<unshowable>>"

In ghci:

ghci> cxtShow (1 :: Int)
"1"
ghci> cxtShow (id :: a -> a)
"<<unshowable>>"

Example 2: make your code asymptotically efficient

The nub function removes duplicate elements from lists. It can be defined as:

nub :: Eq a => [a] -> [a]
nub []     =  []
nub (x:xs) =  x : nub (filter (x/=) xs)

This function takes time O(n^2). But if we also have an Ord constraint, we can define a much more efficient version that takes time O(n log n):

nubOrd :: Ord a => [a] -> [a]
nubOrd = go . sort
    where
        go (x1:x2:xs)
            | x1==x2    =      go (x2:xs)
            | otherwise = x1 : go (x2:xs)
        go [x] = [x]
        go []  = []

Now, we can use the ifCxt function to define a version of nub that will automatically select the most efficient implementation for whatever type we happen to run it on:

cxtNub :: forall a. (Eq a, IfCxt (Ord a)) => [a] -> [a]
cxtNub = ifCxt (Proxy::Proxy (Ord a)) nubOrd nub

Example 3: make your code numerically stable

The simplest way to sum a list of numbers is:

sumSimple :: Num a => [a] -> a
sumSimple = foldl' (+) 0

This method has numerical stability issues on floating point representations. Kahan summation is a more accurate technique shown below:

sumKahan :: Num a => [a] -> a
sumKahan = snd . foldl' go (0,0)
    where
        go (c,t) i = ((t'-t)-y,t')
            where
                y = i-c
                t' = t+y

Because Kahan summation does a lot more work than simple summation, we would prefer not to run it on non-floating point types. The sumCxt function below accomplishes this:

cxtSum :: forall a. (Num a, IfCxt (Floating a)) => [a] -> a
cxtSum = ifCxt (Proxy::Proxy (Floating a)) sumKahan sumSimple

Notice that the ifCxt function is conditioning on the Floating a constraint, which isn't actually used by the sumKahan function.

How it works

The magic of the technique is in the IfCxt class:

class IfCxt (cxt :: Constraint) where
    ifCxt :: proxy cxt -> (cxt => a) -> a -> a

(Notice that making a constraint an instance of a class requires theConstraintKinds extension, and the higher order (cxt => a) parameter requires the RankNTypes extension.)

There is a "global" instance defined as:

instance {-# OVERLAPPABLE #-} IfCxt cxt where ifCxt _ t f = f

What this says is that if no more specific instance is available, then the "global" ifCxt function will be used, which always returns the f (false) parameter.

Then for every instance of every other class, we need to define an overlapping IfCxt instance that always returns the t (true) parameter. For example, for Show Int, we define:

instance {-# OVERLAPS #-} IfCxt (Show Int) where ifCxt _ t f = t

This is a lot of boilerplate, so the template haskell function mkIfCxtInstances can be used to define these instances automatically. Unfortunately, due to a bug in template haskell we cannot enumerate all the classes currently in scope. So you must manually call mkIfCxtInstances on each class you want ifCxt to work with.

About

constraint level if statements

Resources

License

Stars

Watchers

Forks

Packages

No packages published