Join GitHub today
GitHub is home to over 36 million developers working together to host and review code, manage projects, and build software together.Sign up
Proposal: Named functional dependencies #3580
Computing large types using functional dependencies leads to a problem:
class C a b | a -> b where build :: Proxy a -> b
If I want to have my class
built :: forall b. C A b => b built = build (Proxy :: Proxy A)
but that approach leads to the dictionary for
I'd like to propose we add named functional dependencies as a way to refer to the computed type instead.
class C a b | Computed :: a -> b
This would act like a type synonym which would only be defined for types with an instance.
Now I can give a much nicer type annotation which has neither of the problems above:
built :: Computed A built = build (Proxy :: Proxy A)
This essentially turns functional dependencies into associated types.
What do people think of this idea?
I think it could be possible to implement any necessary breaking changes early, such as
I'd think this is probably the simplest path to something which gives the same benefits as associated types, and implementing them separately would be like having two features which do most of the same things.
It mostly comes down to syntax though. You could perhaps dress up functional dependencies with associated type syntax, but with all of the same constraints.
Incidentally, there is a bug in the functional dependencies check which is relevant here. It's possible to write something like
instance c :: C A (B x)
This would be detected if we wrote this as a pseudo type synonym:
type Computed a type instance Computed A = B x -- x not in scope here
I was thinking of Language and Program Design for Functional Dependencies
How would you see this working for something like
class Union (left :: # Type) (right :: # Type) (union :: # Type) | UBoth :: left right -> union , ULeft :: right union -> left , URight :: union left -> right
Something like that?
I think for syntax the
To me, it seems like the most straightforward way to implement this would be as a desugaring step (float out to the "real" constraint and deduplicate), but if that were the case, then your example would necessarily desugar into what you would write today, which requires a dictionary. If it's not a desugaring step, I think you'll be on your way to implementing type families
This is the part that doesn't make sense to me, but I'm having a hard time articulating why. In the paper, it's suggested that functional notation be expanded to the constraint you'd normally write, but we don't want that since it results in a dictionary. Are you suggesting this notation only applies when all arguments have no type variables?
When you write an instance, the mapping from named fundeps to their classes has to be stored in some global lookup. Then, during type synonym expansion, if we see a "synonym" in that lookup, i.e. one from one of these associated types, we find the relevant instance using the usual logic from the elaborator and then expand the synonym using the functional dependency. That doesn't need a dictionary, because it's purely at the level of types, not values.
Edit: technically, more is necessary, since we might not have the type information necessary to apply the "usual logic" at the point of synonym expansion. However, we can hopefully expand it to a placeholder which can be replaced during the usual solve-unify loop, just like we do with dictionaries. But this would be a placeholder for a type, not for a value.
Does this then only apply to classes/instances that have no runtime evidence?
This sounds an awful lot like type families to me
I don't think efficiency is a strong enough reason to add a type system feature like this for me, personally, especially not if it involves breaking changes. I think I might prefer providing a way of asking the compiler to generate specialised versions of values which have constraints.
I really like this idea! Why not demand writing all functional dependencies in the form of associated types like this:
class C a b where type Computed a = b
In the body of the class, every functional dependency would be written out explicitly in the form of an associated type (or "associated type dependency", if you like...). It can only use the variables declared with the class. Using this notation, it is possible to write down all possible relations between types:
-- Open relations: class Cast a b where -- Nothing special here cast :: a -> b -- Surjective relations: class Collection c e where type Elem c = e -- Like the old `| c -> e` empty :: c insert :: e -> c -> c member :: e -> c -> Bool -- Bijective relations class Boxed b u where type Unbox b = u type Box u = b -- Like the old `| b -> u, u -> b` unbox :: b -> u box :: u -> b -- Multiple type arguments: class Add' (a :: Nat) (b :: Nat) (r :: Nat) where type Add a b = r -- Like the old `| a b -> r` -- Multiple type results: class FiniteMap m i e where type Index m = i type Elem m = e -- Like the old `| m -> i e` empty :: m lookup :: i -> m -> Maybe e extend :: i -> e -> m -> m
I think it would create a best of both worlds. From type families:
From functional dependencies:
Only nitpick: no functional syntax while defining associated types...
-- need to type class And' (a :: Boolean) (b :: Boolean) (r :: Boolean) where type And a b = r instance And' True b b else And' False b False else fail -- instead of type And (a :: Boolean) (b :: Boolean) :: Boolean where And True b = b And False b = False
(Could be added as sugar, but I'm not sure if there are really great benefits in doing that. Although Jones and Diatchki  state in section 3.5 that type synonyms are actually a special case of functional dependencies.)
 Jones, M. P. (2000). Type classes with functional dependencies. ESOP (pp. 230-244). Springer, Berlin.