Haskell 中 class 类型的异常约束

Exception constraint in type class in Haskell

我有问题。我正在尝试为文件系统管理写下 class 类型。我从一个简单的 class:

开始
import Control.Exception

class (Monad m) => Commands m where

  --Similar to catch :: Exception e => IO a -> (e -> IO a) -> IO a
  fsCatch
    :: Exception e
    => m a
    -> (e -> m a)
    -> m a

  setCurDir
    :: FilePath
    -> m ()

  caughtError
    :: Exception e
    => e
    -> m ()
  cd
    :: FilePath
    -> m ()
  cd path = fsCatch (setCurDir path) caughtError

有 cd 功能(移动目录)。它尝试使用 setCurDir 更改目录(需要实现),如果出现问题,它会调用 caughtError 函数。

我想制作两个实例:用于 IO 和我的 'toy' 文件系统数据 class。

对于 IO,它将看起来像这样:

instance Commands IO where
  fsCatch = catch

  setCurDir = setCurrentDirectory

  caughtError ex | isDoesNotExistError ex = putStrLn "No such directory"
                 | etc...

IO 有自己的 IOError。我将为我的玩具文件系统创建 MyFSError。但是我的类型 class:

出现了这个错误
 * Could not deduce (Exception e0) arising from a use of `fsCatch'
      from the context: Commands m
        bound by the class declaration for `Commands'
        at D:\University\FP\hw3-olegggatttor\hw3\src\Commands.hs:6:20-27
      The type variable `e0' is ambiguous
      These potential instances exist:
        instance Exception ErrorCall -- Defined in `GHC.Exception'
        instance Exception ArithException
          -- Defined in `GHC.Exception.Type'
        instance Exception SomeException -- Defined in `GHC.Exception.Type'
        ...plus 10 others
        ...plus three instances involving out-of-scope types
        (use -fprint-potential-instances to see them all)
    * In the expression: fsCatch (setCurDir path) catchedError
      In an equation for `cd':
          cd path = fsCatch (setCurDir path) catchedError
   |
61 |   cd path = fsCatch (setCurDir path) catchedError
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Failed, no modules loaded.

问题出在哪里,如何解决?必须使用class类型。

问题是 cd 需要 Exception e 约束来调用 fsCatchcaughtError。但是,cd 的定义或其类型签名中没有提供此类约束的任何内容。哎呀,类型 e 甚至没有 cd.

的类型签名中提到

对于这个特定类型 class,您似乎希望 monad m 确定异常类型 e。如果 m ~ IO,则 e ~ IOError,如果 m ~ ToyFilesystem,则 e ~ MyFSError。这可以使用函数依赖或关联的类型族来完成。我认为共识可能是关联类型族方法是更现代、更直接的方法,因此应该是首选方法,但我会向你们展示这两种方法。

对于功能依赖实现,您重新参数化类型 class 以包含异常类型 e,并添加一个注释 m -> e 指示 monad 的类型唯一确定异常类型:

{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses #-}

class (Monad m, Exception e) => Commands m e | m -> e where
  fsCatch :: m a -> (e -> m a) -> m a
  setCurDir :: FilePath -> m ()
  caughtError :: e -> m ()
  cd :: FilePath -> m ()
  cd path = fsCatch (setCurDir path) caughtError

IO 实例如下所示:

instance Commands IO IOError where
  fsCatch = catch
  setCurDir = setCurrentDirectory
  caughtError ex | isDoesNotExistError ex = putStrLn "No such directory"

请注意,您实际上根本不需要此 class 中的 Monad mException e 约束,因此请将 class 声明切换为:

class Commands m e | m -> e where
  ...

工作正常。我认为这是更好的做法——MonadException 约束不应出现在你的 class 声明中的任何地方——但这是另一个 SO 问题的答案。

对于关联的类型族实现,您将类型族声明添加到 class,明确地将 monad 映射到它的异常类型:

class (Monad m, Exception (E m)) => Commands m where
  type E m  -- exception type for monad m
  fsCatch :: m a -> (E m -> m a) -> m a
  setCurDir :: FilePath -> m ()
  caughtError :: E m -> m ()
  cd :: FilePath -> m ()
  cd path = fsCatch (setCurDir path) caughtError

同样,这里不需要约束,您可以这样写:

class Commands m where
  ...

实例然后为 monad m:

指定类型 E m
instance Commands IO where
  type E IO = IOError
  fsCatch = catch
  setCurDir = setCurrentDirectory
  caughtError ex | isDoesNotExistError ex = putStrLn "No such directory"

函数依赖的完整示例:

{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeSynonymInstances #-}

import Control.Exception
import System.Directory
import System.IO.Error

class Commands m e | m -> e where
  fsCatch :: m a -> (e -> m a) -> m a
  setCurDir :: FilePath -> m ()
  caughtError :: e -> m ()
  cd :: FilePath -> m ()
  cd path = fsCatch (setCurDir path) caughtError

instance Commands IO IOError where
  fsCatch = catch
  setCurDir = setCurrentDirectory
  caughtError ex | isDoesNotExistError ex = putStrLn "No such directory"

对于关联类型族:

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeFamilies #-}

import Control.Exception
import System.Directory
import System.IO.Error

class Commands m where
  type E m
  fsCatch :: m a -> (E m -> m a) -> m a
  setCurDir :: FilePath -> m ()
  caughtError :: E m -> m ()
  cd :: FilePath -> m ()
  cd path = fsCatch (setCurDir path) caughtError

instance Commands IO where
  type E IO = IOError
  fsCatch = catch
  setCurDir = setCurrentDirectory
  caughtError ex | isDoesNotExistError ex = putStrLn "No such directory"