Категории
Самые читаемые
PochitayKnigi » Компьютеры и Интернет » Программирование » Изучай Haskell во имя добра! - Миран Липовача

Изучай Haskell во имя добра! - Миран Липовача

Читать онлайн Изучай Haskell во имя добра! - Миран Липовача

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 45 46 47 48 49 50 51 52 53 ... 96
Перейти на страницу:

Извините, но правильный ответ 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 есть байтовые строки. Они похожи на списки, но каждый элемент имеет размер один байт. Также списки и байтовые строки по-разному реализуют ленивость.

Строгие и ленивые

Байтовые строки бывают двух видов: строгие и ленивые. Строгие байтовые строки объявлены в модуле Data.ByteString, и они полностью не ленивые. Не используется никаких «обещаний», строгая строка байтов представляет собой последовательность байтов в массиве. Подобная строка не может быть бесконечной. Если вы вычисляете первый байт из строгой строки, вы должны вычислить её целиком. Положительный момент – меньше накладных расходов, поскольку не используются «обещания». Отрицательный момент – такие строки заполнят память быстрее, так как они считываются целиком.

Второй вид байтовых строк определён в модуле Data.ByteString. Lazy. Они ленивы – но не настолько, как списки. Как мы говорили ранее, в списке столько же «обещаний», сколько элементов. Вот почему это может сделать его медленным для некоторых целей. Ленивые строки байтов применяют другой подход: они хранятся блоками размером 64 Кб. Если вы вычисляете байт в ленивой байтовой строке (печатая или другим способом), то будут вычислены первые 64 Кб. После этого будет возращено обещание вычислить остальные блоки. Ленивые байтовые строки похожи на список строгих байтовых строк размером 64 Кб. При обработке файла ленивыми байтовыми строками файл будет считываться блок за блоком. Это удобно, потому что не вызывает резкого увеличения потребления памяти, и 64 Кб, вероятно, влезет в L2 – кэш вашего процессора.

Если вы посмотрите документацию на модуль Data.ByteString. Lazy, то увидите множество функций с такими же именами, как и в модуле Data.List, только в сигнатурах функций будет указан тип ByteString вместо [a] и Word8 вместо a. Функции в этом модуле работают со значениями типа ByteString так же, как одноимённые функции – со списками. Поскольку имена совпадают, нам придётся сделать уточнённый импорт в скрипте и затем загрузить этот скрипт в интерпретатор GHCi для того, чтобы поэкспериментировать с типом ByteString.

import qualified Data.ByteString.Lazy as B

import qualified Data.ByteString as S

Модуль B содержит ленивые строки байтов и функции, модуль S – строгие. Главным образом мы будем использовать ленивую версию.

Функция pack имеет сигнатуру pack :: [Word8] –> ByteString. Это означает, что она принимает список байтов типа Word8 и возвращает значение типа ByteString. Можно думать, будто функция принимает ленивый список и делает его менее ленивым, так что он ленив только блоками по 64 Кб.

Что за тип Word8? Он похож на Int, но имеет значительно меньший диапазон, а именно 0 – 255. Тип представляет собой восьми битовое число. Так же как и Int, он имеет экземпляр класса Num. Например, мы знаем, что число 5 полиморфно, а значит, оно может вести себя как любой числовой тип. В том числе – принимать тип Word8.

ghci> B.pack [99,97,110]

Chunk "can" Empty

ghci> B.pack [98..120]

Chunk "bcdefghijklmnopqrstuvwx" Empty

Как можно видеть, Word8 не доставляет много хлопот, поскольку система типов определяет, что числа должны быть преобразованы к нему. Если вы попытаетесь использовать большое число, например 336, в качестве значения типа Word8, число будет взято по модулю 256, то есть сохранится 80.

Мы упаковали всего несколько значений в тип ByteString; они уместились в один блок. Значение Empty – это нечто вроде [] для списков.

Если нужно просмотреть байтовую строку байт за байтом, её нужно распаковать. Функция unpack обратна функции pack. Она принимает строку байтов и возвращает список байтов. Вот пример:

ghci> let by = B.pack [98,111,114,116]

ghci> by

Chunk "bort" Empty

ghci> B.unpack by

[98,111,114,116]

Вы также можете преобразовывать байтовые строки из строгих в ленивые и наоборот. Функция fromChunks принимает список строгих строк и преобразует их в ленивую строку. Соответственно, функция toChunks принимает ленивую строку байтов и преобразует её в список строгих строк.

ghci> B.fromChunks [S.pack [40,41,42], S.pack [43,44,45], S.pack [46,47,48]]

Chunk "()*" (Chunk "+,–" (Chunk "./0" Empty))

Это полезно, если у вас есть множество маленьких строгих строк байтов и вы хотите эффективно обработать их, не объединяя их в памяти в одну большую строгую строку.

Аналог конструктора : для строк байтов называется cons. Он принимает байт и строку байтов и помещает байт в начало строки.

ghci> B.cons 85 $ B.pack [80,81,82,84]

Chunk "U" (Chunk "PQRT" Empty)

Модули для работы со строками байтов содержат большое количество функций, аналогичных функциям в модуле Data.List, включая следующие (но не ограничиваясь ими): head, tail, init, null, length, map, reverse, foldl, foldr, concat, takeWhile, filter и др.

Есть и функции, имя которых совпадает с именем функций из модуля System.IO, и работают они аналогично, только строки заменены значениями типа ByteString. Например, функция readFile в модуле System.IO имеет тип

1 ... 45 46 47 48 49 50 51 52 53 ... 96
Перейти на страницу:
Тут вы можете бесплатно читать книгу Изучай Haskell во имя добра! - Миран Липовача.
Комментарии