Ответ 1
Я думаю, что у вас есть фундаментальное недоразумение в отношении IO в Haskell. В частности, вы говорите следующее:
Может быть, есть функция, которая может конвертировать из 'IO String' в [ Char]?
Нет, не существует 1 и тот факт, что такой функции нет, является одной из самых важных вещей о Haskell.
Haskell - очень принципиальный язык. Он пытается поддерживать различие между "чистыми" функциями (которые не имеют побочных эффектов и всегда возвращают один и тот же результат при предоставлении одного и того же ввода) и "нечистыми" функциями (которые имеют побочные эффекты, такие как чтение из файлов, печать на экран, запись на диск и т.д.). Правила:
- Вы можете использовать чистую функцию в любом месте (в других чистых функциях или в нечистых функциях)
- Вы можете использовать нечистые функции только в других нечистых функциях.
Как код, обозначенный как чистый или нечистый, использует систему типов. Когда вы видите подпись функции, например
digitToInt :: String -> Int
вы знаете, что эта функция чиста. Если вы дадите ему String
, он вернет Int
и, кроме того, он всегда вернет тот же Int
, если вы дадите ему тот же String
. С другой стороны, такая сигнатура функции, как
getLine :: IO String
нечисто, потому что возвращаемый тип String
отмечен IO
. Очевидно, что getLine
(который читает строку ввода пользователя) не всегда возвращает тот же String
, потому что он зависит от того, что пользователь вводит. Вы не можете использовать эту функцию в чистом коде, потому что добавление даже самого маленького бита примеси загрязнит чистый код. Когда вы идете IO
, вы никогда не сможете вернуться.
Вы можете думать о IO
как обертке. Когда вы видите конкретный тип, например, x :: IO String
, вы должны интерпретировать это как означающее "x
- это действие, которое при выполнении делает некоторые произвольные операции ввода-вывода, а затем возвращает что-то типа String
" (примечание что в Haskell String
и [Char]
- это точно одно и то же).
Итак, как вы получаете доступ к значениям из действия IO
? К счастью, тип функции main
равен IO ()
(это действие, которое выполняет некоторые операции ввода-вывода и возвращает ()
, что равнозначно возврату ничего). Поэтому вы всегда можете использовать свои функции IO
внутри main
. Когда вы выполняете программу Haskell, вы выполняете функцию main
, которая фактически выполняет все операции ввода-вывода в определении программы - например, вы можете читать и писать из файлов, запрашивать у пользователя ввод, запись в stdout и т.д. и т.д.
Вы можете думать о структурировании программы Haskell следующим образом:
- Весь код, который делает I/O, получает тег
IO
(в основном, вы помещаете его в блокdo
) - Код, который не нуждается в выполнении ввода/вывода, не обязательно должен быть в блоке
do
- это "чистые" функции. - Функциональные последовательности
main
объединяют действия ввода-вывода, которые вы определили, в порядке, в котором программа делает то, что вы хотите (вкратце с чистыми функциями, где угодно). - При запуске
main
вы вызываете все эти действия ввода/вывода.
Итак, учитывая все это, как вы пишете свою программу? Ну, функция
readFile :: FilePath -> IO String
читает файл как String
. Поэтому мы можем использовать это, чтобы получить содержимое файла. Функция
lines:: String -> [String]
разбивает a String
на строки новой строки, так что теперь у вас есть список String
s, каждый из которых соответствует одной строке файла. Функция
init :: [a] -> [a]
Отбрасывает последний элемент из списка (это избавит от окончательного .
в каждой строке). Функция
read :: (Read a) => String -> a
принимает значение String
и превращает его в произвольный тип данных Haskell, например Int
или Bool
. Сочетание этих функций разумно даст вам вашу программу.
Обратите внимание, что единственный раз, когда вам действительно нужно делать какие-либо операции ввода-вывода, это когда вы читаете файл. Поэтому это единственная часть программы, которая должна использовать тег IO
. Остальную часть программы можно записать "чисто".
Похоже, что вам нужна статья IO Monad для людей, которые просто не заботятся, что должно объяснить многие ваши вопросы. Не пугайтесь термина "монада" - вам не нужно понимать, что такое монада для написания программ Haskell (обратите внимание, что этот абзац является единственным в моем ответе, который использует слово "монада", хотя, по общему признанию, я использовали его четыре раза сейчас...)
Здесь программа, которая (я думаю) вы хотите написать
run :: IO (Int, Int, [(Int,Int,Int)])
run = do
contents <- readFile "text.txt" -- use '<-' here so that 'contents' is a String
let [a,b,c] = lines contents -- split on newlines
let firstLine = read (init a) -- 'init' drops the trailing period
let secondLine = read (init b)
let thirdLine = read (init c) -- this reads a list of Int-tuples
return (firstLine, secondLine, thirdLine)
Чтобы ответить npfedwards
комментарий о применении lines
к выходу readFile text.txt
, вам нужно понять, что readFile text.txt
предоставляет IO String
, и это только при привязке его к переменной (используя contents <-
), что вы получите доступ к базовому String
, чтобы вы могли применить к нему lines
.
Помните: как только вы идете IO
, вы никогда не вернетесь.
1 Я намеренно игнорирую unsafePerformIO
, потому что, как видно из названия, это очень опасно! Никогда не используйте его, если вы действительно не знаете, что делаете.