Sml 函数在应该为字符串选项时返回TextIO.elem选项

Sml 函数在应该为字符串选项时返回TextIO.elem选项,sml,smlnj,Sml,Smlnj,我正在编写一个函数,它接收文件名和读取文件时要替换的字符对列表。我目前在我的一个帮助功能上出错 prac.sml:177.5-182.12 Error: right-hand-side of clause doesn't agree with function result type [tycon mismatch] expression: string option -> string * string -> unit result type: TextIO.elem

我正在编写一个函数,它接收文件名和读取文件时要替换的字符对列表。我目前在我的一个帮助功能上出错

prac.sml:177.5-182.12 Error: right-hand-side of clause doesn't agree with function result type [tycon mismatch]
  expression:  string option -> string * string -> unit
  result type:  TextIO.elem option -> string * string -> unit
下面是给出错误的函数。我不清楚到底是什么导致了这一切,有人能帮我看看到底出了什么问题吗

fun echoFile (infile)  (c) (x,y) = 
    if isSome c
      then (
        printChar (valOf c) (x,y);
        echoFile infile (TextIO.input1 infile) (x,y)
      ) else ()
以下是printChar函数:

fun printChar (c) (x,y)  = 
    if x = c
      then print y
      else print c
下面是调用它的函数

fun fileSubst _ [] = ()
  | fileSubst inputFile ((x,y)::xs) =
    let
      val infile = TextIO.openIn inputFile
    in
      echoFile infile TextIO.input1(infile) (x,y);
      TextIO.closeIn(infile);
      fileSubst inputFile xs
    end

以下是对您编写的代码的一些反馈:

  • 函数
    TextIO.input1
    的类型为TextIO.instream→ TextIO.elem选项。当您(例如,通过在sml提示符中写入
    opentextio;
    )时,您将找到定义
    type elem=char
    。因此,将输出视为字符而不是字符串。您可以使用char类型的函数
    str
    → 一串但是考虑使用行缓冲,因为每次读取一个字符的文件在系统调用和分配方面都是昂贵的。
  • 我已经删除了:
    fun
    val
    和其他声明之后的那些声明只在REPL中需要立即得到结果。
    是一个运算符

  • 我删除了不必要的括号。在构造元组(
    (x,y)
    )和声明优先级时,确实需要括号。例如,
    echoFile infle(TextIO.input1 infle)(x,y)
    表示
    echoFile
    是一个有三个参数的函数,第二个参数是
    TextIO.input1 infle
    ,它本身就是一个应用于参数的函数。但不需要第二对括号来表示函数应用。也就是说,
    TextIO.input1 infle
    TextIO.input1(infle)
    一样好,就像每次你有号码
    42
    时你都不用费心去写
    (42)

    这意味着您在这一行的
    fileSubst
    中仍然有一个bug:

    echoFile infile TextIO.input1(infile) (x,y)
    
    因为这被解释为具有四个参数的
    echoFile
    TextIO.input1
    (infle)
    (x,y)
    。似乎
    TextIO.input1
    (infle)
    粘在一起是因为没有空格,但函数应用程序被认为是函数在其参数前面的定位,而不是括号的存在。此外,函数应用程序关联到左边,因此如果我们在上面的行中添加显式括号,它将变为:

    (((echoFile infile) TextIO.input1) (infile)) (x,y)
    
    为了克服左关联性,我们写:

    echoFile infile (TextIO.input1 infile) (x,y)
    
    将其解释为(粗体括号为显式括号):

    ((echoFile infle)
    )(x,y)

  • 似乎您的函数
    fileSubst
    应该用字符
    y
    替换字符
    x
    。我可能会称之为“文件映射”,因为它非常类似于(char)类型的库函数
    String.map
    → 字符)→ 一串→ 一串是否保留(x,y)映射列表或字符→ char函数非常相似

我可能会编写一个类型为(char)的函数
fileMap
→ 字符)→ 河道内→ 扩展到类似于
String.map

fun fileMap f inFile outFile =
    let fun go () =
            case TextIO.inputLine inFile of
                 NONE   => ()
               | SOME s => ( TextIO.output (outFile, String.map f s) ; go () )
    in go () end
然后使用它,例如:

fun cat () = fileMap (fn c => c) TextIO.stdIn TextIO.stdOut

fun fileSubst pairs =
    fileMap (fn c => case List.find (fn (x,y) => x = c) pairs of
                          NONE       => c
                        | SOME (x,y) => y)
对这些问题的一些思考:

  • 当类似函数的参数可以是文件或文件名时,我希望变量名中的区别更加明确。例如,
    inputFile
    vs.
    infle
    不适合我。我宁愿使用例如
    infle
    filePath

  • 我想,函数应该采用文件路径还是内部流,取决于您希望如何编写它。因此,像
    fileMap
    这样的非常通用的函数可能采用流内/流外,但也可能采用文件路径。如果您同时使用这两种类型的函数,那么最好通过名称来区分它们,或者将它们划分为不同的模块

  • 您可能想要处理任意的外流,而不仅仅是
    TextIO.stdOut
    ,因为您也在处理任意的内流。您可以始终使用特殊情况下的标准输入/输出,如
    cat

  • 我在
    fileMap
    中创建了一个辅助函数
    go
    ,用于处理递归。在这种情况下,我也可以不使用,让
    fileMap
    直接调用自己:

    fun fileMap f inFile outFile =
        case TextIO.inputLine inFile of
             NONE => ()
           | SOME s => ( TextIO.output (outFile, String.map f s)
                       ; fileMap f inFile outFile )
    
    因为
    fileMap
    不会在其他参数中累积任何状态。但是递归函数通常需要额外的参数来保持它们的状态,同时,我不想污染函数的类型签名(比如你的
    echoFile
    c
    )。这是monad的一个主要用例

    我可以使用各种库函数来处理
    /
    选项
    中找到的一些

    local
      val getOpt = Option.getOpt
      val mapOpt = Option.map
      val find = List.find
    in
      fun fileSubst pairs =
        fileMap (fn c => getOpt (mapOpt #2 (find (fn (x,y) => x = c) pairs), c))
    end
    

感谢您如此深入的解释!