Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Provide a different function body for a generic function based on type

Suppose I have some generic function

genericFunc :: a -> b
genericFunc x = doSomeHardWork

But for a particular type, there is a much more efficient way that genericFunc could be done.

genericFunc :: ParticularType -> b
genericFunc x = doSomeEasyWork

What is the best way to combine these two function bodies into the same genericFunc, such that when used on ParticularType, it will doSomeEasyWork, but when used on other types, it will doSomeHardWork? I'm specifically excluding the option of using a different name, or different modules.

I believe this can be done with a typeclass, but I am more interested in solutions that use language pragmas. I have a vague inkling that this can be done with language pragmas but I have no idea how. Bonus points if you compare and contrast these approaches, and/or any other possible approaches.

like image 953
Dan Burton Avatar asked Dec 11 '11 20:12

Dan Burton


2 Answers

This can be done with type classes by defining the general-purpose method in the class definition and overriding it in an instance. The overridden function will always be used.

class ContainsInt c where
  toList :: c -> [Int]

  -- generic function
  elem :: Int -> c -> Bool
  elem n x = Prelude.elem n (toList x)

instance ContainsInt () where
  toList _ = []

  -- Override the generic function for type ()
  elem _ _ = False

An alternative supported by GHC is to use a rewrite rule. The rewrite rule tells GHC to replace one expression by another whenever possible. If the replacement is ill-typed, it will not be done, so you can use this to replace a function by a specialized version. The rewrite rule is given by a {-# RULES #-} pragma.

class ContainsInt c where
  toList :: c -> [Int]

elem :: ContainsInt c => Int -> c -> Bool
elem n x = Prelude.elem n (toList x)

-- Replace 'elem' by 'elemUnit' if it has the same type
{-# RULES "elem()" forall. elem = elemUnit #-}

elemUnit :: Int -> () -> Bool
elemUnit _ _ = False

Rewrite rules are performed at the discretion of the compiler, so the specialized function may or may not be called in any given situation. For example, the rewrite may depend on whether the compiler decides to inline a function:

foo :: ContainsInt c -> Int -> [c] -> [Bool]
-- Must use the generic function
foo n cs = map (elem n) cs

useFoo :: Int -> [()] -> [Bool]
-- If 'foo' is inlined and 'elem' is not inlined, then this function will contain a rewritable call to 'elem'.
-- Otherwise rewriting cannot happen.
useFoo n cs = foo n cs
like image 158
Heatsink Avatar answered Nov 08 '22 00:11

Heatsink


With GHC, you can use a RULES pragma.

{-# RULES "genericFunc/easy" genericFunc = doSomeEasyWork #-}

This will apply the rewrite rule whenever the types match and use the generic implementation otherwise.

like image 34
hammar Avatar answered Nov 08 '22 00:11

hammar