Мы проверяем, равняется ли number
случайно сгенерированному числу, и выдаём пользователю соответствующее сообщение. Затем рекурсивно вызываем нашу функцию askForNumber
, но на сей раз с вновь полученным генератором; это возвращает нам такое же действие ввода-вывода, как мы только что выполнили, но основанное на новом генераторе. Затем это действие выполняется.
Функция main
состоит всего лишь из получения генератора случайных чисел от системы и вызова функции askForNumber
с этим генератором для того, чтобы получить первое действие.
Посмотрим, как работает наша программа!
$ ./guess_the_number
Я задумал число от 1 до 10. Какое?
4
Извините, но правильный ответ 3
Я задумал число от 1 до 10. Какое?
10
Правильно!
Я задумал число от 1 до 10. Какое?
2
Извините, но правильный ответ 4
Я задумал число от 1 до 10. Какое?
5
Извините, но правильный ответ 10
Я задумал число от 1 до 10. Какое?
Можно написать эту же программу по-другому:
import System.Random
import Control.Monad (when)
main = do
gen <- getStdGen
let (randNumber, _) = randomR (1,10) gen :: (Int, StdGen)
putStr "Я задумал число от 1 до 10. Какое? "
numberString <- getLine
when (not $ null numberString) $ do
let number = read numberString
if randNumber == number
then putStrLn "Правильно!"
else putStrLn $ "Извините, но правильный ответ "
++ show randNumber
newStdGen
main
Эта версия очень похожа на предыдущую, но вместо создания функции, которая принимает генератор и вызывает сама себя рекурсивно с вновь полученным генератором, мы производим все действия внутри функции main
. После того как пользователь получит ответ, угадал ли он число, мы обновим глобальный генератор и снова вызовем функцию main
. Оба подхода хороши, но мне больше нравится первый способ, так как он предусматривает меньше действий в функции main
и даёт нам функцию, которую мы можем легко использовать повторно.
Bytestring: тот же String, но быстрее
Список – полезная и удобная структура данных. Мы использовали списки почти что везде. Существует очень много функций, работающих со списками, и ленивость языка Haskell позволяет нам заменить циклы типа for
и while
из других языков программирования на фильтрацию и отображение списков, потому что вычисление произойдёт только тогда, когда оно действительно понадобится. Вот почему такие вещи, как бесконечные списки (и даже бесконечные списки бесконечных списков!) для нас не проблема. По той же причине списки могут быть использованы в качестве потоков, читаем ли мы со стандартного ввода или из файла. Мы можем открыть файл и считать его как строку, но на самом деле обращение к файлу будет происходить только по мере необходимости.
Тем не менее обработка файлов как строк имеет один недостаток: она может оказаться медленной. Как вы знаете, тип String
– это просто синоним для типа [Char]
. У символов нет фиксированного размера, так как для представления, скажем, символа в кодировке Unicode может потребоваться несколько байтов. Более того, список – ленивая структура. Если у вас есть, например, список [1,2,3,4]
, он будет вычислен только тогда, когда это необходимо. На самом деле список, в некотором смысле, – это обещание списка. Вспомним, что [1,2,3,4]
– это всего лишь синтаксический сахар для записи 1:2:3:4:[]
. Когда мы принудительно выполняем вычисление первого элемента списка (например, выводим его на экран), остаток списка 2:3:4:[]
также представляет собой «обещание списка», и т. д. Список всего лишь обещает, что следующий элемент будет вычислен, как только он действительно понадобится, причём вместе с элементом будет создано обещание следующего элемента. Не нужно прилагать больших умственных усилий, чтобы понять, что обработка простого списка чисел как серии обещаний – не самая эффективная вещь на свете!
Все эти накладные расходы, связанные со списками, обычно нас не волнуют, но при чтении больших файлов и манипулировании ими это становится помехой. Вот почему в языке Haskell есть байтовые строки. Они похожи на списки, но каждый элемент имеет размер один байт. Также списки и байтовые строки по-разному реализуют ленивость.
Строгие и ленивые