Подтвердить что ты не робот

Как я могу проанализировать строку IO в Haskell?

У меня проблема с Haskell. У меня есть текстовый файл, выглядящий так:

5.
7. 
[(1,2,3),(4,5,6),(7,8,9),(10,11,12)].

Я не знаю, как я могу получить первые 2 числа (2 и 7 выше) и список из последней строки. В конце каждой строки есть точки.

Я попытался создать парсер, но функция, называемая 'readFile', возвращает Monad с именем IO String. Я не знаю, как я могу получить информацию из этого типа строки.

Я предпочитаю работать над массивом символов. Может быть, есть функция, которая может конвертировать из "IO String" в [ Char]?

4b9b3361

Ответ 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, потому что, как видно из названия, это очень опасно! Никогда не используйте его, если вы действительно не знаете, что делаете.

Ответ 2

Как нуб-программист, меня тоже смутили IO. Просто помните, что если вы идете IO вы никогда не выходите. Крис написал отличное объяснение почему. Я просто подумал, что это может помочь привести некоторые примеры использования IO String в монаде. Я буду использовать getLine, который читает пользовательский ввод и возвращает IO String.

line <- getLine 

Все, что это делает - привязывает ввод пользователя из getLine к значению с именем line. Если вы наберете this в ghci и наберете :type line он вернет:

:type line
line :: String

Но ждать! getLine возвращает IO String

:type getLine
getLine :: IO String

Так что же случилось с IO Несс от getLine? <- это то, что случилось. <- твой IO друг. Это позволяет вам вывести значение, которое портит IO внутри монады, и использовать его с вашими обычными функциями. Монады легко идентифицируются, потому что они начинаются с do. Вот так:

main = do
    putStrLn "How much do you love Haskell?"
    amount <- getLine
    putStrln ("You love Haskell this much: " ++ amount) 

Если вы похожи на меня, вы скоро обнаружите, что liftIO - ваш следующий лучший друг-монада, и что $ help уменьшает количество скобок, которые вам нужно написать.

Итак, как вы получаете информацию из readFile? Хорошо, если вывод readFile - IO String примерно так:

:type readFile
readFile :: FilePath -> IO String

Тогда все, что вам нужно, это ваш дружелюбный <-:

 yourdata <- readFile "samplefile.txt"

Теперь, если ввести это в ghci и проверить тип yourdata вы заметите, что это простая String.

:type yourdata
text :: String

Ответ 3

Как уже говорят люди, если у вас есть две функции, одна из них - readStringFromFile :: FilePath -> IO String, а другая - doTheRightThingWithString :: String -> Something, тогда вам действительно не нужно избегать строки из IO, так как вы можете объединить эти две функции по-разному:

С fmap для IO (IO is Functor):

fmap doTheRightThingWithString readStringFromFile

С (<$>) для IO (IO есть Applicative и (<$>) == fmap):

import Control.Applicative

...

doTheRightThingWithString <$> readStringFromFile

С liftM для IO (liftM == fmap):

import Control.Monad

...

liftM doTheRightThingWithString readStringFromFile

С (>>=) для IO (IO is Monad, fmap == (<$>) == liftM == \f m -> m >>= return . f):

readStringFromFile >>= \string -> return (doTheRightThingWithString string)
readStringFromFile >>= \string -> return $ doTheRightThingWithString string
readStringFromFile >>= return . doTheRightThingWithString
return . doTheRightThingWithString =<< readStringFromFile

Обозначение do:

do
  ...
  string <- readStringFromFile
  -- ^ you escape String from IO but only inside this do-block
  let result = doTheRightThingWithString string
  ...
  return result

Каждый раз, когда вы получите IO Something.

Почему вы хотели бы сделать это так? Ну, с этим у вас будет чистое и ссылочно прозрачные программы (функции) на вашем языке. Это означает, что каждая функция типа IO-free является чистой и ссылочной прозрачностью, поэтому для тех же аргументов она возвращает те же значения. Например, doTheRightThingWithString вернет тот же Something для того же String. Однако readStringFromFile, который не является IO-бесплатным, может возвращать разные строки каждый раз (потому что файл может измениться), так что вы не можете избежать такого нечеткого значения из IO.

Ответ 4

Если у вас есть синтаксический анализатор этого типа:

myParser :: String -> Foo

и вы читаете файл, используя

readFile "thisfile.txt"

тогда вы можете прочитать и проанализировать файл, используя

fmap myParser (readFile "thisfile.txt")

Результат этого будет иметь тип IO Foo.

fmap означает myParser выполняет "внутри" IO.

Еще один способ подумать, что в то время как myParser :: String -> Foo, fmap myParser :: IO String -> IO Foo.