定义接口

Control.App 提供的运行 App 的唯一方法是通过 run 函数,它接收一个具体的错误列表 Init 。对这个错误列表的所有具体扩展都是通过 handle 以引入一个新的异常,或者 new 以引入一个新状态。为了有效地组成 App 程序,而不是笼统地引入具体的异常和状态,我们为在特定错误列表中工作的操作集合定义接口。

Console I/O 示例

我们已经看到了一个使用 Console 接口的初始示例,它在 Control.App.Console 中声明如下:

interface Console e where
  putChar : Char -> App {l} e ()
  putStr : String -> App {l} e ()
  getChar : App {l} e Char
  getLine : App {l} e String

它提供了用于写入和读取控制台的原语,并将路径参数推广到 | 意味着两者都不能抛出异常,因为它们必须在 NoThrowMayThrow 上下文中工作。

为了在顶层 IO 程序中实现这一点,我们需要访问原始的 IO 操作。 Control.App 库为此定义了一个原语接口:

interface PrimIO e where
  primIO : IO a -> App {l} e a
  fork : (forall e' . PrimIO e' => App {l} e' ()) -> App e ()

我们使用 primIO 来调用 IO 函数。我们还有一个 fork 原语,它在支持 PrimIO 的新错误列表中启动一个新线程。请注意, fork 启动了一个新的错误列表 e ,因此状态仅在单个线程中可用。

PrimIO 有一个错误列表的实现,可以将空类型作为异常抛出。这意味着如果 PrimIO 是唯一可用的接口,我们不能抛出异常,这与 IO 的定义是一致的。这也允许我们在初始错误列表 Init 中使用 PrimIO

HasErr AppHasIO e => PrimIO e where ...

鉴于此,我们可以实现 Console 并在 IO 中运行我们的 hello 程序。它在 Control.App.Console 中实现如下:

PrimIO e => Console e where
  putChar c = primIO $ putChar c
  putStr str = primIO $ putStr str
  getChar = primIO getChar
  getLine = primIO getLine

示例:文件 I/O

控制台 I/O 可以直接实现,但大多数 I/O 操作可能会失败。例如,打开文件失败的原因有多种:文件不存在;用户拥有错误的权限等。在 Idris 中, IO 原语在其类型中反映了这一点:

openFile : String -> Mode -> IO (Either FileError File)

虽然精确,但当有很长的 IO 操作序列时,这会变得笨拙。使用 App 时,我们可以提供一个接口,当操作失败时抛出异常,并保证使用 handle 在顶层处理任何异常。我们首先在 Control.App.FileIO 中定义 FileIO 接口:

interface Has [Exception IOError] e => FileIO e where
  withFile : String -> Mode ->
             (onError : IOError -> App e a) ->
             (onOpen : File -> App e a) ->
             App e a
  fGetStr : File -> App e String
  fGetChars : File -> Int -> App e String
  fGetChar : File -> App e Char
  fPutStr : File -> String -> App e ()
  fPutStrLn : File -> String -> App e ()
  fflush : File -> App e ()
  fEOF : File -> App e Bool

我们使用资源括号 - 将函数传递给 withFile 来处理打开的文件 - 而不是显式的 open 操作来打开文件,以确保文件句柄在完成时被清理。

还可以想象一个接口使用文件的线性资源,这在某些安全关键的上下文中可能是合适的,但对于大多数编程任务,异常应该就足够了。所有的操作都可能失败,接口明确表示,如果错误列表支持抛出和捕获 IOError 异常,我们只能实现 FileIOIOErrorControl.App 中定义。

例如,我们可以使用这个接口来实现 readFile ,如果在 withFile 中打开文件失败则抛出异常:

readFile : FileIO e => String -> App e String
readFile f = withFile f Read throw $ \h =>
               do content <- read [] h
                  pure (concat content)
where
  read : List String -> File -> App e (List String)
  read acc h = do eof <- fEOF h
                  if eof then pure (reverse acc)
                         else do str <- fGetStr h
                                 read (str :: acc) h

同样,这是在 Control.App.FileIO 中定义的。

要实现 FileIO ,我们需要通过 PrimIO 访问原始操作,以及在任何操作失败时抛出异常的能力。有了这个,我们可以如下实现 withFile ,例如:

Has [PrimIO, Exception IOError] e => FileIO e where
  withFile fname m onError proc
      = do Right h <- primIO $ openFile fname m
              | Left err => onError (FileErr (toFileEx err))
           res <- catch (proc h) onError
           primIO $ closeFile h
           pure res
  ...

鉴于 FileIO 的这个实现,我们可以运行 readFile ,前提是我们将它包装在一个顶级的 handle 函数中以处理 readFile 抛出的任何错误:

readMain : String -> App Init ()
readMain fname = handle (readFile fname)
       (\str => putStrLn $ "Success:\n" ++ show str)
       (\err : IOError => putStrLn $ "Error: " ++ show err)