将 getLine 输入与 haskell 数组连接会引发类型错误

concatenate getLine input with haskell array throws a type error

我完全是 Haskell 的初学者 我来自 js 环境,我有一个简单的数组 students 我想 push 一些学生对象但是遗憾的是 Haskell 不支持对象(如果有办法我可以做到,请指导我)所以尝试制作一个简单的程序来读取用户输入(数组)和 push 进入 students 数组,这是我尝试过的:

main :: IO()
main = do 
  let students = []
  studentArray <- getLine
  students ++ studentArray
  print(students)

但抛出以下错误:Couldn't match type `[]' with `IO'

首先,您可能想看一下 this SO answer 中的资源。如果您还没有完成 "Absolute Beginner" 部分中的教程,那将是一个很好的起点。

请注意,对于其他编程语言,通常从在屏幕上打印 "Hello, world!" 的程序开始,或者像您的示例一样从控制台读取学生列表并将它们打印出来。对于 Haskell,一开始使用完全不同类型的程序通常更有意义。例如,教程 "Learn You a Haskell for Great Good" doesn't get to "Hello, world!" until Chapter 9, and the "Happy Learn Haskell Tutorial" 直到第 15 章才涉及到它(然后它只涉及输出——输入直到第 20 章才出现)。

无论如何,回到你的例子。问题出在 students ++ studentArray 行。这是一个将空列表 students = []studentArray 的值连接起来的表达式,studentArraygetLine 检索到的 String。由于 String 只是一个字符列表,空列表只是空字符串,因此您正在编写 JavaScript 函数的粗略等效项:

function main() {
    var students = ""          // empty list is just empty string
    var studentArray = readLineFromSomewhere()
    students + studentArray    // concatenate strings and throw away result
    console.log(students)      // print the empty string
}

在 JavaScript 中,这会 运行 并打印空字符串,因为 students + studentArray 行不执行任何操作。在 Haskell 中,这不会进行类型检查,因为 Haskell 期望此 do 块中的所有(非 let)行都是 I/O 操作:

main :: IO ()         -- signature forces `do` block to be I/O
main = do 
  let students = []          -- "let" line is okay
  studentArray <- getLine    -- `getLine` is IO action
  students ++ studentArray   -- **NOT** IO action:  it's a String AKA [Char]
  print students             -- `print students` is IO action

因为 students ++ studentArrayString / [Char] / 出现在 IO do 块中的字符列表,Haskell 期望 IO something 但发现 [something],它抱怨列表类型 ([]) 和 IO 不匹配。

但是,即使您可以解决此问题,也无济于事,因为与 JavaScript + 运算符不同,与 JavaScript push 方法不同, Haskell ++ 运算符不修改其参数,因此 a ++ b 仅 returns ab 的串联而不更改 ab.

这是 Haskell 的一个非常基本的方面,这使得它与大多数其他编程语言不同。默认情况下,Haskell 变量是不可变的。一旦它们在顶层被 let 语句赋值,或者在函数调用中作为参数赋值,它们的值就不会改变。 (事实上​​,因为它们不是真正的 "variable",我们通常称它们为 "bindings" 而不是 "variables"。)所以,如果你想建立一个 students 的列表在 Haskell 中,您不会首先将一个空列表分配给一个变量,然后尝试通过添加学生来修改该变量。相反,您要么一次完成所有操作:

import Control.Monad (replicateM)

main :: IO ()
main = do
  putStrLn "Enter number of students:"
  n <- readLn
  putStrLn $ "Enter " ++ show n ++ " student names:"
  students <- replicateM n getLine
  putStrLn $ "List of students:"
  print students

或使用函数调用通过将标识符重新绑定到更新后的值来模拟变量:

main :: IO ()
main = do
  putStrLn "Enter number of students:"
  n <- readLn
  putStrLn $ "Enter " ++ show n ++ " student names:"
  students <- getStudents n []
  print students

getStudents :: Int -> [String] -> IO [String]
getStudents 0 studentsSoFar = return studentsSoFar
getStudents n studentsSoFar = do
  student <- getLine
  getStudents (n-1) (studentsSoFar ++ [student])

在这里查看 getStudents 最初是如何使用学生总数和初始空列表(在 [=43= 中分别绑定到 nstudentsSoFar ] 调用),然后使用递归重新绑定 nstudentsSoFar 以减少 n 而 "pushing" 更多学生到 studentsSoFar.

表达式 studentsSoFar ++ [student] 本身不会做任何事情,但是通过在递归 getStudents 调用中使用它,这个新值可以重新绑定为 studentsSoFar 来模拟变化这个 "variable".

的值

无论如何,这在 Haskell 中是一种非常标准的方法,但对于来自 JavaScript 或其他语言的人来说可能不常见,因此值得在 [=88] 之前学习涵盖递归的教程=]...像"Learn You"(第5章的递归,第9章的I/O)或"Happy Learn"(第10章的递归,第15章和第20章的I/O)或"Haskell Programming from First Principles" (recursion in Chapter 8, I/O in Chapter 29) or "Programming in Haskell"(第六章递归,第十章I/O)。我确定您在这里看到了模式。