Раздел 21 · Haskell

IO: do для мира и чистое ядро

middle~35 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

IO: do для мира и чистое ядро

Финал блока абстракций и мост к первому проекту. Весь раздел мы держали программу чистой, а ввод-вывод называли грязным миром и обходили стороной. Пора посмотреть на него прямо. Главная мысль урока: IO a это не действие, которое уже случилось, а рецепт действия, обычное значение, которое выполняет только рантайм. В этом уроке: getLine, putStrLn, чтение и запись файлов, аргументы командной строки, interact, и архитектурный приём всего раздела, отделение чистого ядра от грязной оболочки.

Идея

В любом другом языке print("привет") это инструкция: дошёл до строки, напечатал. В Haskell так нельзя, и вот почему. Чистая функция обязана давать один и тот же ответ на одни и те же аргументы и ничего не менять в мире. А getLine каждый раз возвращает разное, putStrLn меняет экран. Это прямое нарушение чистоты. Как Haskell мирится с этим, не отказываясь от чистоты?

Хитрость в том, что IO a это не выполненное действие, а его описание. Значение типа IO String это рецепт: вот как добыть строку из мира, когда придёт время. Сам рецепт чистый, его можно передавать, складывать, хранить в списке. Готовку запускает ровно одно место, рантайм, когда исполняет main:

main :: IO ()
main = putStrLn "привет, мир"

main имеет тип IO (): рецепт, который при исполнении даёт пустой результат () (юнит, ничего полезного) и попутно меняет мир. putStrLn "привет" это не печать, это значение, описывающее печать. Программа целиком это один большой рецепт main, который рантайм разворачивает и исполняет. IO это рецепт, а не выполнение.

IO это монада, и do работает как раньше

Хорошая новость: ты уже всё умеешь. IO это монада из прошлого урока, и do-нотация над ней это та же самая do-нотация:

main :: IO ()
main = do
  putStrLn "Как тебя зовут?"
  name <- getLine
  putStrLn ("Привет, " <> name <> "!")

Разбери по правилам разворачивания из урока 12. name <- getLine это getLine >>= \name -> ...: исполни рецепт getLine, получи строку, назови её name. Строки без <- (как putStrLn ...) это шаги через >>, чей результат () никому не нужен. Никакой новой механики: IO это просто ещё одна монада, у которой эффект это взаимодействие с миром.

Базовый словарь ввода-вывода:

putStrLn :: String -> IO ()        -- напечатать строку и перевод строки
putStr   :: String -> IO ()        -- без перевода строки
print    :: Show a => a -> IO ()   -- putStrLn . show, для любого Show
getLine  :: IO String              -- прочитать строку со ввода

print это putStrLn . show: печатает что угодно с инстансом Show из урока про типоклассы. Вот зачем deriving Show нужен в каждом учебном типе.

премиум-урок

Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.

Оплата пока не подключена: премиум и курсы открываются промокодом.