2

I have two functions like these:

foo :: a -> b -> x -> x
bar :: c -> y -> y

I would like to unify them into single interface so they both could have same name. In my case it's guaranteed that types x and y are different (but there might be multiple x's). So it looks possible. But all my attempts have bad type inference in case of partial application :(

Could you help to come up with interface for this case?

My previous attempts look like this but this solution requires explicit type application to disambiguate cases:

class Insert s i where
  insert :: i

instance (Ord k, i ~ (k -> Set k -> Set k)) => Insert Set i where
  insert = Set.insert
Shersh
  • 9,019
  • 3
  • 33
  • 61
  • Can you provide a bit more information? Why is it important that two functions with incompatible signatures share a name? What sort of code are you writing that abstracts over the interface? – Carl Jul 22 '18 at 06:02
  • @Carl I would like to unify these two functions: `insert :: Ord k => k -> v -> Map k v -> Map k v` and `insert :: Ord a => a -> Set a -> Set a`. Yes, I know that this might be a bad idea and that some people might not like it. But I don't force anyone to use this approach. I just interested whether it's possible or not. – Shersh Jul 22 '18 at 06:12
  • 2
    That doesn't sound like a useful thing to do. You should create unified interfaces when it's possible to write meaningful code that doesn't care what instance of the interface it's using, not when names happen to be the same. This is true in all languages. If you don't have the ability to use different instantiations interchangeably, you don't have an abstraction, you have a coincidence. – Carl Jul 22 '18 at 06:18
  • The indentation you show on types can be misleading. You should match the first argument with the first, the second with the second, etc. At the end on one type you have the return type `y` which is matched on the other side with `x -> x`. In general, overloading could be done via type classes, but it can create ambiguities in some cases during type inference, especially when using partial application and/or higher order functions (`map (foo a)`). This will require the user to use a type annotation, making it less convenient. – chi Jul 22 '18 at 06:45
  • 1
    @Carl It's possible to have separate names like `insertMap` and `insertSet` to separate use cases. But it's less convenient since historically name is `insert` for different types :( I don't think this is only _coincidence_ because `Set a` can be just a special case of `Map` like `Map a ()`. But it's not convenient to insert `()` manually. – Shersh Jul 22 '18 at 07:05
  • @chi Yes, I have solution which works with all arguments provided but requires `-XTypeApplications` in case of partial application. This is not very convenient because one of most often uses cases is something like `x = foldr insert mempty myData` but when type of `x` is known in advance so I want compiler to be able to pick `insert` for `Set`. Re indentation: that's a fair point, I fixed this. – Shersh Jul 22 '18 at 07:07
  • 1
    If `foo` were uncurried to `foo :: (a, b) -> x -> x`, it would make more sense to unify `foo` and `bar`. – chepner Jul 22 '18 at 15:24
  • @chepner Yeah, it's quite trivial to unify `(a, b)` and `c` but `insert k v` is more convenient than `insert (k, v)` and has less performance overhead. – Shersh Jul 22 '18 at 15:32
  • You said you tried something and weren't satisfied with it. How about including what you tried and what specifically was wrong with it (e.g., in what context did you need to provide an explicit type application)? – K. A. Buhr Jul 22 '18 at 16:42
  • @K.A.Buhr Done. I've added my solution. – Shersh Jul 23 '18 at 06:08

1 Answers1

1

I guess the following probably works. As per the comments, it's still a terrible, terrible, terrible idea. If you really want to write Haskell like this, then you don't really want to write Haskell.

Anyway, the idea is to provide a type class for:

insert :: (Ord k) => k -> v -> a

parameterized in all three parameters with a functional dependency specifying that a determines k and v. In your case, a ~ (Map k' v' -> Map k' v') implies k ~ k' and v ~ v', while a ~ Set t implies k ~ t and v ~ Set t.

{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, FunctionalDependencies,
             NoMonomorphismRestriction #-}

module UnifiedInsert where

-- insert :: Ord k => k -> v -> Map k v -> Map k v
-- insert :: Ord a => a -> Set a -> Set a

import Data.Map (Map)
import qualified Data.Map as Map
import Data.Set (Set)
import qualified Data.Set as Set

class Insert k v a | a -> k, a -> v where
  insert :: (Ord k) => k -> v -> a
instance Insert k (Set k) (Set k) where
  insert = Set.insert
instance Insert k v (Map k v -> Map k v) where
  insert = Map.insert

fromList = foldr insert mempty

foo1 :: Set Int
foo1 = fromList [1..10]

Note that the type checker can deduce the type of the function fromList (even if foo1 is dropped from the program), as long as the monomorphism restriction is off. It will have the rather preposterous type:

fromList :: (Foldable t, Insert k v v, Ord k, Monoid v) => t k -> v

which can, indeed, be specialized to Ord a => [a] -> Set a, as required.

K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71