1

As an example, let's take the following algorithm for computing the longest common subsequence of two sequences, copied from Rosetta Code:

longest xs ys = if length xs > length ys then xs else ys
 
lcs [] _ = []
lcs _ [] = []
lcs (x:xs) (y:ys) 
  | x == y    = x : lcs xs ys
  | otherwise = longest (lcs (x:xs) ys) (lcs xs (y:ys))

lcs impicitly assumes that both arguments are of type Eq a => [a]; indeed, if I try to explicitly give the signature lcs :: [a] -> [a] -> [a] an error occurs on the line where x == y is, whereas the signature lcs :: Eq a => [a] -> [a] -> [a] works.

Now let's suppose I have two lists l1 and l2, both of type [(a,b)], and that I want the LCS between them, but in a way that uses the == operator in the definition of lcs only between the snds of each element (obviously b is required to belong to Eq typeclass).

I could provide lcs with not only the two lists, but also with the equality operator, which in the specific example above would be (==) `on` snd. The signature of lcs would then be (a -> a -> Bool) -> [a] -> [a] -> [a].

However, this would force the user to provide the equality operator even in the trivial case that one wants to use a plain (==), and wrapping the (a -> a) argument in a Maybe wouldn't help either.

In other words I feel that in the general case the constraint on the argument via Eq a => is ok, but in other cases one might want to pass a custom equality operator removing that constraint, which makes me thing of function overloading.

How should I go about this?

Enlico
  • 23,259
  • 6
  • 48
  • 102

2 Answers2

4

You simply provide both – a custom-operator version lcsBy, and an Eq a => version which is implemented in terms of that.

lcsBy :: (a->a->Bool) -> [a] -> [a] -> [a]
...
lcsBy compOp (x:xs) (y:ys) 
 | compOp x y  = ...
 ...

lcs :: Eq a => [a] -> [a] -> [a]
lcs = lcsBy (==)

This is analogous to how the base library provides both maximum and maximumBy.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
3

Providing both is the simpler option, as @leftaroundabout wrote.

Still, in some specific cases, there are ways to call a function like

lcs :: Eq a => [a] -> [a] -> [a]

providing a custom equality operator like yours. For instance,

{-# LANGUAGE ScopedTypeVariables #-}
import Data.Coerce
import Data.Function

newtype OnSnd a b = OnSnd { getPair :: (a, b) }
instance Eq b => Eq (OnSnd a b) where
   (==) = (==) `on` (snd . getPair)

lcsOnSnd :: forall a b. Eq b => [(a,b)] -> [(a,b)] -> [(a,b)]
lcsOnSnd xs ys = coerce $ lcs (coerce xs) (coerce ys :: [OnSnd a b])
-- OR turn on TypeApplications, then:
-- lcsOnSnd = coerce (lcs @(OnSnd a b))

converts [(a,b)] to [OnSnd a b] using a zero-cost safe coercion, applies lcs (which will use the custom == of OnSnd a b), and converts the result back (zero-cost, again).

For this approach to work, however, == must be definable at the top-level, i.e. it can not be a generic closure depending on, say, an additional parameter to lcsOnSnd.

Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
chi
  • 111,837
  • 3
  • 133
  • 218
  • This `coerce` thing is interesting. However this solution still requires one to have two names, `lcs` and `lcsOnSnd`, to refer conceptually to the same thing, so I think the solution of simply having `lcs` and `lcsBy` is more practical. – Enlico Feb 20 '21 at 13:12
  • 3
    @Enlico the point is that you would _not_ need to provide two versions, but _users_ of your function could hack into it to get it to behave as if an explicit comparison operator was passed, even though it's not. Incidentally, the `coerce` isn't strictly needed, it could also be done with a simple `map` but that would be slower. — Note also that [a direct way of passing explicit dictionaries](https://doi.org/10.1145/3242744.3242752) is being researched. – leftaroundabout Feb 20 '21 at 13:25
  • 1
    Oh, now I understand. So yours is the solution from the point of view of the user who uses a library which doesn't provide a customization point for the equality operator used in an algorithm, whereas the other answer is from the point of view of the library writer. – Enlico Feb 20 '21 at 13:30
  • 2
    Your `OnSnd` (...well, actually, `OnFst`) is also available in the standard library as [`Arg`](https://hackage.haskell.org/package/base-4.14.1.0/docs/Data-Semigroup.html#t:Arg). – Daniel Wagner Feb 20 '21 at 16:48