Provide a different function body for a generic function based on type
Asked Answered
A

2

5

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.

Adventitia answered 11/12, 2011 at 20:57 Comment(1)
Vaguely inspired by thinking about this question: If something is not a listAdventitia
R
8

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
Rostock answered 11/12, 2011 at 21:24 Comment(0)
L
2

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.

Lumumba answered 11/12, 2011 at 21:17 Comment(3)
Whenever GHC can prove that the types match.Orris
Are there some good examples of this "in real code" that I can look at?Adventitia
Don't use rules, use a typeclass. This is what typeclasses is for.Debut

© 2022 - 2024 — McMap. All rights reserved.