【问题标题】:Functional dependencies in HaskellHaskell 中的函数依赖
【发布时间】:2013-11-18 13:24:07
【问题描述】:

我正试图围绕功能依赖关系展开思考,但我自己并没有取得任何进展。在论文《Monad Transformers Step by Step》中,作者给出了这两个类型类的定义:

class (Monad m) => MonadError e m | m -> e where
    throwError :: e -> m a
    catchError :: m a -> (e -> m a) -> m a

class (Monad m) => MonadReader r m | m -> r where
    ask :: m r
    local :: (r -> r) -> m a -> m a

根据我对网上找的一些资料的理解,表示类型变量e是由m决定的。我只是不明白那是什么意思。它是如何确定的?任何人都可以先用最少的理论阐明一些观点,然后再将更重的理论联系起来吗?

谢谢

【问题讨论】:

    标签: haskell typeclass


    【解决方案1】:

    当你有一个多参数类型类时,默认情况下,类型变量是独立考虑的。所以当类型推断器试图找出哪个实例时

    class Foo a b
    

    选择,它必须独立确定ab,然后去查看实例是否存在。有了函数依赖,我们可以减少这种搜索。当我们做类似的事情时

    class Foo a b | a -> b
    

    我们在说 “看,如果你确定 a 是什么,那么就有一个唯一的 b,所以 Foo a b 存在所以不要费心去推断 b,去吧查找实例并进行类型检查”。 这让类型推断器更加有效,并有助于在许多地方进行推断。

    这对于返回类型多态性特别有用,例如

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

    现在无法推断

      bar (bar "foo" 'c') 1
    

    因为我们无法确定c。即使我们只为StringChar 编写了一个实例,我们也必须假设有人可能/将会出现并稍后添加另一个实例。如果没有fundeps,我们必须实际指定返回类型,这很烦人。但是我们可以写

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

    现在很容易看出bar "foo" 'c' 的返回类型是唯一的,因此可以推断。

    【讨论】:

      【解决方案2】:

      这意味着类型系统将始终能够从类型m 中确定类型(er)。

      让我们自己做一个例子:

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

      我们应该始终能够从b 中确定a

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

      类型系统知道对于这个实例,只要它有一个Example1,它的a 的类型就是Int。它是怎么知道的?它不允许我们拥有另一个类型的另一个实例。

      如果我们添加

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

      我们得到一个错误:

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

      但如果b 有不同类型,我们可以为a 添加另一个类型不同的实例

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

      这可预测的输出

      1
      "two"
      

      【讨论】:

        【解决方案3】:

        当 Haskell 尝试解析 MonadError e m 时,默认情况下,它会同时搜索 em 参数,以查找恰好有实例的任何对。如果我们没有 e 出现在约束本身之外的类型签名中的任何位置,这尤其困难

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

        函数依赖表明,一旦我们解决了m,就只能有一个e 有效。这让上面的片段可以编译,因为它向 Haskell 保证那里有足够的信息让它有一个明确的类型。

        如果没有函数依赖,Haskell 会抱怨 unitError 是模棱两可的,因为它可能对任何类型 e 都有效,而且我们无法知道该类型是什么——信息不知何故蒸发成稀薄的空气。


        对于MonadError,函数依赖通常意味着 monad 本身是由错误类型参数化的。例如,这里有一个实例

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

        e ~ em ~ Either e 的位置,我们看到m 确实唯一标识了一个可能有效的e


        函数依赖也“几乎”等同于类型族。类型家庭有时更容易消化。例如,这是一个MonadError 类,TypeFamilies 样式

        {-# 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
        

        这里,Err 是一个类型函数,它将一个 m 带到它的特定错误类型 e 中,并且对于任何 m 是否有一个等于 Err me 的概念自然来自我们对函数的理解。

        【讨论】:

        • +1 用于类型族。我认为函数依赖更受欢迎,不仅仅是因为它们更老,还因为我们可以继续使用小写字母来表示我们认为是类型变量的东西。
        • @Cirdec:不过,您始终可以将e ~ Err m 添加到您的约束中以获得如此短的小写类型变量。我真的很喜欢生成的签名,它们几乎与FunctionalDependencies 一样简洁,同时与TypeFamilies 一样富有表现力。
        猜你喜欢
        • 2020-09-10
        • 2012-01-25
        • 2023-03-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-11-29
        相关资源
        最近更新 更多