Functional dependencies in Haskell

后端 未结 3 1797
你的背包
你的背包 2020-12-13 01:06

I\'m trying to wrap my head around functional dependencies, but I am not getting anywhere on my own. In the paper \"Monad Transformers Step by Step\", the author gives thes

相关标签:
3条回答
  • 2020-12-13 01:15

    When Haskell is trying to resolve MonadError e m it, by default, scours both the e and m parameters together looking for any pair which happens to have an instance. This is especially hard if we don't have an e showing up anywhere in the type signature outside of the constraint itself

    unitError :: MonadError e m => m ()
    unitError = return ()
    

    The functional dependency says that once we've resolved m, there can be only one e that works. That lets the above fragment compile as it reassures Haskell that there's enough information there for it to have a non-ambiguous type.

    Without the functional dependency, Haskell would complain that unitError is ambiguous because it could be valid for any type e and we don't have any way to know what that type is—the information has somehow evaporated into thin air.


    For MonadError the functional dependency usually means that the monad itself is parameterized by the error type. For instance, here's an instance

    instance MonadError e (Either e) where
      thowError = Left
      catchError (Left e)  f = f e
      catchError (Right a) _ = Right a
    

    Where e ~ e and m ~ Either e and we see that m does indeed uniquely identify a single e which could be valid.


    Functional dependencies are "nearly" equivalent to Type Families as well. Type Families are sometimes a little easier to digest. For instance, here's a MonadError class, TypeFamilies style

    {-# LANGUAGE TypeFamilies #-}
    
    class MonadError m where
      type Err m
      throwError :: Err m -> m a
      catchError :: m a -> (Err m -> m a) -> m a
    
    instance MonadError (Either e) where
      type Err (Either e) = e
      throwError = Left
      catchError (Left e) f  = f e
      catchError (Right a) _ = Right a
    

    Here, Err is a type function which takes a m to its particular error type e and the notion if there being exactly one e equal to Err m for any m comes naturally from our understanding of functions.

    0 讨论(0)
  • 2020-12-13 01:21

    When you have a multiparameter typeclass, by default, the type variables are considered independently. So when the type inferencer is trying to figure out which instance of

    class Foo a b
    

    to choose, it has to determine a and b independently, then go look check to see if the instance exists. With functional dependencies, we can cut this search down. When we do something like

    class Foo a b | a -> b
    

    We're saying "Look, if you determine what a is, then there is a unique b so that Foo a b exists so don't bother trying to infer b, just go look up the instance and typecheck that". This let's the type inferencer by much more effective and helps inference in a number of places.

    This is particularly helpful with return type polymorphism, for example

    class Foo a b c where
      bar :: a -> b -> c
    

    Now there's no way to infer

      bar (bar "foo" 'c') 1
    

    Because we have no way of determining c. Even if we only wrote one instance for String and Char, we have to assume that someone might/will come along and add another instance later on. Without fundeps we'd have to actually specify the return type, which is annoying. However we could write

    class Foo a b c | a b -> c where
      bar :: a -> b -> c
    

    And now it's easy to see that the return type of bar "foo" 'c' is unique and thus inferable.

    0 讨论(0)
  • 2020-12-13 01:32

    It means that the type system will always be able to determine the type (e or r) from the type m.

    Let's make our own example:

    class KnowsA a b | b -> a where
        known :: b -> a
    

    We should always be able to determine a from b

    data Example1 = Example1 deriving (Show)
    
    instance KnowsA Int Example1 where
        known = const 1 
    

    The type system knows that for this instnace, whenever it has an Example1, the type of it's a will be Int. How does it know? It doesn't allow us to have another instance with another type.

    If we add

    instance KnowsA [Char] Example1 where
        known = const "one"
    

    We get an error:

        Functional dependencies conflict between instance declarations:
          instance KnowsA Int Example1
          instance KnowsA [Char] Example1
    

    But we can add another instance with a different type for a if we have a different type for b

    data Example2 = Example2 deriving (Show)
    
    instance KnowsA [Char] Example2 where
        known = const "two"
    
    main = do
        print . known $ Example1
        print . known $ Example2
    

    This predictably outputs

    1
    "two"
    
    0 讨论(0)
提交回复
热议问题